From 3606875e1753c40e6381642c4ded4e0557e4fe89 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Wed, 21 Feb 2024 20:36:24 +0200 Subject: [PATCH 01/50] refactor: metadata squashing --- index.d.ts | 13 +- src/builtin-plugins/docblock.ts | 183 -- src/builtin-plugins/index.ts | 1 - src/builtin-plugins/sourceCode.ts | 2 + src/metadata/MetadataSquasher.ts | 66 - src/metadata/constants.ts | 6 - src/metadata/docblock/index.ts | 140 ++ src/metadata/index.ts | 2 +- src/metadata/mergers.ts | 81 - .../proxies/AllureTestItemMetadataProxy.ts | 9 +- src/metadata/squasher/MetadataSelector.ts | 185 ++ src/metadata/squasher/MetadataSquasher.ts | 114 + .../mergeTestCaseMetadata.test.ts.snap | 1849 +++++++++++++++++ .../mergeTestFileMetadata.test.ts.snap | 248 +++ src/metadata/squasher/__tests__/fixtures.ts | 213 ++ .../__tests__/mergeTestCaseMetadata.test.ts | 134 ++ .../__tests__/mergeTestFileMetadata.test.ts | 75 + src/metadata/squasher/index.ts | 1 + src/metadata/squasher/mergers.ts | 161 ++ src/metadata/utils/getStage.ts | 20 - src/metadata/utils/getStart.ts | 19 - src/metadata/utils/getStatusAndDetails.ts | 51 - src/metadata/utils/getStop.ts | 20 - src/metadata/utils/index.ts | 4 - src/options/default-options/plugins.ts | 1 - src/options/default-options/testStep.ts | 4 +- src/reporter/JestAllure2Reporter.ts | 7 +- src/runtime/modules/CoreModule.ts | 6 +- src/utils/index.ts | 1 + src/utils/weakMemoize.test.ts | 26 + src/utils/weakMemoize.ts | 19 + 31 files changed, 3191 insertions(+), 470 deletions(-) delete mode 100644 src/builtin-plugins/docblock.ts create mode 100644 src/builtin-plugins/sourceCode.ts delete mode 100644 src/metadata/MetadataSquasher.ts create mode 100644 src/metadata/docblock/index.ts delete mode 100644 src/metadata/mergers.ts create mode 100644 src/metadata/squasher/MetadataSelector.ts create mode 100644 src/metadata/squasher/MetadataSquasher.ts create mode 100644 src/metadata/squasher/__tests__/__snapshots__/mergeTestCaseMetadata.test.ts.snap create mode 100644 src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap create mode 100644 src/metadata/squasher/__tests__/fixtures.ts create mode 100644 src/metadata/squasher/__tests__/mergeTestCaseMetadata.test.ts create mode 100644 src/metadata/squasher/__tests__/mergeTestFileMetadata.test.ts create mode 100644 src/metadata/squasher/index.ts create mode 100644 src/metadata/squasher/mergers.ts delete mode 100644 src/metadata/utils/getStage.ts delete mode 100644 src/metadata/utils/getStart.ts delete mode 100644 src/metadata/utils/getStatusAndDetails.ts delete mode 100644 src/metadata/utils/getStop.ts delete mode 100644 src/metadata/utils/index.ts create mode 100644 src/utils/weakMemoize.test.ts create mode 100644 src/utils/weakMemoize.ts diff --git a/index.d.ts b/index.d.ts index b00b9e43..fccd04f9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -173,12 +173,12 @@ declare module 'jest-allure2-reporter' { stop: TestCaseExtractor; /** * Extractor for the test case description. - * @example ({ testCaseMetadata }) => '```js\n' + testCaseMetadata.code + '\n```' + * @example ({ testCaseMetadata }) => '```js\n' + testCaseMetadata.sourceCode + '\n```' */ description: TestCaseExtractor; /** * Extractor for the test case description in HTML format. - * @example ({ testCaseMetadata }) => '
' + testCaseMetadata.code + '
' + * @example ({ testCaseMetadata }) => '
' + testCaseMetadata.sourceCode + '
' */ descriptionHtml: TestCaseExtractor; /** @@ -515,13 +515,15 @@ declare module 'jest-allure2-reporter' { /** * Recursive data structure to represent test steps for more granular reporting. */ - steps?: Omit[]; + steps?: AllureNestedTestStepMetadata[]; /** * Stop timestamp in milliseconds. */ stop?: number; } + export type AllureNestedTestStepMetadata = Omit; + /** @inheritDoc */ export interface AllureTestStepMetadata extends AllureTestItemMetadata { /** @@ -540,10 +542,7 @@ declare module 'jest-allure2-reporter' { } /** @inheritDoc */ - export interface AllureTestFileMetadata extends AllureTestCaseMetadata { - code?: never; - steps?: never; - } + export interface AllureTestFileMetadata extends AllureTestCaseMetadata {} export interface AllureGlobalMetadata { config: Pick; diff --git a/src/builtin-plugins/docblock.ts b/src/builtin-plugins/docblock.ts deleted file mode 100644 index 9965cb78..00000000 --- a/src/builtin-plugins/docblock.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-imports,import/no-extraneous-dependencies */ -import fs from 'node:fs/promises'; - -import type { - AllureTestItemMetadata, - AllureTestCaseMetadata, - AllureTestFileMetadata, - AllureTestStepMetadata, - DocblockContext, - Label, - LabelName, - Link, - LinkType, - Plugin, - PluginConstructor, -} from 'jest-allure2-reporter'; - -import { splitDocblock } from '../utils'; - -export const docblockPlugin: PluginConstructor = () => { - let parse: DocblockParser | undefined; - - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/docblock', - async globalContext() { - parse = await initParser(); - }, - async testFileContext(context) { - const testFilePath = context.testFile.testFilePath; - const fileContents = await fs.readFile(testFilePath, 'utf8'); - if (parse && hasDocblockAtStart(fileContents)) { - context.testFileDocblock = parse(fileContents); - mergeIntoTestFile(context.testFileMetadata, context.testFileDocblock); - } - }, - async testCaseContext(context) { - if (parse && context.testCaseMetadata.sourceCode) { - context.testCaseDocblock = parse(context.testCaseMetadata.sourceCode); - mergeIntoTestCase(context.testCaseMetadata, context.testCaseDocblock); - } - }, - async testStepContext(context) { - if (parse && context.testStepMetadata.sourceCode) { - context.testStepDocblock = parse(context.testStepMetadata.sourceCode); - mergeIntoTestStep(context.testStepMetadata, context.testStepDocblock); - } - }, - }; - - return plugin; -}; - -function hasDocblockAtStart(string_: string) { - return /^\s*\/\*\*/.test(string_); -} - -function mergeIntoTestItem( - metadata: AllureTestItemMetadata, - comments: string, - pragmas: Record, - rawDocblock: string, - shouldLeaveComments: boolean, -) { - if (comments) { - metadata.description ??= []; - metadata.description.push(comments); - } - - if (pragmas.description) { - metadata.description ??= []; - metadata.description.push(...pragmas.description); - } - - if (metadata.sourceCode && rawDocblock) { - const [left, right, ...rest] = metadata.sourceCode.split(rawDocblock); - const leftTrimmed = left.trimEnd(); - const replacement = shouldLeaveComments - ? `/** ${comments.trimStart()} */\n` - : '\n'; - const joined = right ? [leftTrimmed, right].join(replacement) : leftTrimmed; - metadata.sourceCode = [joined, ...rest].join('\n'); - } -} - -function mergeIntoTestFile( - metadata: AllureTestFileMetadata, - docblock: DocblockContext | undefined, -) { - return mergeIntoTestCase(metadata, docblock); -} - -function mergeIntoTestCase( - metadata: AllureTestCaseMetadata, - docblock: DocblockContext | undefined, -) { - const { raw = '', comments = '', pragmas = {} } = docblock ?? {}; - mergeIntoTestItem(metadata, comments, pragmas, raw, false); - - const epic = pragmas.epic?.map(createLabelMapper('epic')) ?? []; - const feature = pragmas.feature?.map(createLabelMapper('feature')) ?? []; - const owner = pragmas.owner?.map(createLabelMapper('owner')) ?? []; - const severity = pragmas.severity?.map(createLabelMapper('severity')) ?? []; - const story = pragmas.story?.map(createLabelMapper('story')) ?? []; - const tag = pragmas.tag?.map(createLabelMapper('tag')) ?? []; - const labels = [...epic, ...feature, ...owner, ...severity, ...story, ...tag]; - if (labels.length > 0) { - metadata.labels ??= []; - metadata.labels.push(...labels); - } - - const issue = pragmas.issue?.map(createLinkMapper('issue')) ?? []; - const tms = pragmas.tms?.map(createLinkMapper('tms')) ?? []; - const links = [...issue, ...tms]; - if (links.length > 0) { - metadata.links ??= []; - metadata.links.push(...links); - } - - if (pragmas.descriptionHtml) { - metadata.descriptionHtml ??= []; - metadata.descriptionHtml.push(...pragmas.descriptionHtml); - } -} - -function createLabelMapper(name: LabelName) { - return (value: string): Label => ({ name, value }); -} - -function createLinkMapper(type?: LinkType) { - return (url: string): Link => ({ type, url, name: url }); -} - -function mergeIntoTestStep( - metadata: AllureTestStepMetadata, - docblock: DocblockContext | undefined, -) { - const { raw = '', comments = '', pragmas = {} } = docblock ?? {}; - mergeIntoTestItem(metadata, comments, pragmas, raw, true); -} - -type DocblockParser = (raw: string) => DocblockContext | undefined; - -async function initParser(): Promise { - try { - const jestDocblock = await import('jest-docblock'); - return (snippet) => { - const [jsdoc] = splitDocblock(snippet); - const result = jestDocblock.parseWithComments(jsdoc); - return { - raw: jsdoc, - comments: result.comments, - pragmas: normalize(result.pragmas), - }; - }; - } catch (error: any) { - // TODO: log warning - if (error?.code === 'MODULE_NOT_FOUND') { - return () => void 0; - } - - throw error; - } -} - -const SPLITTERS: Record string[]> = { - tag: (string_) => string_.split(/\s*,\s*/), -}; - -function normalize( - pragmas: Record, -): Record { - const result: Record = {}; - - for (const [key, value] of Object.entries(pragmas)) { - result[key] = Array.isArray(value) ? value : [value]; - const splitter = SPLITTERS[key]; - if (splitter) { - result[key] = result[key].flatMap(splitter); - } - } - - return result; -} diff --git a/src/builtin-plugins/index.ts b/src/builtin-plugins/index.ts index 40dda1dd..d9f9300d 100644 --- a/src/builtin-plugins/index.ts +++ b/src/builtin-plugins/index.ts @@ -1,5 +1,4 @@ export { detectPlugin as detect } from './detect'; -export { docblockPlugin as docblock } from './docblock'; export { githubPlugin as github } from './github'; export { manifestPlugin as manifest } from './manifest'; export { prettierPlugin as prettier } from './prettier'; diff --git a/src/builtin-plugins/sourceCode.ts b/src/builtin-plugins/sourceCode.ts new file mode 100644 index 00000000..964f5576 --- /dev/null +++ b/src/builtin-plugins/sourceCode.ts @@ -0,0 +1,2 @@ +// TODO: implement source code plugin +// It should go over the test steps, and unite the code snippets into a single diff --git a/src/metadata/MetadataSquasher.ts b/src/metadata/MetadataSquasher.ts deleted file mode 100644 index cf082561..00000000 --- a/src/metadata/MetadataSquasher.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { HookInvocationMetadata } from 'jest-metadata'; -import type { TestFileMetadata, TestInvocationMetadata } from 'jest-metadata'; -import type { - AllureTestCaseMetadata, - AllureTestFileMetadata, - AllureTestStepMetadata, -} from 'jest-allure2-reporter'; - -import { getStage, getStart, getStatusAndDetails, getStop } from './utils'; -import { PREFIX } from './constants'; -import { - mergeTestCaseMetadata, - mergeTestFileMetadata, - mergeTestStepMetadata, -} from './mergers'; - -export class MetadataSquasher { - testFile(metadata: TestFileMetadata): AllureTestFileMetadata { - const input = [metadata.globalMetadata.get(PREFIX), metadata.get(PREFIX)]; - return (input as AllureTestFileMetadata[]).reduce( - mergeTestFileMetadata, - {}, - ); - } - - testInvocation( - fileMetadata: AllureTestFileMetadata, - metadata: TestInvocationMetadata, - ): AllureTestCaseMetadata { - const ancestors = [ - ...metadata.definition.ancestors(), - metadata.definition, - metadata, - metadata.fn, - ] - .map((item) => - item ? (item.get(PREFIX) as AllureTestCaseMetadata) : undefined, - ) - .filter(Boolean) as AllureTestCaseMetadata[]; - ancestors.unshift(fileMetadata); - - const result = ancestors.reduce(mergeTestCaseMetadata, {}); - const befores = [...metadata.beforeAll, ...metadata.beforeEach].map( - resolveTestStep, - ); - const afters = [...metadata.afterEach, ...metadata.afterAll].map( - resolveTestStep, - ); - const steps = result.steps ?? []; - - return { - ...result, - ...getStatusAndDetails(metadata), - stage: getStage(metadata), - start: getStart(metadata), - stop: getStop(metadata), - steps: [...befores, ...steps, ...afters], - }; - } -} - -const resolveTestStep = (item: HookInvocationMetadata) => - mergeTestStepMetadata( - item.definition.get(PREFIX) as AllureTestStepMetadata, - item.get(PREFIX) as AllureTestStepMetadata, - ); diff --git a/src/metadata/constants.ts b/src/metadata/constants.ts index 807f76ee..9d519e83 100644 --- a/src/metadata/constants.ts +++ b/src/metadata/constants.ts @@ -1,11 +1,5 @@ export const PREFIX = 'allure2' as const; -export const START = [PREFIX, 'start'] as const; -export const STOP = [PREFIX, 'stop'] as const; -export const STAGE = [PREFIX, 'stage'] as const; -export const STATUS = [PREFIX, 'status'] as const; -export const STATUS_DETAILS = [PREFIX, 'statusDetails'] as const; - export const CURRENT_STEP = [PREFIX, 'currentStep'] as const; export const DESCRIPTION = [PREFIX, 'description'] as const; export const DESCRIPTION_HTML = [PREFIX, 'descriptionHtml'] as const; diff --git a/src/metadata/docblock/index.ts b/src/metadata/docblock/index.ts new file mode 100644 index 00000000..3758fa16 --- /dev/null +++ b/src/metadata/docblock/index.ts @@ -0,0 +1,140 @@ +// import {splitDocblock} from "../../utils"; +// import type { +// AllureTestCaseMetadata, +// AllureTestFileMetadata, +// AllureTestItemMetadata, AllureTestStepMetadata, +// DocblockContext, Label, LabelName, Link, LinkType +// } from "jest-allure2-reporter"; +// +// async function initParser(): Promise { +// try { +// const jestDocblock = await import('jest-docblock'); +// return (snippet) => { +// const [jsdoc] = splitDocblock(snippet); +// const result = jestDocblock.parseWithComments(jsdoc); +// return { +// raw: jsdoc, +// comments: result.comments, +// pragmas: normalize(result.pragmas), +// }; +// }; +// } catch (error: any) { +// // TODO: log warning +// if (error?.code === 'MODULE_NOT_FOUND') { +// return () => void 0; +// } +// +// throw error; +// } +// } +// +// const SPLITTERS: Record string[]> = { +// tag: (string_) => string_.split(/\s*,\s*/), +// }; +// +// function normalize( +// pragmas: Record, +// ): Record { +// const result: Record = {}; +// +// for (const [key, value] of Object.entries(pragmas)) { +// result[key] = Array.isArray(value) ? value : [value]; +// const splitter = SPLITTERS[key]; +// if (splitter) { +// result[key] = result[key].flatMap(splitter); +// } +// } +// +// return result; +// } +// +// function hasDocblockAtStart(string_: string) { +// return /^\s*\/\*\*/.test(string_); +// } +// +// function mergeIntoTestItem( +// metadata: AllureTestItemMetadata, +// comments: string, +// pragmas: Record, +// rawDocblock: string, +// shouldLeaveComments: boolean, +// ) { +// if (comments) { +// metadata.description ??= []; +// metadata.description.push(comments); +// } +// +// if (pragmas.description) { +// metadata.description ??= []; +// metadata.description.push(...pragmas.description); +// } +// +// if (metadata.sourceCode && rawDocblock) { +// const [left, right, ...rest] = metadata.sourceCode.split(rawDocblock); +// const leftTrimmed = left.trimEnd(); +// const replacement = shouldLeaveComments +// ? `/** ${comments.trimStart()} */\n` +// : '\n'; +// const joined = right ? [leftTrimmed, right].join(replacement) : leftTrimmed; +// metadata.sourceCode = [joined, ...rest].join('\n'); +// } +// } +// +// function mergeIntoTestFile( +// metadata: AllureTestFileMetadata, +// docblock: DocblockContext | undefined, +// ) { +// return mergeIntoTestCase(metadata, docblock); +// } +// +// function mergeIntoTestCase( +// metadata: AllureTestCaseMetadata, +// docblock: DocblockContext | undefined, +// ) { +// const { raw = '', comments = '', pragmas = {} } = docblock ?? {}; +// mergeIntoTestItem(metadata, comments, pragmas, raw, false); +// +// const epic = pragmas.epic?.map(createLabelMapper('epic')) ?? []; +// const feature = pragmas.feature?.map(createLabelMapper('feature')) ?? []; +// const owner = pragmas.owner?.map(createLabelMapper('owner')) ?? []; +// const severity = pragmas.severity?.map(createLabelMapper('severity')) ?? []; +// const story = pragmas.story?.map(createLabelMapper('story')) ?? []; +// const tag = pragmas.tag?.map(createLabelMapper('tag')) ?? []; +// const labels = [...epic, ...feature, ...owner, ...severity, ...story, ...tag]; +// if (labels.length > 0) { +// metadata.labels ??= []; +// metadata.labels.push(...labels); +// } +// +// const issue = pragmas.issue?.map(createLinkMapper('issue')) ?? []; +// const tms = pragmas.tms?.map(createLinkMapper('tms')) ?? []; +// const links = [...issue, ...tms]; +// if (links.length > 0) { +// metadata.links ??= []; +// metadata.links.push(...links); +// } +// +// if (pragmas.descriptionHtml) { +// metadata.descriptionHtml ??= []; +// metadata.descriptionHtml.push(...pragmas.descriptionHtml); +// } +// } +// +// function createLabelMapper(name: LabelName) { +// return (value: string): Label => ({ name, value }); +// } +// +// function createLinkMapper(type?: LinkType) { +// return (url: string): Link => ({ type, url, name: url }); +// } +// +// function mergeIntoTestStep( +// metadata: AllureTestStepMetadata, +// docblock: DocblockContext | undefined, +// ) { +// const { raw = '', comments = '', pragmas = {} } = docblock ?? {}; +// mergeIntoTestItem(metadata, comments, pragmas, raw, true); +// } +// +// type DocblockParser = (raw: string) => DocblockContext | undefined; +// diff --git a/src/metadata/index.ts b/src/metadata/index.ts index 4a918c51..902082a5 100644 --- a/src/metadata/index.ts +++ b/src/metadata/index.ts @@ -1,2 +1,2 @@ export * from './proxies'; -export * from './MetadataSquasher'; +export * from './squasher'; diff --git a/src/metadata/mergers.ts b/src/metadata/mergers.ts deleted file mode 100644 index f7ec9896..00000000 --- a/src/metadata/mergers.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { - AllureTestItemMetadata, - AllureTestFileMetadata, - AllureTestCaseMetadata, - AllureTestStepMetadata, -} from 'jest-allure2-reporter'; - -export function mergeTestFileMetadata( - a: AllureTestFileMetadata, - b: AllureTestFileMetadata | undefined, -): AllureTestFileMetadata { - return b - ? { - ...mergeTestItemMetadata(a, b), - code: undefined, - steps: undefined, - workerId: b.workerId ?? a.workerId, - } - : a; -} - -export function mergeTestCaseMetadata( - a: AllureTestCaseMetadata, - b: AllureTestCaseMetadata | undefined, -): AllureTestCaseMetadata { - return b - ? { - ...mergeTestItemMetadata(a, b), - descriptionHtml: mergeArrays(a.descriptionHtml, b.descriptionHtml), - historyId: b.historyId ?? a.historyId, - labels: mergeArrays(a.labels, b.labels), - links: mergeArrays(a.links, b.links), - workerId: b.workerId ?? a.workerId, - } - : a; -} - -export function mergeTestStepMetadata( - a: AllureTestStepMetadata, - b: AllureTestStepMetadata | undefined, -): AllureTestStepMetadata { - return b - ? { - ...mergeTestItemMetadata(a, b), - hookType: b.hookType ?? a.hookType, - } - : a; -} - -function mergeTestItemMetadata( - a: AllureTestItemMetadata, - b: AllureTestItemMetadata | undefined, -): AllureTestItemMetadata { - return b - ? { - attachments: mergeArrays(a.attachments, b.attachments), - currentStep: b.currentStep ?? a.currentStep, - sourceCode: b.sourceCode ?? a.sourceCode, - sourceLocation: b.sourceLocation ?? a.sourceLocation, - description: mergeArrays(a.description, b.description), - parameters: mergeArrays(a.parameters, b.parameters), - stage: b.stage ?? a.stage, - start: b.start ?? a.start, - status: b.status ?? a.status, - statusDetails: b.statusDetails ?? a.statusDetails, - steps: b.steps ?? a.steps, - stop: b.stop ?? a.stop, - } - : a; -} - -function mergeArrays( - a: T[] | undefined, - b: T[] | undefined, -): T[] | undefined { - if (a && b) { - return [...a, ...b]; - } - - return a ?? b; -} diff --git a/src/metadata/proxies/AllureTestItemMetadataProxy.ts b/src/metadata/proxies/AllureTestItemMetadataProxy.ts index d13b643d..0b9fce3d 100644 --- a/src/metadata/proxies/AllureTestItemMetadataProxy.ts +++ b/src/metadata/proxies/AllureTestItemMetadataProxy.ts @@ -25,10 +25,11 @@ export class AllureTestItemMetadataProxy< return localPath ? `${this.$metadata.id}:${localPath}` : this.$metadata.id; } - $bind(): AllureTestItemMetadataProxy { - return new AllureTestItemMetadataProxy(this.$metadata, [ - ...this.$metadata.get(CURRENT_STEP, []), - ]); + $bind(step?: null): AllureTestItemMetadataProxy { + return new AllureTestItemMetadataProxy( + this.$metadata, + step === null ? [] : [...this.$metadata.get(CURRENT_STEP, [])], + ); } $startStep(): this { diff --git a/src/metadata/squasher/MetadataSelector.ts b/src/metadata/squasher/MetadataSelector.ts new file mode 100644 index 00000000..2a5ed89c --- /dev/null +++ b/src/metadata/squasher/MetadataSelector.ts @@ -0,0 +1,185 @@ +import type { AllureTestItemMetadata, AllureTestStepMetadata } from 'jest-allure2-reporter'; + +import { weakMemoize } from '../../utils'; + +export type MetadataSelectorOptions = { + empty(): Result; + getMetadata(metadata: Metadata): Result | undefined; + getDocblock(metadata: Metadata): Result | undefined; + mergeUnsafe(a: Result, b: Result | undefined): Result; +}; + +export interface HasParent { + readonly parent?: LikeDescribeBlock; +} + +export type LikeDescribeBlock = Metadata & HasParent; + +export type LikeTestDefinition = Metadata & { + readonly describeBlock: LikeDescribeBlock; +}; + +export type LikeStepInvocation = Metadata & { + readonly definition: Metadata; +}; + +export type LikeTestFile = Metadata & { + readonly globalMetadata: Metadata; +}; + +export type LikeTestInvocation = Metadata & { + allInvocations(): Iterable; + readonly file: LikeTestFile; + readonly beforeAll: Iterable>; + readonly beforeEach: Iterable>; + readonly afterEach: Iterable>; + readonly afterAll: Iterable>; + readonly definition: LikeTestDefinition; + readonly fn?: Metadata; +}; + +export class MetadataSelector { + constructor( + protected readonly _options: MetadataSelectorOptions, + ) { + this.belowTestInvocation = weakMemoize(this.belowTestInvocation.bind(this)); + this.testInvocationAndBelow = weakMemoize( + this.testInvocationAndBelow.bind(this), + ); + this.testInvocationAndBelowDirect = weakMemoize( + this.testInvocationAndBelowDirect.bind(this), + ); + this.testDefinitionAndBelow = weakMemoize( + this.testDefinitionAndBelow.bind(this), + ); + } + + protected readonly _getDocblock = ( + metadata: Metadata | undefined, + ): T | undefined => + metadata ? this._options.getDocblock(metadata) : undefined; + + protected readonly _getMetadataUnsafe = ( + metadata: Metadata | undefined, + ): T | undefined => + metadata ? this._options.getMetadata(metadata) : undefined; + + readonly getMetadataWithDocblock = (metadata: Metadata | undefined): T => + this._options.mergeUnsafe( + this._options.mergeUnsafe( + this._options.empty(), + this._getDocblock(metadata), + ), + this._getMetadataUnsafe(metadata), + ); + + protected _describeAncestors = weakMemoize( + (metadata: LikeDescribeBlock | undefined): T => + metadata + ? this._options.mergeUnsafe( + this._describeAncestors(metadata.parent), + this._getMetadataUnsafe(metadata), + ) + : this._options.empty(), + ); + + protected _ancestors(metadata: LikeTestInvocation): T { + return this.merge( + this.globalAndTestFile(metadata.file), + this._describeAncestors(metadata.definition.describeBlock), + ); + } + + protected _hookMetadata = (metadata: LikeStepInvocation): T => { + const definitionMetadata = this.getMetadataWithDocblock( + metadata.definition, + ); + return this.merge(definitionMetadata, this._getMetadataUnsafe(metadata)); + }; + + public readonly merge = (a: T | undefined, b: T | undefined): T => + this._options.mergeUnsafe( + this._options.mergeUnsafe(this._options.empty(), a), + b, + ); + + testInvocation(metadata: Metadata): T { + return this.getMetadataWithDocblock(metadata); + } + + testDefinition(metadata: LikeTestInvocation): T { + return this.getMetadataWithDocblock(metadata.definition); + } + + belowTestInvocation(metadata: LikeTestInvocation): T { + return [...metadata.allInvocations()] + .map(this._getMetadataUnsafe) + .reduce(this._options.mergeUnsafe, this._options.empty()); + } + + testInvocationAndBelow(metadata: LikeTestInvocation): T { + return this.merge( + this.testInvocation(metadata), + this.belowTestInvocation(metadata), + ); + } + + testInvocationAndBelowDirect(metadata: LikeTestInvocation): T { + return this.merge( + this.testInvocation(metadata), + this.getMetadataWithDocblock(metadata.fn), + ); + } + + testDefinitionAndBelow(metadata: LikeTestInvocation): T { + return this.merge( + this.testDefinition(metadata), + this.testInvocationAndBelow(metadata), + ); + } + + testDefinitionAndBelowDirect(metadata: LikeTestInvocation): T { + return this.merge( + this.testDefinition(metadata), + this.testInvocationAndBelowDirect(metadata), + ); + } + + testVertical(metadata: LikeTestInvocation): T { + return this.merge( + this._ancestors(metadata), + this.testDefinitionAndBelow(metadata), + ); + } + + steps(metadata: LikeTestInvocation): AllureTestStepMetadata[] { + const before = [...metadata.beforeAll, ...metadata.beforeEach].map( + this._hookMetadata, + ); + + const after = [...metadata.afterEach, ...metadata.afterAll].map( + this._hookMetadata, + ); + + const testFunctionSteps = + (metadata.fn && this.getMetadataWithDocblock(metadata.fn).steps) || []; + + return [...before, ...testFunctionSteps, ...after]; + } + + globalAndTestFile(metadata: LikeTestFile): T { + return this.merge( + this._getMetadataUnsafe(metadata.globalMetadata), + this.getMetadataWithDocblock(metadata), + ); + } + + globalAndTestFileAndTestInvocation( + metadata: LikeTestInvocation, + ): T { + return this.merge( + this.globalAndTestFile(metadata.file), + this._getMetadataUnsafe(metadata), + ); + } +} diff --git a/src/metadata/squasher/MetadataSquasher.ts b/src/metadata/squasher/MetadataSquasher.ts new file mode 100644 index 00000000..adbcbde1 --- /dev/null +++ b/src/metadata/squasher/MetadataSquasher.ts @@ -0,0 +1,114 @@ +import type { + Metadata, + TestFileMetadata, + TestInvocationMetadata, +} from 'jest-metadata'; +import type { + AllureNestedTestStepMetadata, + AllureTestCaseMetadata, + AllureTestFileMetadata, + AllureTestItemMetadata, +} from 'jest-allure2-reporter'; + +import { PREFIX } from '../constants'; + +import { MetadataSelector } from './MetadataSelector'; +import { + mergeTestCaseMetadata, + mergeTestFileMetadata, + mergeTestStepMetadata, +} from './mergers'; + +export type MetadataSquasherConfig = { + getDocblockMetadata: ( + metadata: Metadata | undefined, + ) => T | undefined; +}; + +export class MetadataSquasher { + protected readonly _fileSelector: MetadataSelector< + Metadata, + AllureTestFileMetadata + >; + + protected readonly _testSelector: MetadataSelector< + Metadata, + AllureTestCaseMetadata + >; + + protected readonly _stepSelector: MetadataSelector< + Metadata, + AllureNestedTestStepMetadata + >; + + constructor(config: MetadataSquasherConfig) { + this._fileSelector = new MetadataSelector({ + empty: () => ({}), + getDocblock: config.getDocblockMetadata, + getMetadata: (metadata) => metadata.get(PREFIX), + mergeUnsafe: mergeTestFileMetadata, + }); + + this._testSelector = new MetadataSelector({ + empty: () => ({}), + getDocblock: config.getDocblockMetadata, + getMetadata: (metadata) => metadata.get(PREFIX), + mergeUnsafe: mergeTestCaseMetadata, + }); + + this._stepSelector = new MetadataSelector({ + empty: () => ({}), + getDocblock: config.getDocblockMetadata, + getMetadata: (metadata) => + metadata.get(PREFIX), + mergeUnsafe: mergeTestStepMetadata, + }); + } + + testFile(jest_metadata: TestFileMetadata): AllureTestFileMetadata { + const result = { + ...this._fileSelector.globalAndTestFile(jest_metadata), + steps: this._stepSelector.getMetadataWithDocblock(jest_metadata).steps, + }; + + return result; + } + + testInvocation(invocation: TestInvocationMetadata): AllureTestCaseMetadata { + const test_vertical = this._testSelector.testVertical(invocation); + const test_definition_and_below = + this._testSelector.testDefinitionAndBelow(invocation); + const test_definition_and_below_direct = + this._testSelector.testDefinitionAndBelowDirect(invocation); + const test_invocation = this._testSelector.testInvocation(invocation); + const test_invocation_and_below = + this._testSelector.testInvocationAndBelow(invocation); + const global_file_and_test_invocation = this._testSelector.merge( + this._testSelector.globalAndTestFileAndTestInvocation(invocation), + test_invocation, + ); + + const result: AllureTestCaseMetadata = { + attachments: test_definition_and_below_direct.attachments, + description: test_vertical.description, + descriptionHtml: test_vertical.descriptionHtml, + displayName: test_definition_and_below.displayName, + fullName: test_definition_and_below.fullName, + historyId: test_definition_and_below.historyId, + labels: test_vertical.labels, + links: test_vertical.links, + parameters: test_definition_and_below_direct.parameters, + sourceCode: test_definition_and_below_direct.sourceCode, + sourceLocation: test_definition_and_below_direct.sourceLocation, + stage: test_invocation_and_below.stage, + start: test_invocation_and_below.start, + status: test_invocation_and_below.status, + statusDetails: test_invocation_and_below.statusDetails, + steps: this._stepSelector.steps(invocation), + stop: test_invocation_and_below.stop, + workerId: global_file_and_test_invocation.workerId, + }; + + return result; + } +} diff --git a/src/metadata/squasher/__tests__/__snapshots__/mergeTestCaseMetadata.test.ts.snap b/src/metadata/squasher/__tests__/__snapshots__/mergeTestCaseMetadata.test.ts.snap new file mode 100644 index 00000000..f78b58ce --- /dev/null +++ b/src/metadata/squasher/__tests__/__snapshots__/mergeTestCaseMetadata.test.ts.snap @@ -0,0 +1,1849 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`mergeTestCaseMetadata belowTestInvocation 1`] = ` +{ + "attachments": [ + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "test_fn_invocation.txt", + "source": "/tmp/test_fn_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:hook_invocation", + "description:hook_invocation", + "description:test_fn_invocation", + "description:hook_invocation", + "description:hook_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:hook_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:test_fn_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:hook_invocation", + ], + "displayName": "displayName:hook_invocation", + "fullName": "fullName:hook_invocation", + "historyId": "historyId:hook_invocation", + "labels": [ + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "test_fn_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "test_fn_invocation", + "url": "https://example.com/test_fn_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:test_fn_invocation", + "value": "value:test_fn_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + ], + "sourceCode": "sourceCode:hook_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_invocation", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 8, + "status": "broken", + "statusDetails": { + "message": "message:hook_invocation", + "trace": "trace:hook_invocation", + }, + "stop": 109, + "workerId": "9", +} +`; + +exports[`mergeTestCaseMetadata globalAndTestFile 1`] = ` +{ + "attachments": [ + { + "name": "global.txt", + "source": "/tmp/global.txt", + "type": "text/plain", + }, + { + "name": "file_docblock.txt", + "source": "/tmp/file_docblock.txt", + "type": "text/plain", + }, + { + "name": "file.txt", + "source": "/tmp/file.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:global", + "description:file_docblock", + "description:file", + ], + "descriptionHtml": [ + "descriptionHtml:global", + "descriptionHtml:file_docblock", + "descriptionHtml:file", + ], + "displayName": "displayName:file", + "fullName": "fullName:file", + "historyId": "historyId:file", + "labels": [ + { + "name": "tag", + "value": "global", + }, + { + "name": "tag", + "value": "file_docblock", + }, + { + "name": "tag", + "value": "file", + }, + ], + "links": [ + { + "name": "link", + "type": "global", + "url": "https://example.com/global", + }, + { + "name": "link", + "type": "file_docblock", + "url": "https://example.com/file_docblock", + }, + { + "name": "link", + "type": "file", + "url": "https://example.com/file", + }, + ], + "parameters": [ + { + "name": "parameter:global", + "value": "value:global", + }, + { + "name": "parameter:file_docblock", + "value": "value:file_docblock", + }, + { + "name": "parameter:file", + "value": "value:file", + }, + ], + "sourceCode": "sourceCode:file", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:file", + "lineNumber": 1, + }, + "stage": "scheduled", + "start": 0, + "status": "unknown", + "statusDetails": { + "message": "message:file", + "trace": "trace:file", + }, + "stop": 102, + "workerId": "2", +} +`; + +exports[`mergeTestCaseMetadata globalAndTestFileAndTestInvocation 1`] = ` +{ + "attachments": [ + { + "name": "global.txt", + "source": "/tmp/global.txt", + "type": "text/plain", + }, + { + "name": "file_docblock.txt", + "source": "/tmp/file_docblock.txt", + "type": "text/plain", + }, + { + "name": "file.txt", + "source": "/tmp/file.txt", + "type": "text/plain", + }, + { + "name": "test_invocation.txt", + "source": "/tmp/test_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:global", + "description:file_docblock", + "description:file", + "description:test_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:global", + "descriptionHtml:file_docblock", + "descriptionHtml:file", + "descriptionHtml:test_invocation", + ], + "displayName": "displayName:test_invocation", + "fullName": "fullName:test_invocation", + "historyId": "historyId:test_invocation", + "labels": [ + { + "name": "tag", + "value": "global", + }, + { + "name": "tag", + "value": "file_docblock", + }, + { + "name": "tag", + "value": "file", + }, + { + "name": "tag", + "value": "test_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "global", + "url": "https://example.com/global", + }, + { + "name": "link", + "type": "file_docblock", + "url": "https://example.com/file_docblock", + }, + { + "name": "link", + "type": "file", + "url": "https://example.com/file", + }, + { + "name": "link", + "type": "test_invocation", + "url": "https://example.com/test_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:global", + "value": "value:global", + }, + { + "name": "parameter:file_docblock", + "value": "value:file_docblock", + }, + { + "name": "parameter:file", + "value": "value:file", + }, + { + "name": "parameter:test_invocation", + "value": "value:test_invocation", + }, + ], + "sourceCode": "sourceCode:test_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:test_invocation", + "lineNumber": 1, + }, + "stage": "scheduled", + "start": 0, + "status": "unknown", + "statusDetails": { + "message": "message:test_invocation", + "trace": "trace:test_invocation", + }, + "stop": 107, + "workerId": "7", +} +`; + +exports[`mergeTestCaseMetadata merge 1`] = `{}`; + +exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` +[ + { + "attachments": [ + { + "name": "hook_definition.txt", + "source": "/tmp/hook_definition.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:hook_definition", + "hookType": "beforeAll", + "parameters": [ + { + "name": "parameter:hook_definition", + "value": "value:hook_definition", + }, + ], + "sourceCode": "sourceCode:hook_definition", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_definition", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 4, + "status": "broken", + "statusDetails": { + "message": "message:hook_definition", + "trace": "trace:hook_definition", + }, + "steps": undefined, + "stop": 104, + }, + { + "attachments": [ + { + "name": "hook_definition.txt", + "source": "/tmp/hook_definition.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:hook_definition", + "hookType": "beforeEach", + "parameters": [ + { + "name": "parameter:hook_definition", + "value": "value:hook_definition", + }, + ], + "sourceCode": "sourceCode:hook_definition", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_definition", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 4, + "status": "broken", + "statusDetails": { + "message": "message:hook_definition", + "trace": "trace:hook_definition", + }, + "steps": undefined, + "stop": 104, + }, + { + "attachments": [ + { + "name": "hook_definition.txt", + "source": "/tmp/hook_definition.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:hook_definition", + "hookType": "afterEach", + "parameters": [ + { + "name": "parameter:hook_definition", + "value": "value:hook_definition", + }, + ], + "sourceCode": "sourceCode:hook_definition", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_definition", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 4, + "status": "broken", + "statusDetails": { + "message": "message:hook_definition", + "trace": "trace:hook_definition", + }, + "steps": undefined, + "stop": 104, + }, + { + "attachments": [ + { + "name": "hook_definition.txt", + "source": "/tmp/hook_definition.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:hook_definition", + "hookType": "afterAll", + "parameters": [ + { + "name": "parameter:hook_definition", + "value": "value:hook_definition", + }, + ], + "sourceCode": "sourceCode:hook_definition", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_definition", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 4, + "status": "broken", + "statusDetails": { + "message": "message:hook_definition", + "trace": "trace:hook_definition", + }, + "steps": undefined, + "stop": 104, + }, +] +`; + +exports[`mergeTestCaseMetadata steps 1`] = ` +[ + { + "attachments": [ + { + "name": "hook_definition.txt", + "source": "/tmp/hook_definition.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:hook_invocation", + "hookType": "beforeAll", + "parameters": [ + { + "name": "parameter:hook_definition", + "value": "value:hook_definition", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + ], + "sourceCode": "sourceCode:hook_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_invocation", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 4, + "status": "broken", + "statusDetails": { + "message": "message:hook_definition", + "trace": "trace:hook_definition", + }, + "steps": undefined, + "stop": 109, + }, + { + "attachments": [ + { + "name": "hook_definition.txt", + "source": "/tmp/hook_definition.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:hook_invocation", + "hookType": "beforeEach", + "parameters": [ + { + "name": "parameter:hook_definition", + "value": "value:hook_definition", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + ], + "sourceCode": "sourceCode:hook_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_invocation", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 4, + "status": "broken", + "statusDetails": { + "message": "message:hook_definition", + "trace": "trace:hook_definition", + }, + "steps": undefined, + "stop": 109, + }, + { + "attachments": [ + { + "name": "hook_definition.txt", + "source": "/tmp/hook_definition.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:hook_invocation", + "hookType": "afterEach", + "parameters": [ + { + "name": "parameter:hook_definition", + "value": "value:hook_definition", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + ], + "sourceCode": "sourceCode:hook_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_invocation", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 4, + "status": "broken", + "statusDetails": { + "message": "message:hook_definition", + "trace": "trace:hook_definition", + }, + "steps": undefined, + "stop": 109, + }, + { + "attachments": [ + { + "name": "hook_definition.txt", + "source": "/tmp/hook_definition.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:hook_invocation", + "hookType": "afterAll", + "parameters": [ + { + "name": "parameter:hook_definition", + "value": "value:hook_definition", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + ], + "sourceCode": "sourceCode:hook_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_invocation", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 4, + "status": "broken", + "statusDetails": { + "message": "message:hook_definition", + "trace": "trace:hook_definition", + }, + "steps": undefined, + "stop": 109, + }, +] +`; + +exports[`mergeTestCaseMetadata stepsSelector merge 1`] = `{}`; + +exports[`mergeTestCaseMetadata stepsSelector merge 2`] = ` +{ + "attachments": undefined, + "currentStep": undefined, + "displayName": undefined, + "hookType": undefined, + "parameters": undefined, + "sourceCode": undefined, + "sourceLocation": undefined, + "stage": undefined, + "start": undefined, + "status": undefined, + "statusDetails": undefined, + "steps": undefined, + "stop": undefined, +} +`; + +exports[`mergeTestCaseMetadata testDefinition 1`] = ` +{ + "attachments": [ + { + "name": "test_definition_docblock.txt", + "source": "/tmp/test_definition_docblock.txt", + "type": "text/plain", + }, + { + "name": "test_definition.txt", + "source": "/tmp/test_definition.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:test_definition_docblock", + "description:test_definition", + ], + "descriptionHtml": [ + "descriptionHtml:test_definition_docblock", + "descriptionHtml:test_definition", + ], + "displayName": "displayName:test_definition", + "fullName": "fullName:test_definition", + "historyId": "historyId:test_definition", + "labels": [ + { + "name": "tag", + "value": "test_definition_docblock", + }, + { + "name": "tag", + "value": "test_definition", + }, + ], + "links": [ + { + "name": "link", + "type": "test_definition_docblock", + "url": "https://example.com/test_definition_docblock", + }, + { + "name": "link", + "type": "test_definition", + "url": "https://example.com/test_definition", + }, + ], + "parameters": [ + { + "name": "parameter:test_definition_docblock", + "value": "value:test_definition_docblock", + }, + { + "name": "parameter:test_definition", + "value": "value:test_definition", + }, + ], + "sourceCode": "sourceCode:test_definition", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:test_definition", + "lineNumber": 1, + }, + "stage": "scheduled", + "start": 5, + "status": "skipped", + "statusDetails": { + "message": "message:test_definition", + "trace": "trace:test_definition", + }, + "stop": 106, + "workerId": "6", +} +`; + +exports[`mergeTestCaseMetadata testDefinitionAndBelow 1`] = ` +{ + "attachments": [ + { + "name": "test_definition_docblock.txt", + "source": "/tmp/test_definition_docblock.txt", + "type": "text/plain", + }, + { + "name": "test_definition.txt", + "source": "/tmp/test_definition.txt", + "type": "text/plain", + }, + { + "name": "test_invocation.txt", + "source": "/tmp/test_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "test_fn_invocation.txt", + "source": "/tmp/test_fn_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:test_definition_docblock", + "description:test_definition", + "description:test_invocation", + "description:hook_invocation", + "description:hook_invocation", + "description:test_fn_invocation", + "description:hook_invocation", + "description:hook_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:test_definition_docblock", + "descriptionHtml:test_definition", + "descriptionHtml:test_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:test_fn_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:hook_invocation", + ], + "displayName": "displayName:hook_invocation", + "fullName": "fullName:hook_invocation", + "historyId": "historyId:hook_invocation", + "labels": [ + { + "name": "tag", + "value": "test_definition_docblock", + }, + { + "name": "tag", + "value": "test_definition", + }, + { + "name": "tag", + "value": "test_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "test_fn_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "test_definition_docblock", + "url": "https://example.com/test_definition_docblock", + }, + { + "name": "link", + "type": "test_definition", + "url": "https://example.com/test_definition", + }, + { + "name": "link", + "type": "test_invocation", + "url": "https://example.com/test_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "test_fn_invocation", + "url": "https://example.com/test_fn_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:test_definition_docblock", + "value": "value:test_definition_docblock", + }, + { + "name": "parameter:test_definition", + "value": "value:test_definition", + }, + { + "name": "parameter:test_invocation", + "value": "value:test_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:test_fn_invocation", + "value": "value:test_fn_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + ], + "sourceCode": "sourceCode:hook_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_invocation", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 5, + "status": "broken", + "statusDetails": { + "message": "message:hook_invocation", + "trace": "trace:hook_invocation", + }, + "stop": 109, + "workerId": "9", +} +`; + +exports[`mergeTestCaseMetadata testDefinitionAndBelowDirect 1`] = ` +{ + "attachments": [ + { + "name": "test_definition_docblock.txt", + "source": "/tmp/test_definition_docblock.txt", + "type": "text/plain", + }, + { + "name": "test_definition.txt", + "source": "/tmp/test_definition.txt", + "type": "text/plain", + }, + { + "name": "test_invocation.txt", + "source": "/tmp/test_invocation.txt", + "type": "text/plain", + }, + { + "name": "test_fn_invocation.txt", + "source": "/tmp/test_fn_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:test_definition_docblock", + "description:test_definition", + "description:test_invocation", + "description:test_fn_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:test_definition_docblock", + "descriptionHtml:test_definition", + "descriptionHtml:test_invocation", + "descriptionHtml:test_fn_invocation", + ], + "displayName": "displayName:test_fn_invocation", + "fullName": "fullName:test_fn_invocation", + "historyId": "historyId:test_fn_invocation", + "labels": [ + { + "name": "tag", + "value": "test_definition_docblock", + }, + { + "name": "tag", + "value": "test_definition", + }, + { + "name": "tag", + "value": "test_invocation", + }, + { + "name": "tag", + "value": "test_fn_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "test_definition_docblock", + "url": "https://example.com/test_definition_docblock", + }, + { + "name": "link", + "type": "test_definition", + "url": "https://example.com/test_definition", + }, + { + "name": "link", + "type": "test_invocation", + "url": "https://example.com/test_invocation", + }, + { + "name": "link", + "type": "test_fn_invocation", + "url": "https://example.com/test_fn_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:test_definition_docblock", + "value": "value:test_definition_docblock", + }, + { + "name": "parameter:test_definition", + "value": "value:test_definition", + }, + { + "name": "parameter:test_invocation", + "value": "value:test_invocation", + }, + { + "name": "parameter:test_fn_invocation", + "value": "value:test_fn_invocation", + }, + ], + "sourceCode": "sourceCode:test_fn_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:test_fn_invocation", + "lineNumber": 1, + }, + "stage": "scheduled", + "start": 5, + "status": "failed", + "statusDetails": { + "message": "message:test_fn_invocation", + "trace": "trace:test_fn_invocation", + }, + "stop": 108, + "workerId": "8", +} +`; + +exports[`mergeTestCaseMetadata testInvocation 1`] = ` +{ + "attachments": [ + { + "name": "test_invocation.txt", + "source": "/tmp/test_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:test_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:test_invocation", + ], + "displayName": "displayName:test_invocation", + "fullName": "fullName:test_invocation", + "historyId": "historyId:test_invocation", + "labels": [ + { + "name": "tag", + "value": "test_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "test_invocation", + "url": "https://example.com/test_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:test_invocation", + "value": "value:test_invocation", + }, + ], + "sourceCode": "sourceCode:test_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:test_invocation", + "lineNumber": 1, + }, + "stage": "finished", + "start": 7, + "status": "unknown", + "statusDetails": { + "message": "message:test_invocation", + "trace": "trace:test_invocation", + }, + "stop": 107, + "workerId": "7", +} +`; + +exports[`mergeTestCaseMetadata testInvocationAndBelow 1`] = ` +{ + "attachments": [ + { + "name": "test_invocation.txt", + "source": "/tmp/test_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "test_fn_invocation.txt", + "source": "/tmp/test_fn_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:test_invocation", + "description:hook_invocation", + "description:hook_invocation", + "description:test_fn_invocation", + "description:hook_invocation", + "description:hook_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:test_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:test_fn_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:hook_invocation", + ], + "displayName": "displayName:hook_invocation", + "fullName": "fullName:hook_invocation", + "historyId": "historyId:hook_invocation", + "labels": [ + { + "name": "tag", + "value": "test_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "test_fn_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "test_invocation", + "url": "https://example.com/test_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "test_fn_invocation", + "url": "https://example.com/test_fn_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:test_invocation", + "value": "value:test_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:test_fn_invocation", + "value": "value:test_fn_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + ], + "sourceCode": "sourceCode:hook_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_invocation", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 7, + "status": "broken", + "statusDetails": { + "message": "message:hook_invocation", + "trace": "trace:hook_invocation", + }, + "stop": 109, + "workerId": "9", +} +`; + +exports[`mergeTestCaseMetadata testInvocationAndBelowDirect 1`] = ` +{ + "attachments": [ + { + "name": "test_invocation.txt", + "source": "/tmp/test_invocation.txt", + "type": "text/plain", + }, + { + "name": "test_fn_invocation.txt", + "source": "/tmp/test_fn_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:test_invocation", + "description:test_fn_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:test_invocation", + "descriptionHtml:test_fn_invocation", + ], + "displayName": "displayName:test_fn_invocation", + "fullName": "fullName:test_fn_invocation", + "historyId": "historyId:test_fn_invocation", + "labels": [ + { + "name": "tag", + "value": "test_invocation", + }, + { + "name": "tag", + "value": "test_fn_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "test_invocation", + "url": "https://example.com/test_invocation", + }, + { + "name": "link", + "type": "test_fn_invocation", + "url": "https://example.com/test_fn_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:test_invocation", + "value": "value:test_invocation", + }, + { + "name": "parameter:test_fn_invocation", + "value": "value:test_fn_invocation", + }, + ], + "sourceCode": "sourceCode:test_fn_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:test_fn_invocation", + "lineNumber": 1, + }, + "stage": "finished", + "start": 7, + "status": "failed", + "statusDetails": { + "message": "message:test_fn_invocation", + "trace": "trace:test_fn_invocation", + }, + "stop": 108, + "workerId": "8", +} +`; + +exports[`mergeTestCaseMetadata testVertical (no overrides in step invocations) 1`] = ` +{ + "attachments": [ + { + "name": "global.txt", + "source": "/tmp/global.txt", + "type": "text/plain", + }, + { + "name": "file_docblock.txt", + "source": "/tmp/file_docblock.txt", + "type": "text/plain", + }, + { + "name": "file.txt", + "source": "/tmp/file.txt", + "type": "text/plain", + }, + { + "name": "describe_block.txt", + "source": "/tmp/describe_block.txt", + "type": "text/plain", + }, + { + "name": "describe_block.txt", + "source": "/tmp/describe_block.txt", + "type": "text/plain", + }, + { + "name": "test_definition_docblock.txt", + "source": "/tmp/test_definition_docblock.txt", + "type": "text/plain", + }, + { + "name": "test_definition.txt", + "source": "/tmp/test_definition.txt", + "type": "text/plain", + }, + { + "name": "test_invocation.txt", + "source": "/tmp/test_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:global", + "description:file_docblock", + "description:file", + "description:describe_block", + "description:describe_block", + "description:test_definition_docblock", + "description:test_definition", + "description:test_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:global", + "descriptionHtml:file_docblock", + "descriptionHtml:file", + "descriptionHtml:describe_block", + "descriptionHtml:describe_block", + "descriptionHtml:test_definition_docblock", + "descriptionHtml:test_definition", + "descriptionHtml:test_invocation", + ], + "displayName": "displayName:test_invocation", + "fullName": "fullName:test_invocation", + "historyId": "historyId:test_invocation", + "labels": [ + { + "name": "tag", + "value": "global", + }, + { + "name": "tag", + "value": "file_docblock", + }, + { + "name": "tag", + "value": "file", + }, + { + "name": "tag", + "value": "describe_block", + }, + { + "name": "tag", + "value": "describe_block", + }, + { + "name": "tag", + "value": "test_definition_docblock", + }, + { + "name": "tag", + "value": "test_definition", + }, + { + "name": "tag", + "value": "test_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "global", + "url": "https://example.com/global", + }, + { + "name": "link", + "type": "file_docblock", + "url": "https://example.com/file_docblock", + }, + { + "name": "link", + "type": "file", + "url": "https://example.com/file", + }, + { + "name": "link", + "type": "describe_block", + "url": "https://example.com/describe_block", + }, + { + "name": "link", + "type": "describe_block", + "url": "https://example.com/describe_block", + }, + { + "name": "link", + "type": "test_definition_docblock", + "url": "https://example.com/test_definition_docblock", + }, + { + "name": "link", + "type": "test_definition", + "url": "https://example.com/test_definition", + }, + { + "name": "link", + "type": "test_invocation", + "url": "https://example.com/test_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:global", + "value": "value:global", + }, + { + "name": "parameter:file_docblock", + "value": "value:file_docblock", + }, + { + "name": "parameter:file", + "value": "value:file", + }, + { + "name": "parameter:describe_block", + "value": "value:describe_block", + }, + { + "name": "parameter:describe_block", + "value": "value:describe_block", + }, + { + "name": "parameter:test_definition_docblock", + "value": "value:test_definition_docblock", + }, + { + "name": "parameter:test_definition", + "value": "value:test_definition", + }, + { + "name": "parameter:test_invocation", + "value": "value:test_invocation", + }, + ], + "sourceCode": "sourceCode:test_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:test_invocation", + "lineNumber": 1, + }, + "stage": "scheduled", + "start": 0, + "status": "failed", + "statusDetails": { + "message": "message:describe_block", + "trace": "trace:describe_block", + }, + "stop": 107, + "workerId": "7", +} +`; + +exports[`mergeTestCaseMetadata testVertical 1`] = ` +{ + "attachments": [ + { + "name": "global.txt", + "source": "/tmp/global.txt", + "type": "text/plain", + }, + { + "name": "file_docblock.txt", + "source": "/tmp/file_docblock.txt", + "type": "text/plain", + }, + { + "name": "file.txt", + "source": "/tmp/file.txt", + "type": "text/plain", + }, + { + "name": "describe_block.txt", + "source": "/tmp/describe_block.txt", + "type": "text/plain", + }, + { + "name": "describe_block.txt", + "source": "/tmp/describe_block.txt", + "type": "text/plain", + }, + { + "name": "test_definition_docblock.txt", + "source": "/tmp/test_definition_docblock.txt", + "type": "text/plain", + }, + { + "name": "test_definition.txt", + "source": "/tmp/test_definition.txt", + "type": "text/plain", + }, + { + "name": "test_invocation.txt", + "source": "/tmp/test_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "test_fn_invocation.txt", + "source": "/tmp/test_fn_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + { + "name": "hook_invocation.txt", + "source": "/tmp/hook_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:global", + "description:file_docblock", + "description:file", + "description:describe_block", + "description:describe_block", + "description:test_definition_docblock", + "description:test_definition", + "description:test_invocation", + "description:hook_invocation", + "description:hook_invocation", + "description:test_fn_invocation", + "description:hook_invocation", + "description:hook_invocation", + ], + "descriptionHtml": [ + "descriptionHtml:global", + "descriptionHtml:file_docblock", + "descriptionHtml:file", + "descriptionHtml:describe_block", + "descriptionHtml:describe_block", + "descriptionHtml:test_definition_docblock", + "descriptionHtml:test_definition", + "descriptionHtml:test_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:test_fn_invocation", + "descriptionHtml:hook_invocation", + "descriptionHtml:hook_invocation", + ], + "displayName": "displayName:hook_invocation", + "fullName": "fullName:hook_invocation", + "historyId": "historyId:hook_invocation", + "labels": [ + { + "name": "tag", + "value": "global", + }, + { + "name": "tag", + "value": "file_docblock", + }, + { + "name": "tag", + "value": "file", + }, + { + "name": "tag", + "value": "describe_block", + }, + { + "name": "tag", + "value": "describe_block", + }, + { + "name": "tag", + "value": "test_definition_docblock", + }, + { + "name": "tag", + "value": "test_definition", + }, + { + "name": "tag", + "value": "test_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "test_fn_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + { + "name": "tag", + "value": "hook_invocation", + }, + ], + "links": [ + { + "name": "link", + "type": "global", + "url": "https://example.com/global", + }, + { + "name": "link", + "type": "file_docblock", + "url": "https://example.com/file_docblock", + }, + { + "name": "link", + "type": "file", + "url": "https://example.com/file", + }, + { + "name": "link", + "type": "describe_block", + "url": "https://example.com/describe_block", + }, + { + "name": "link", + "type": "describe_block", + "url": "https://example.com/describe_block", + }, + { + "name": "link", + "type": "test_definition_docblock", + "url": "https://example.com/test_definition_docblock", + }, + { + "name": "link", + "type": "test_definition", + "url": "https://example.com/test_definition", + }, + { + "name": "link", + "type": "test_invocation", + "url": "https://example.com/test_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "test_fn_invocation", + "url": "https://example.com/test_fn_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + { + "name": "link", + "type": "hook_invocation", + "url": "https://example.com/hook_invocation", + }, + ], + "parameters": [ + { + "name": "parameter:global", + "value": "value:global", + }, + { + "name": "parameter:file_docblock", + "value": "value:file_docblock", + }, + { + "name": "parameter:file", + "value": "value:file", + }, + { + "name": "parameter:describe_block", + "value": "value:describe_block", + }, + { + "name": "parameter:describe_block", + "value": "value:describe_block", + }, + { + "name": "parameter:test_definition_docblock", + "value": "value:test_definition_docblock", + }, + { + "name": "parameter:test_definition", + "value": "value:test_definition", + }, + { + "name": "parameter:test_invocation", + "value": "value:test_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:test_fn_invocation", + "value": "value:test_fn_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + { + "name": "parameter:hook_invocation", + "value": "value:hook_invocation", + }, + ], + "sourceCode": "sourceCode:hook_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:hook_invocation", + "lineNumber": 1, + }, + "stage": "interrupted", + "start": 0, + "status": "broken", + "statusDetails": { + "message": "message:hook_invocation", + "trace": "trace:hook_invocation", + }, + "stop": 109, + "workerId": "9", +} +`; diff --git a/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap b/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap new file mode 100644 index 00000000..616a5903 --- /dev/null +++ b/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap @@ -0,0 +1,248 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`mergeTestFileMetadata getMetadataWithDocblock 1`] = ` +{ + "attachments": [ + { + "name": "file_docblock.txt", + "source": "/tmp/file_docblock.txt", + "type": "text/plain", + }, + { + "name": "file.txt", + "source": "/tmp/file.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:file_docblock", + "description:file", + ], + "descriptionHtml": [ + "descriptionHtml:file_docblock", + "descriptionHtml:file", + ], + "historyId": "historyId:file", + "labels": [ + { + "name": "tag", + "value": "file_docblock", + }, + { + "name": "tag", + "value": "file", + }, + ], + "links": [ + { + "name": "link", + "type": "file_docblock", + "url": "https://example.com/file_docblock", + }, + { + "name": "link", + "type": "file", + "url": "https://example.com/file", + }, + ], + "parameters": [ + { + "name": "parameter:file_docblock", + "value": "value:file_docblock", + }, + { + "name": "parameter:file", + "value": "value:file", + }, + ], + "sourceCode": "sourceCode:file", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:file", + "lineNumber": 1, + }, + "stage": "running", + "start": 1, + "status": "unknown", + "statusDetails": { + "message": "message:file", + "trace": "trace:file", + }, + "stop": 102, + "workerId": "2", +} +`; + +exports[`mergeTestFileMetadata globalAndTestFile 1`] = ` +{ + "attachments": [ + { + "name": "global.txt", + "source": "/tmp/global.txt", + "type": "text/plain", + }, + { + "name": "file_docblock.txt", + "source": "/tmp/file_docblock.txt", + "type": "text/plain", + }, + { + "name": "file.txt", + "source": "/tmp/file.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "description": [ + "description:global", + "description:file_docblock", + "description:file", + ], + "descriptionHtml": [ + "descriptionHtml:global", + "descriptionHtml:file_docblock", + "descriptionHtml:file", + ], + "historyId": "historyId:file", + "labels": [ + { + "name": "tag", + "value": "global", + }, + { + "name": "tag", + "value": "file_docblock", + }, + { + "name": "tag", + "value": "file", + }, + ], + "links": [ + { + "name": "link", + "type": "global", + "url": "https://example.com/global", + }, + { + "name": "link", + "type": "file_docblock", + "url": "https://example.com/file_docblock", + }, + { + "name": "link", + "type": "file", + "url": "https://example.com/file", + }, + ], + "parameters": [ + { + "name": "parameter:global", + "value": "value:global", + }, + { + "name": "parameter:file_docblock", + "value": "value:file_docblock", + }, + { + "name": "parameter:file", + "value": "value:file", + }, + ], + "sourceCode": "sourceCode:file", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:file", + "lineNumber": 1, + }, + "stage": "scheduled", + "start": 0, + "status": "unknown", + "statusDetails": { + "message": "message:file", + "trace": "trace:file", + }, + "stop": 102, + "workerId": "2", +} +`; + +exports[`mergeTestFileMetadata merge 1`] = `{}`; + +exports[`mergeTestFileMetadata merge 2`] = ` +{ + "attachments": undefined, + "currentStep": undefined, + "description": undefined, + "descriptionHtml": undefined, + "displayName": undefined, + "fullName": undefined, + "historyId": undefined, + "labels": undefined, + "links": undefined, + "parameters": undefined, + "sourceCode": undefined, + "sourceLocation": undefined, + "stage": undefined, + "start": undefined, + "status": undefined, + "statusDetails": undefined, + "stop": undefined, + "workerId": undefined, +} +`; + +exports[`mergeTestFileMetadata steps 1`] = ` +[ + { + "attachments": [ + { + "name": "step_invocation.txt", + "source": "/tmp/step_invocation.txt", + "type": "text/plain", + }, + ], + "currentStep": undefined, + "displayName": "displayName:step_invocation", + "hookType": undefined, + "parameters": [ + { + "name": "parameter:step_invocation", + "value": "value:step_invocation", + }, + ], + "sourceCode": "sourceCode:step_invocation", + "sourceLocation": { + "columnNumber": 2, + "fileName": "fileName:step_invocation", + "lineNumber": 1, + }, + "stage": "scheduled", + "start": 10, + "status": "passed", + "statusDetails": { + "message": "message:step_invocation", + "trace": "trace:step_invocation", + }, + "steps": [ + { + "attachments": undefined, + "currentStep": undefined, + "displayName": undefined, + "hookType": undefined, + "parameters": undefined, + "sourceCode": undefined, + "sourceLocation": undefined, + "stage": undefined, + "start": undefined, + "status": undefined, + "statusDetails": undefined, + "steps": undefined, + "stop": undefined, + }, + ], + "stop": 110, + }, +] +`; diff --git a/src/metadata/squasher/__tests__/fixtures.ts b/src/metadata/squasher/__tests__/fixtures.ts new file mode 100644 index 00000000..3f3f0e46 --- /dev/null +++ b/src/metadata/squasher/__tests__/fixtures.ts @@ -0,0 +1,213 @@ +import type { + AllureTestCaseMetadata, + AllureTestFileMetadata, + AllureTestItemMetadata, + AllureTestStepMetadata, + Stage, + Status, +} from 'jest-allure2-reporter'; + +import type { + LikeDescribeBlock, + LikeStepInvocation, + LikeTestFile, + LikeTestInvocation, +} from '../MetadataSelector'; + +const SCOPE_NUMBERS = { + global: 0, + file_docblock: 1, + file: 2, + describe_block: 3, + hook_definition: 4, + test_definition_docblock: 5, + test_definition: 6, + test_invocation: 7, + test_fn_invocation: 8, + hook_invocation: 9, + step_invocation: 10, +} as const; + +type Scope = keyof typeof SCOPE_NUMBERS; + +export function createTestItemMetadata( + scope: Scope, + extra?: AllureTestItemMetadata, +): AllureTestItemMetadata { + return { + attachments: [ + { + name: scope + '.txt', + type: 'text/plain', + source: `/tmp/${scope}.txt`, + }, + ], + description: [`description:${scope}`], + historyId: `historyId:${scope}`, + parameters: [ + { + name: `parameter:${scope}`, + value: `value:${scope}`, + }, + ], + sourceCode: `sourceCode:${scope}`, + sourceLocation: { + fileName: `fileName:${scope}`, + lineNumber: 1, + columnNumber: 2, + }, + stage: castStage(scope), + start: castStart(scope), + status: castStatus(scope), + statusDetails: { + message: `message:${scope}`, + trace: `trace:${scope}`, + }, + stop: castStop(scope), + ...extra, + }; +} + +export function createTestStepMetadata( + scope: 'hook_definition' | 'hook_invocation' | 'step_invocation', + extra?: AllureTestStepMetadata, +): AllureTestItemMetadata { + return { + ...createTestCaseMetadata(scope, extra), + ...extra, + }; +} + +export function createTestFileMetadata( + scope: Scope, + extra?: AllureTestFileMetadata, +): AllureTestFileMetadata { + return { + ...createTestItemMetadata(scope, extra), + descriptionHtml: [`descriptionHtml:${scope}`], + labels: [ + { + name: 'tag', + value: scope, + }, + ], + links: [ + { + name: 'link', + url: `https://example.com/${scope}`, + type: scope, + }, + ], + workerId: castWorkerId(scope), + ...extra, + }; +} + +export function createTestCaseMetadata( + scope: Scope, + extra?: AllureTestCaseMetadata, +): AllureTestCaseMetadata { + return { + ...createTestFileMetadata(scope, extra), + }; +} + +type StubMetadata = { + data: T; +}; + +export type StubTestCaseMetadata = StubMetadata; +export type StubTestFileMetadata = StubMetadata; +export type StubTestStepMetadata = StubMetadata; + +function createHookMetadata( + hookType: AllureTestStepMetadata['hookType'], +): LikeStepInvocation { + return { + definition: { + data: createTestStepMetadata('hook_definition', { + hookType, + }), + }, + data: createTestStepMetadata('hook_invocation', { + hookType, + }), + }; +} + +export function getFullBlownTestCase(): LikeTestInvocation { + const testFile: LikeTestFile = { + globalMetadata: { data: createTestFileMetadata('global') }, + data: createTestFileMetadata('file'), + }; + + const rootDescribeBlock: LikeDescribeBlock = { + data: createTestCaseMetadata('describe_block'), + }; + + const describeBlock: LikeDescribeBlock = { + parent: rootDescribeBlock, + data: createTestCaseMetadata('describe_block'), + }; + + const beforeAll = [createHookMetadata('beforeAll')]; + const beforeEach = [createHookMetadata('beforeEach')]; + const afterEach = [createHookMetadata('afterEach')]; + const afterAll = [createHookMetadata('afterAll')]; + + return { + beforeAll, + beforeEach, + afterEach, + afterAll, + *allInvocations(): Iterable { + yield* this.beforeAll; + yield* this.beforeEach; + if (this.fn) yield this.fn; + yield* this.afterEach; + yield* this.afterAll; + }, + definition: { + data: createTestCaseMetadata('test_definition'), + describeBlock, + }, + file: testFile, + fn: { data: createTestCaseMetadata('test_fn_invocation') }, + data: createTestCaseMetadata('test_invocation'), + }; +} + +const STAGES: Stage[] = [ + 'scheduled', + 'running', + 'finished', + 'pending', + 'interrupted', +]; +const STATUSES: Status[] = ['passed', 'skipped', 'unknown', 'failed', 'broken']; + +function castStart(scope: Scope): number { + return toNumber(scope); +} + +function castStop(scope: Scope): number { + return 100 + toNumber(scope); +} + +function castStage(scope: Scope): Stage { + const index = toNumber(scope) % 5; + return STAGES[index]; +} + +function castStatus(scope: Scope): Status { + const index = toNumber(scope) % 5; + return STATUSES[index]; +} + +function castWorkerId(scope: Scope): string { + return String(toNumber(scope)); +} + +function toNumber(scope: Scope): number { + return SCOPE_NUMBERS[scope]; +} diff --git a/src/metadata/squasher/__tests__/mergeTestCaseMetadata.test.ts b/src/metadata/squasher/__tests__/mergeTestCaseMetadata.test.ts new file mode 100644 index 00000000..c3dec3c1 --- /dev/null +++ b/src/metadata/squasher/__tests__/mergeTestCaseMetadata.test.ts @@ -0,0 +1,134 @@ +import type { + AllureTestCaseMetadata, + AllureTestStepMetadata, +} from 'jest-allure2-reporter'; + +import { MetadataSelector } from '../MetadataSelector'; +import { mergeTestCaseMetadata, mergeTestStepMetadata } from '../mergers'; + +import { + createTestCaseMetadata, + createTestFileMetadata, + getFullBlownTestCase, + type StubTestCaseMetadata, + type StubTestStepMetadata, +} from './fixtures'; + +describe('mergeTestCaseMetadata', () => { + let docblocks = new WeakMap(); + let stepSelector: MetadataSelector< + StubTestStepMetadata, + AllureTestStepMetadata + >; + let testSelector: MetadataSelector< + StubTestCaseMetadata, + AllureTestCaseMetadata + >; + let testCase: ReturnType; + + beforeEach(() => { + docblocks = new WeakMap(); + }); + + beforeEach(() => { + testSelector = new MetadataSelector({ + empty: () => ({}), + getDocblock: (metadata) => docblocks.get(metadata), + getMetadata: (metadata) => metadata.data, + mergeUnsafe: mergeTestCaseMetadata, + }); + + stepSelector = new MetadataSelector({ + empty: () => ({}), + getDocblock: (metadata) => docblocks.get(metadata), + getMetadata: (metadata) => metadata.data, + mergeUnsafe: mergeTestStepMetadata, + }); + + testCase = getFullBlownTestCase(); + docblocks.set(testCase.file, createTestFileMetadata('file_docblock')); + docblocks.set( + testCase.definition, + createTestCaseMetadata('test_definition_docblock'), + ); + }); + + test('merge', () => { + expect(testSelector.merge(void 0, void 0)).toMatchSnapshot(); + }); + + test('testInvocation', () => { + expect(testSelector.testInvocation(testCase)).toMatchSnapshot(); + }); + + test('testDefinition', () => { + expect(testSelector.testDefinition(testCase)).toMatchSnapshot(); + }); + + test('belowTestInvocation', () => { + expect(testSelector.belowTestInvocation(testCase)).toMatchSnapshot(); + }); + + test('testInvocationAndBelow', () => { + expect(testSelector.testInvocationAndBelow(testCase)).toMatchSnapshot(); + }); + + test('testInvocationAndBelowDirect', () => { + expect( + testSelector.testInvocationAndBelowDirect(testCase), + ).toMatchSnapshot(); + }); + + test('testDefinitionAndBelow', () => { + expect(testSelector.testDefinitionAndBelow(testCase)).toMatchSnapshot(); + }); + + test('testDefinitionAndBelowDirect', () => { + expect( + testSelector.testDefinitionAndBelowDirect(testCase), + ).toMatchSnapshot(); + }); + + test('testVertical', () => { + expect(testSelector.testVertical(testCase)).toMatchSnapshot(); + }); + + test('testVertical (no overrides in step invocations)', () => { + for (const invocation of testCase.allInvocations()) { + invocation.data = {}; + } + + // we are testing that hook definition metadata + // does not override the test metadata here + expect(testSelector.testVertical(testCase)).toMatchSnapshot(); + }); + + test('steps', () => { + expect(stepSelector.steps(testCase)).toMatchSnapshot(); + }); + + test('steps (no overrides in invocations)', () => { + for (const invocation of testCase.allInvocations()) { + invocation.data = {}; + } + + // we are testing that hook definition metadata + // does not override the test metadata here + expect(stepSelector.steps(testCase)).toMatchSnapshot(); + }); + + test('globalAndTestFile', () => { + expect(testSelector.globalAndTestFile(testCase.file)).toMatchSnapshot(); + }); + + test('globalAndTestFileAndTestInvocation', () => { + expect( + testSelector.globalAndTestFileAndTestInvocation(testCase), + ).toMatchSnapshot(); + }); + + test('stepsSelector merge', () => { + expect(stepSelector.merge(void 0, void 0)).toMatchSnapshot(); + expect(stepSelector.merge({}, {})).toMatchSnapshot(); + }); +}); diff --git a/src/metadata/squasher/__tests__/mergeTestFileMetadata.test.ts b/src/metadata/squasher/__tests__/mergeTestFileMetadata.test.ts new file mode 100644 index 00000000..52ca9668 --- /dev/null +++ b/src/metadata/squasher/__tests__/mergeTestFileMetadata.test.ts @@ -0,0 +1,75 @@ +import type { + AllureTestFileMetadata, + AllureTestStepMetadata, +} from 'jest-allure2-reporter'; + +import { MetadataSelector } from '../MetadataSelector'; +import { mergeTestFileMetadata, mergeTestStepMetadata } from '../mergers'; + +import { + createTestFileMetadata, + createTestStepMetadata, + getFullBlownTestCase, + type StubTestFileMetadata, + type StubTestStepMetadata, +} from './fixtures'; + +describe('mergeTestFileMetadata', () => { + let docblocks = new WeakMap(); + let stepSelector: MetadataSelector< + StubTestStepMetadata, + AllureTestStepMetadata + >; + let fileSelector: MetadataSelector< + StubTestFileMetadata, + AllureTestFileMetadata + >; + let testCase: ReturnType; + + beforeEach(() => { + docblocks = new WeakMap(); + }); + + beforeEach(() => { + fileSelector = new MetadataSelector({ + empty: () => ({}), + getDocblock: (metadata) => docblocks.get(metadata), + getMetadata: (metadata) => metadata.data, + mergeUnsafe: mergeTestFileMetadata, + }); + + stepSelector = new MetadataSelector({ + empty: () => ({}), + getDocblock: (metadata) => docblocks.get(metadata), + getMetadata: (metadata) => metadata.data, + mergeUnsafe: mergeTestStepMetadata, + }); + + testCase = getFullBlownTestCase(); + testCase.file.data.steps = [ + createTestStepMetadata('step_invocation', { steps: [{}] }), + ]; + docblocks.set(testCase.file, createTestFileMetadata('file_docblock')); + }); + + test('merge', () => { + expect(fileSelector.merge(void 0, void 0)).toMatchSnapshot(); + expect(fileSelector.merge({}, {})).toMatchSnapshot(); + }); + + test('getMetadataWithDocblock', () => { + expect( + fileSelector.getMetadataWithDocblock(testCase.file), + ).toMatchSnapshot(); + }); + + test('globalAndTestFile', () => { + expect(fileSelector.globalAndTestFile(testCase.file)).toMatchSnapshot(); + }); + + test('steps', () => { + expect( + stepSelector.getMetadataWithDocblock(testCase.file).steps, + ).toMatchSnapshot(); + }); +}); diff --git a/src/metadata/squasher/index.ts b/src/metadata/squasher/index.ts new file mode 100644 index 00000000..50e00522 --- /dev/null +++ b/src/metadata/squasher/index.ts @@ -0,0 +1 @@ +export * from './MetadataSquasher'; diff --git a/src/metadata/squasher/mergers.ts b/src/metadata/squasher/mergers.ts new file mode 100644 index 00000000..946c074c --- /dev/null +++ b/src/metadata/squasher/mergers.ts @@ -0,0 +1,161 @@ +import type { + AllureTestItemMetadata, + AllureTestFileMetadata, + AllureTestCaseMetadata, + AllureTestStepMetadata, + AllureTestStepPath, + Stage, + Status, + StatusDetails, +} from 'jest-allure2-reporter'; + +export function mergeTestFileMetadata( + a: AllureTestFileMetadata, + b: AllureTestFileMetadata | undefined, +): AllureTestFileMetadata { + return mergeTestCaseMetadata(a, b); +} + +export function mergeTestCaseMetadata( + a: AllureTestCaseMetadata, + b: AllureTestCaseMetadata | undefined, +): AllureTestCaseMetadata { + return b + ? { + ...mergeTestItemMetadata(a, b), + description: mergeArrays(a.description, b.description), + descriptionHtml: mergeArrays(a.descriptionHtml, b.descriptionHtml), + historyId: b.historyId ?? a.historyId, + labels: mergeArrays(a.labels, b.labels), + links: mergeArrays(a.links, b.links), + workerId: b.workerId ?? a.workerId, + } + : a; +} + +export function mergeTestStepMetadata( + a: AllureTestStepMetadata, + b: AllureTestStepMetadata | undefined, +): AllureTestStepMetadata { + return b + ? { + ...mergeTestItemMetadata(a, b), + hookType: b.hookType ?? a.hookType, + steps: mergeArrays(a.steps, b.steps)?.map((step) => + mergeTestStepMetadata({}, step), + ), + } + : a; +} + +function mergeTestItemMetadata( + a: AllureTestItemMetadata, + b: AllureTestItemMetadata | undefined, +): AllureTestItemMetadata { + return b + ? { + attachments: mergeArrays(a.attachments, b.attachments), + currentStep: mergeCurrentStep(a, b), + displayName: b.displayName ?? a.displayName, + sourceCode: b.sourceCode ?? a.sourceCode, + sourceLocation: b.sourceLocation ?? a.sourceLocation, + parameters: mergeArrays(a.parameters, b.parameters), + stage: mergeStage(b.stage, a.stage), + start: min(b.start, a.start), + status: mergeStatus(a.status, b.status), + statusDetails: mergeStatusDetails(a, b), + stop: max(b.stop, a.stop), + } + : a; +} + +function mergeArrays( + a: T[] | undefined, + b: T[] | undefined, +): T[] | undefined { + if (a && b) { + return [...a, ...b]; + } + + return a ?? b; +} + +function min(a: number | undefined, b: number | undefined): number | undefined { + if (a != null && b != null) { + return Math.min(a, b); + } + + return a ?? b; +} + +function max(a: number | undefined, b: number | undefined): number | undefined { + if (a != null && b != null) { + return Math.max(a, b); + } + + return a ?? b; +} + +function mergeStage( + a: Stage | undefined, + b: Stage | undefined, +): Stage | undefined { + if (a === 'interrupted' || b === 'interrupted') { + return 'interrupted'; + } + + return b ?? a; +} + +function mergeStatus( + a: Status | undefined, + b: Status | undefined, +): Status | undefined { + if (a === 'broken' || b === 'broken') { + return 'broken'; + } + + if (a === 'failed' || b === 'failed') { + return 'failed'; + } + + return b ?? a; +} + +function mergeStatusDetails( + a: AllureTestItemMetadata, + b: AllureTestItemMetadata, +): StatusDetails | undefined { + if (a.status === 'broken') { + return a.statusDetails; + } + + if (b.status === 'broken') { + return b.statusDetails; + } + + if (a.status === 'failed') { + return a.statusDetails; + } + + if (b.status === 'failed') { + return b.statusDetails; + } + + return b.statusDetails ?? a.statusDetails; +} + +function mergeCurrentStep( + a: AllureTestItemMetadata, + b: AllureTestItemMetadata, +): AllureTestStepPath | undefined { + if (a.stage === 'interrupted') { + return a.currentStep; + } + + if (b.stage === 'interrupted') { + return b.currentStep; + } + + return b.currentStep ?? a.currentStep; +} diff --git a/src/metadata/utils/getStage.ts b/src/metadata/utils/getStage.ts deleted file mode 100644 index f990b36b..00000000 --- a/src/metadata/utils/getStage.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestInvocationMetadata } from 'jest-metadata'; - -import { STAGE } from '../constants'; - -export const getStage = (testInvocation: TestInvocationMetadata) => { - let finished: boolean | undefined; - for (const invocation of testInvocation.allInvocations()) { - finished ??= true; - - const stage = invocation.get(STAGE); - if (stage === 'interrupted') { - return 'interrupted'; - } - if (stage !== 'finished') { - finished = false; - } - } - - return finished ? 'finished' : undefined; -}; diff --git a/src/metadata/utils/getStart.ts b/src/metadata/utils/getStart.ts deleted file mode 100644 index 09faa491..00000000 --- a/src/metadata/utils/getStart.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { TestInvocationMetadata } from 'jest-metadata'; - -import { START } from '../constants'; - -export const getStart = (testInvocation: TestInvocationMetadata) => { - const firstBlock = - testInvocation.beforeAll[0] ?? - testInvocation.beforeEach[0] ?? - testInvocation.fn; - - const start1 = testInvocation.get(START); - const start2 = firstBlock?.get(START); - - if (typeof start1 === 'number') { - return typeof start2 === 'number' ? Math.min(start1, start2) : start1; - } else { - return typeof start2 === 'number' ? start2 : undefined; - } -}; diff --git a/src/metadata/utils/getStatusAndDetails.ts b/src/metadata/utils/getStatusAndDetails.ts deleted file mode 100644 index 2ed15b03..00000000 --- a/src/metadata/utils/getStatusAndDetails.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Metadata, TestInvocationMetadata } from 'jest-metadata'; -import type { - Status, - StatusDetails, - AllureTestItemMetadata, -} from 'jest-allure2-reporter'; - -import { PREFIX } from '../constants'; - -export const getStatusAndDetails = (testInvocation: TestInvocationMetadata) => { - return getBadResult(testInvocation) ?? getInnerResult(testInvocation); -}; - -function getInnerResult(testInvocation: TestInvocationMetadata) { - const allInvocations = [...testInvocation.allInvocations()].reverse(); - let status: Status | undefined; - let statusDetails: StatusDetails | undefined; - - for (const invocation of allInvocations) { - const result = getResult(invocation); - if (result) { - if (isBadStatus(result.status)) { - return result; - } - - status ??= result.status; - statusDetails ??= result.statusDetails; - } - } - - return status ? { status, statusDetails } : {}; -} - -function getResult(metadata: Metadata): Result | undefined { - const item = metadata.get(PREFIX, {}); - const { status, statusDetails } = item; - - return status ? { status, statusDetails } : undefined; -} - -function isBadStatus(status: Status | undefined) { - return status === 'failed' || status === 'broken'; -} - -function getBadResult(metadata: Metadata): Result | undefined { - const result = getResult(metadata); - const status = result?.status; - return isBadStatus(status) ? result : undefined; -} - -type Result = Pick; diff --git a/src/metadata/utils/getStop.ts b/src/metadata/utils/getStop.ts deleted file mode 100644 index de4e54ea..00000000 --- a/src/metadata/utils/getStop.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TestInvocationMetadata } from 'jest-metadata'; - -import { last } from '../../utils'; -import { STOP } from '../constants'; - -export const getStop = (testInvocation: TestInvocationMetadata) => { - const lastBlock = - last(testInvocation.afterAll) ?? - last(testInvocation.afterEach) ?? - testInvocation.fn; - - const stop1 = testInvocation.get(STOP); - const stop2 = lastBlock?.get(STOP); - - if (typeof stop1 === 'number') { - return typeof stop2 === 'number' ? Math.max(stop1, stop2) : stop1; - } else { - return typeof stop2 === 'number' ? stop2 : undefined; - } -}; diff --git a/src/metadata/utils/index.ts b/src/metadata/utils/index.ts deleted file mode 100644 index ec7e9ab7..00000000 --- a/src/metadata/utils/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './getStage'; -export * from './getStart'; -export * from './getStatusAndDetails'; -export * from './getStop'; diff --git a/src/options/default-options/plugins.ts b/src/options/default-options/plugins.ts index 54e0edc8..095fcee6 100644 --- a/src/options/default-options/plugins.ts +++ b/src/options/default-options/plugins.ts @@ -5,7 +5,6 @@ import * as plugins from '../../builtin-plugins'; export async function defaultPlugins(context: PluginContext) { return [ plugins.detect({}, context), - plugins.docblock({}, context), plugins.github({}, context), plugins.manifest({}, context), plugins.prettier({}, context), diff --git a/src/options/default-options/testStep.ts b/src/options/default-options/testStep.ts index 36a970bf..06782829 100644 --- a/src/options/default-options/testStep.ts +++ b/src/options/default-options/testStep.ts @@ -10,7 +10,9 @@ import { stripStatusDetails } from '../utils'; export const testStep: ResolvedTestStepCustomizer = { hidden: () => false, name: ({ testStepMetadata }) => - testStepMetadata.description?.at(-1) ?? testStepMetadata.hookType, + testStepMetadata.displayName || + testStepMetadata.hookType || + 'Untitled step', start: ({ testStepMetadata }) => testStepMetadata.start, stop: ({ testStepMetadata }) => testStepMetadata.stop, stage: ({ testStepMetadata }) => testStepMetadata.stage, diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index 18f71889..a976ae9b 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -161,7 +161,11 @@ export class JestAllure2Reporter extends JestMetadataReporter { this._allure.writeCategoriesDefinitions(categories as Category[]); } - const squasher = new MetadataSquasher(); + const docblockParser: any = { find: () => void 0 }; // TODO: await initParser(); + const squasher = new MetadataSquasher({ + getDocblockMetadata: (metadata) => + metadata && docblockParser.find(metadata), + }); for (const testResult of results.testResults) { const testFileContext: TestFileExtractorContext = { @@ -213,7 +217,6 @@ export class JestAllure2Reporter extends JestMetadataReporter { for (const testInvocationMetadata of allInvocations) { const testCaseMetadata = squasher.testInvocation( - testFileContext.testFileMetadata, testInvocationMetadata, ); diff --git a/src/runtime/modules/CoreModule.ts b/src/runtime/modules/CoreModule.ts index d413200a..c2135171 100644 --- a/src/runtime/modules/CoreModule.ts +++ b/src/runtime/modules/CoreModule.ts @@ -25,15 +25,15 @@ export class CoreModule { } description(value: string) { - this.context.metadata.push('description', [value]); + this.context.metadata.$bind(null).push('description', [value]); } descriptionHtml(value: string) { - this.context.metadata.push('descriptionHtml', [value]); + this.context.metadata.$bind(null).push('descriptionHtml', [value]); } historyId(value: string) { - this.context.metadata.set('historyId', value); + this.context.metadata.$bind(null).set('historyId', value); } label(name: LabelName | string, value: string) { diff --git a/src/utils/index.ts b/src/utils/index.ts index d5735f65..444af817 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,5 +13,6 @@ export * from './md5'; export * from './processMaybePromise'; export * from './shallowEqualArrays'; export * from './splitDocblock'; +export * from './weakMemoize'; export * from './wrapFunction'; export * from './types'; diff --git a/src/utils/weakMemoize.test.ts b/src/utils/weakMemoize.test.ts new file mode 100644 index 00000000..612559cb --- /dev/null +++ b/src/utils/weakMemoize.test.ts @@ -0,0 +1,26 @@ +import { weakMemoize } from './weakMemoize'; + +describe('weakMemoize', () => { + let random: (object?: object | null) => number; + + beforeEach(() => { + random = weakMemoize(() => Math.random()); + }); + + it('should not memoize on undefined', () => { + expect(random()).not.toBe(random()); + }); + + it('should not memoize on null', () => { + expect(random(null)).not.toBe(random(null)); + }); + + it('should not memoize on different objects', () => { + expect(random({})).not.toBe(random({})); + }); + + it('should memoize on the same object', () => { + const object = {}; + expect(random(object)).toBe(random(object)); + }); +}); diff --git a/src/utils/weakMemoize.ts b/src/utils/weakMemoize.ts new file mode 100644 index 00000000..3a82d545 --- /dev/null +++ b/src/utils/weakMemoize.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export function weakMemoize any>(function_: F): F { + const cache = new WeakMap(); + + const memoizedFunction = ((argument: any) => { + if (argument == null) { + return function_(argument); + } + + if (!cache.has(argument)) { + cache.set(argument, function_(argument)); + } + + return cache.get(argument)!; + }) as F; + + return memoizedFunction; +} From 8997611023f44f8cc3b07898be54297f6cb48acd Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 1 Mar 2024 16:56:07 +0200 Subject: [PATCH 02/50] feat: allure.fullName(string) --- index.d.ts | 1 + package-e2e/api.test.ts | 1 + .../__snapshots__/mergeTestFileMetadata.test.ts.snap | 2 ++ src/metadata/squasher/__tests__/fixtures.ts | 3 ++- src/metadata/squasher/mergers.ts | 1 + src/options/default-options/testCase.ts | 3 ++- src/runtime/AllureRuntime.ts | 5 +++++ src/runtime/modules/CoreModule.ts | 4 ++++ src/runtime/types.ts | 10 ++++++---- 9 files changed, 24 insertions(+), 6 deletions(-) diff --git a/index.d.ts b/index.d.ts index fccd04f9..3dd66d2c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -536,6 +536,7 @@ declare module 'jest-allure2-reporter' { /** @inheritDoc */ export interface AllureTestCaseMetadata extends AllureTestItemMetadata { descriptionHtml?: string[]; + fullName?: string; labels?: Label[]; links?: Link[]; workerId?: string; diff --git a/package-e2e/api.test.ts b/package-e2e/api.test.ts index 0efd8c07..2ed521cf 100644 --- a/package-e2e/api.test.ts +++ b/package-e2e/api.test.ts @@ -57,6 +57,7 @@ test('typings of jest-allure2-reporter/api', async () => { allure.description('This is a _description_ generated in runtime'); allure.descriptionHtml('This is a description generated in runtime'); allure.historyId('00-11-22-33-44-55'); + allure.fullName('Custom full name'); allure.status('failed'); allure.status('passed'); allure.status('broken'); diff --git a/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap b/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap index 616a5903..39914c8b 100644 --- a/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap +++ b/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap @@ -23,6 +23,7 @@ exports[`mergeTestFileMetadata getMetadataWithDocblock 1`] = ` "descriptionHtml:file_docblock", "descriptionHtml:file", ], + "fullName": "fullName:file", "historyId": "historyId:file", "labels": [ { @@ -104,6 +105,7 @@ exports[`mergeTestFileMetadata globalAndTestFile 1`] = ` "descriptionHtml:file_docblock", "descriptionHtml:file", ], + "fullName": "fullName:file", "historyId": "historyId:file", "labels": [ { diff --git a/src/metadata/squasher/__tests__/fixtures.ts b/src/metadata/squasher/__tests__/fixtures.ts index 3f3f0e46..b9bc4d01 100644 --- a/src/metadata/squasher/__tests__/fixtures.ts +++ b/src/metadata/squasher/__tests__/fixtures.ts @@ -42,7 +42,6 @@ export function createTestItemMetadata( source: `/tmp/${scope}.txt`, }, ], - description: [`description:${scope}`], historyId: `historyId:${scope}`, parameters: [ { @@ -84,6 +83,8 @@ export function createTestFileMetadata( ): AllureTestFileMetadata { return { ...createTestItemMetadata(scope, extra), + fullName: `fullName:${scope}`, + description: [`description:${scope}`], descriptionHtml: [`descriptionHtml:${scope}`], labels: [ { diff --git a/src/metadata/squasher/mergers.ts b/src/metadata/squasher/mergers.ts index 946c074c..8ebdda03 100644 --- a/src/metadata/squasher/mergers.ts +++ b/src/metadata/squasher/mergers.ts @@ -26,6 +26,7 @@ export function mergeTestCaseMetadata( description: mergeArrays(a.description, b.description), descriptionHtml: mergeArrays(a.descriptionHtml, b.descriptionHtml), historyId: b.historyId ?? a.historyId, + fullName: b.fullName ?? a.fullName, labels: mergeArrays(a.labels, b.labels), links: mergeArrays(a.links, b.links), workerId: b.workerId ?? a.workerId, diff --git a/src/options/default-options/testCase.ts b/src/options/default-options/testCase.ts index 8df874d6..07be3d9b 100644 --- a/src/options/default-options/testCase.ts +++ b/src/options/default-options/testCase.ts @@ -40,7 +40,8 @@ export const testCase: ResolvedTestCaseCustomizer = { historyId: ({ testCase, testCaseMetadata }) => testCaseMetadata.historyId ?? testCase.fullName, name: ({ testCase }) => testCase.title, - fullName: ({ testCase }) => testCase.fullName, + fullName: ({ testCase, testCaseMetadata }) => + testCaseMetadata.fullName ?? testCase.fullName, description: ({ testCaseMetadata }) => { const text = testCaseMetadata.description?.join('\n\n') ?? ''; const before = extractCode( diff --git a/src/runtime/AllureRuntime.ts b/src/runtime/AllureRuntime.ts index 655dff8f..6d4d2be3 100644 --- a/src/runtime/AllureRuntime.ts +++ b/src/runtime/AllureRuntime.ts @@ -66,6 +66,11 @@ export class AllureRuntime implements IAllureRuntime { this.#coreModule.descriptionHtml(value); }; + fullName: IAllureRuntime['fullName'] = (value) => { + // TODO: assert is a string + this.#coreModule.fullName(value); + }; + historyId(value: string) { // TODO: assert is a string this.#coreModule.historyId(value); diff --git a/src/runtime/modules/CoreModule.ts b/src/runtime/modules/CoreModule.ts index c2135171..203fdc4b 100644 --- a/src/runtime/modules/CoreModule.ts +++ b/src/runtime/modules/CoreModule.ts @@ -32,6 +32,10 @@ export class CoreModule { this.context.metadata.$bind(null).push('descriptionHtml', [value]); } + fullName(value: string) { + this.context.metadata.set('fullName', value); + } + historyId(value: string) { this.context.metadata.$bind(null).set('historyId', value); } diff --git a/src/runtime/types.ts b/src/runtime/types.ts index f58e1d01..d80a6d7b 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -28,11 +28,9 @@ export interface IAllureRuntime { descriptionHtml(value: string): void; - historyId(value: string): void; - - status(status: Status, statusDetails?: StatusDetails): void; + fullName(value: string): void; - statusDetails(statusDetails: StatusDetails): void; + historyId(value: string): void; label(name: LabelName, value: string): void; @@ -42,6 +40,10 @@ export interface IAllureRuntime { parameters(parameters: Record): void; + status(status: Status, statusDetails?: StatusDetails): void; + + statusDetails(statusDetails: StatusDetails): void; + attachment( name: string, content: MaybePromise, From d0cee56c92e003b8d1d35d5a0eacb524e2d0c934 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 1 Mar 2024 17:06:10 +0200 Subject: [PATCH 03/50] feat: allure.displayName(string) --- e2e/src/programmatic/grouping/names.test.ts | 47 +++++++++++++++++++ index.d.ts | 4 ++ package-e2e/api.test.ts | 1 + src/environment/listener.ts | 2 +- .../mergeTestFileMetadata.test.ts.snap | 2 + src/metadata/squasher/__tests__/fixtures.ts | 1 + src/options/default-options/testCase.ts | 3 +- src/runtime/AllureRuntime.ts | 5 ++ .../__snapshots__/AllureRuntime.test.ts.snap | 16 ++----- src/runtime/modules/CoreModule.ts | 4 ++ src/runtime/modules/StepsModule.ts | 4 +- src/runtime/types.ts | 2 + 12 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 e2e/src/programmatic/grouping/names.test.ts diff --git a/e2e/src/programmatic/grouping/names.test.ts b/e2e/src/programmatic/grouping/names.test.ts new file mode 100644 index 00000000..d0b768a2 --- /dev/null +++ b/e2e/src/programmatic/grouping/names.test.ts @@ -0,0 +1,47 @@ +/** + * This suite checks how default and custom naming works for test files, test cases and test steps. + * + * @tag displayName + * @tag fullName + * @tag description + */ + +import { allure } from 'jest-allure2-reporter/api'; + +describe('Names', () => { + beforeAll(() => { + /* Regular beforeAll */ + }); + beforeAll(() => { + /** Docblock beforeAll */ + }); + beforeAll(() => { + allure.displayName('Programmatic beforeAll'); + }); + + afterEach(() => { + /* Regular afterEach */ + }); + afterEach(() => { + /** Docblock afterEach */ + }); + afterEach(() => { + allure.displayName('Programmatic afterEach'); + }); + + test('Docblock test', () => { + /** + * Extra description (docblock) + * @displayName Docblock test (custom) + * @fullName Names - Docblock test (custom) + * @description Even more description (docblock) + */ + }); + + test('Programmatic test', () => { + allure.displayName('Programmatic test (custom)'); + allure.fullName('Names - Programmatic test (custom)'); + allure.description('Extra description (programmatic)'); + allure.description('Even more description (programmatic)'); + }); +}); diff --git a/index.d.ts b/index.d.ts index 3dd66d2c..0436ef0e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -480,6 +480,10 @@ declare module 'jest-allure2-reporter' { * Markdown description of the test case or test file, or plain text description of a test step. */ description?: string[]; + /** + * Title of the test case or test step. + */ + displayName?: string; /** * Custom history ID to distinguish between tests and their retry attempts. */ diff --git a/package-e2e/api.test.ts b/package-e2e/api.test.ts index 2ed521cf..8948d7ac 100644 --- a/package-e2e/api.test.ts +++ b/package-e2e/api.test.ts @@ -57,6 +57,7 @@ test('typings of jest-allure2-reporter/api', async () => { allure.description('This is a _description_ generated in runtime'); allure.descriptionHtml('This is a description generated in runtime'); allure.historyId('00-11-22-33-44-55'); + allure.displayName('Custom test case name'); allure.fullName('Custom full name'); allure.status('failed'); allure.status('passed'); diff --git a/src/environment/listener.ts b/src/environment/listener.ts index 9c82a8ee..f1a6a014 100644 --- a/src/environment/listener.ts +++ b/src/environment/listener.ts @@ -92,7 +92,7 @@ function addSourceCode({ event }: TestEnvironmentCircusEvent) { code = ''; realm.runtimeContext .getCurrentMetadata() - .push('description', ['Reset mocks, modules and timers (Jest)']); + .set('displayName', 'Reset mocks, modules and timers (Jest)'); } } diff --git a/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap b/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap index 39914c8b..78735c0d 100644 --- a/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap +++ b/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap @@ -23,6 +23,7 @@ exports[`mergeTestFileMetadata getMetadataWithDocblock 1`] = ` "descriptionHtml:file_docblock", "descriptionHtml:file", ], + "displayName": "displayName:file", "fullName": "fullName:file", "historyId": "historyId:file", "labels": [ @@ -105,6 +106,7 @@ exports[`mergeTestFileMetadata globalAndTestFile 1`] = ` "descriptionHtml:file_docblock", "descriptionHtml:file", ], + "displayName": "displayName:file", "fullName": "fullName:file", "historyId": "historyId:file", "labels": [ diff --git a/src/metadata/squasher/__tests__/fixtures.ts b/src/metadata/squasher/__tests__/fixtures.ts index b9bc4d01..28cbc0fd 100644 --- a/src/metadata/squasher/__tests__/fixtures.ts +++ b/src/metadata/squasher/__tests__/fixtures.ts @@ -42,6 +42,7 @@ export function createTestItemMetadata( source: `/tmp/${scope}.txt`, }, ], + displayName: `displayName:${scope}`, historyId: `historyId:${scope}`, parameters: [ { diff --git a/src/options/default-options/testCase.ts b/src/options/default-options/testCase.ts index 07be3d9b..e1644a4f 100644 --- a/src/options/default-options/testCase.ts +++ b/src/options/default-options/testCase.ts @@ -39,7 +39,8 @@ export const testCase: ResolvedTestCaseCustomizer = { hidden: () => false, historyId: ({ testCase, testCaseMetadata }) => testCaseMetadata.historyId ?? testCase.fullName, - name: ({ testCase }) => testCase.title, + name: ({ testCase, testCaseMetadata }) => + testCaseMetadata.displayName ?? testCase.title, fullName: ({ testCase, testCaseMetadata }) => testCaseMetadata.fullName ?? testCase.fullName, description: ({ testCaseMetadata }) => { diff --git a/src/runtime/AllureRuntime.ts b/src/runtime/AllureRuntime.ts index 6d4d2be3..3b5186d1 100644 --- a/src/runtime/AllureRuntime.ts +++ b/src/runtime/AllureRuntime.ts @@ -89,6 +89,11 @@ export class AllureRuntime implements IAllureRuntime { this.#coreModule.link({ name, url, type }); }; + displayName: IAllureRuntime['displayName'] = (value) => { + // TODO: assert is a string + this.#coreModule.displayName(value); + }; + parameter: IAllureRuntime['parameter'] = (name, value, options) => { // TODO: assert name is a string this.#coreModule.parameter({ diff --git a/src/runtime/__snapshots__/AllureRuntime.test.ts.snap b/src/runtime/__snapshots__/AllureRuntime.test.ts.snap index ac67315b..bfeaacb1 100644 --- a/src/runtime/__snapshots__/AllureRuntime.test.ts.snap +++ b/src/runtime/__snapshots__/AllureRuntime.test.ts.snap @@ -24,9 +24,7 @@ exports[`AllureRuntime should add attachments within the steps 1`] = ` "type": "text/plain", }, ], - "description": [ - "outer step", - ], + "displayName": "outer step", "stage": "finished", "start": 0, "status": "passed", @@ -39,9 +37,7 @@ exports[`AllureRuntime should add attachments within the steps 1`] = ` "type": "text/plain", }, ], - "description": [ - "inner step 1", - ], + "displayName": "inner step 1", "stage": "interrupted", "start": 1, "status": "failed", @@ -51,9 +47,7 @@ exports[`AllureRuntime should add attachments within the steps 1`] = ` "stop": 2, }, { - "description": [ - "inner step 2", - ], + "displayName": "inner step 2", "stage": "finished", "start": 3, "status": "passed", @@ -67,9 +61,7 @@ exports[`AllureRuntime should add attachments within the steps 1`] = ` "type": "text/plain", }, ], - "description": [ - "inner step 3", - ], + "displayName": "inner step 3", "parameters": [ { "name": "0", diff --git a/src/runtime/modules/CoreModule.ts b/src/runtime/modules/CoreModule.ts index 203fdc4b..0dbd2d7d 100644 --- a/src/runtime/modules/CoreModule.ts +++ b/src/runtime/modules/CoreModule.ts @@ -32,6 +32,10 @@ export class CoreModule { this.context.metadata.$bind(null).push('descriptionHtml', [value]); } + displayName(value: string) { + this.context.metadata.set('displayName', value); + } + fullName(value: string) { this.context.metadata.set('fullName', value); } diff --git a/src/runtime/modules/StepsModule.ts b/src/runtime/modules/StepsModule.ts index 1a8a0486..f5d5de98 100644 --- a/src/runtime/modules/StepsModule.ts +++ b/src/runtime/modules/StepsModule.ts @@ -51,11 +51,11 @@ export class StepsModule { } } - #startStep = (name: string) => { + #startStep = (displayName: string) => { this.context.metadata.$startStep().assign({ stage: 'scheduled', start: this.context.now, - description: [name], + displayName, }); }; diff --git a/src/runtime/types.ts b/src/runtime/types.ts index d80a6d7b..abb4bfe2 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -28,6 +28,8 @@ export interface IAllureRuntime { descriptionHtml(value: string): void; + displayName(value: string): void; + fullName(value: string): void; historyId(value: string): void; From 258bf826c689fbcfa2f1fa822a597e4a69030e1b Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 8 Mar 2024 17:39:37 +0200 Subject: [PATCH 04/50] fix: restore docblock parsing partially --- e2e/coffee-machine.js | 14 ++ e2e/configs/default.js | 57 ------- e2e/jest.config.js | 64 +++++++- e2e/package.json | 1 + e2e/presets/default.js | 1 + e2e/{configs => presets}/no-circus.js | 0 e2e/{configs => presets}/no-environment.js | 0 .../grouping/coffeescript.test.coffee | 17 +++ e2e/src/programmatic/grouping/names.test.ts | 21 +-- index.d.ts | 110 ++++++++++++-- src/builtin-plugins/sourceCode.ts | 2 - src/environment/listener.ts | 68 +++++---- src/metadata/docblock/index.ts | 141 +----------------- src/metadata/docblock/mapping.ts | 76 ++++++++++ src/metadata/squasher/MetadataSelector.ts | 5 +- src/options/default-options/plugins.ts | 3 +- .../augs.d.ts | 0 .../detect.ts | 0 src/reporter-plugins/fallback.ts | 32 ++++ .../github.ts | 0 .../index.ts | 1 + .../manifest.ts | 0 .../prettier.ts | 0 .../remark.ts | 0 src/reporter-plugins/sourceCode.ts | 74 +++++++++ src/reporter/JestAllure2Reporter.ts | 80 ++++++---- .../extractJsdocAbove.test.ts.snap | 23 +++ .../docblock/extractJsdocAbove.test.ts | 48 ++++++ .../docblock/extractJsdocAbove.ts | 60 ++++++++ src/runtime-plugins/docblock/index.ts | 21 +++ src/runtime-plugins/index.ts | 1 + src/runtime/utils/FileNavigatorCache.ts | 26 ++++ src/runtime/utils/LineNavigator.test.ts | 64 ++++++++ src/runtime/utils/LineNavigator.ts | 62 ++++++++ src/runtime/utils/index.ts | 2 + src/utils/index.ts | 1 + src/utils/isLibraryPath.test.ts | 23 +++ src/utils/isLibraryPath.ts | 10 ++ 38 files changed, 827 insertions(+), 281 deletions(-) create mode 100644 e2e/coffee-machine.js delete mode 100644 e2e/configs/default.js create mode 100644 e2e/presets/default.js rename e2e/{configs => presets}/no-circus.js (100%) rename e2e/{configs => presets}/no-environment.js (100%) create mode 100644 e2e/src/programmatic/grouping/coffeescript.test.coffee delete mode 100644 src/builtin-plugins/sourceCode.ts create mode 100644 src/metadata/docblock/mapping.ts rename src/{builtin-plugins => reporter-plugins}/augs.d.ts (100%) rename src/{builtin-plugins => reporter-plugins}/detect.ts (100%) create mode 100644 src/reporter-plugins/fallback.ts rename src/{builtin-plugins => reporter-plugins}/github.ts (100%) rename src/{builtin-plugins => reporter-plugins}/index.ts (82%) rename src/{builtin-plugins => reporter-plugins}/manifest.ts (100%) rename src/{builtin-plugins => reporter-plugins}/prettier.ts (100%) rename src/{builtin-plugins => reporter-plugins}/remark.ts (100%) create mode 100644 src/reporter-plugins/sourceCode.ts create mode 100644 src/runtime-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap create mode 100644 src/runtime-plugins/docblock/extractJsdocAbove.test.ts create mode 100644 src/runtime-plugins/docblock/extractJsdocAbove.ts create mode 100644 src/runtime-plugins/docblock/index.ts create mode 100644 src/runtime-plugins/index.ts create mode 100644 src/runtime/utils/FileNavigatorCache.ts create mode 100644 src/runtime/utils/LineNavigator.test.ts create mode 100644 src/runtime/utils/LineNavigator.ts create mode 100644 src/runtime/utils/index.ts create mode 100644 src/utils/isLibraryPath.test.ts create mode 100644 src/utils/isLibraryPath.ts diff --git a/e2e/coffee-machine.js b/e2e/coffee-machine.js new file mode 100644 index 00000000..d1ca82e7 --- /dev/null +++ b/e2e/coffee-machine.js @@ -0,0 +1,14 @@ +module.exports = { + process(src, path) { + const coffee = require('coffeescript'); + const js = coffee.compile(src, { + bare: true, + inline: true, + }); + + process.stdout.write(js); + return { + code: js, + }; + }, +}; diff --git a/e2e/configs/default.js b/e2e/configs/default.js deleted file mode 100644 index 5a4cbe90..00000000 --- a/e2e/configs/default.js +++ /dev/null @@ -1,57 +0,0 @@ -// eslint-disable-next-line node/no-extraneous-require,@typescript-eslint/no-var-requires,import/no-extraneous-dependencies -const _ = require('lodash'); -const PRESET = process.env.ALLURE_PRESET ?? 'default'; - -/** @type {import('jest-allure2-reporter').ReporterOptions} */ -const jestAllure2ReporterOptions = { - resultsDir: `allure-results/${PRESET}`, - categories: [ - { - name: 'Snapshot mismatches', - matchedStatuses: ['failed'], - messageRegex: /.*\btoMatch(?:[A-Za-z]+)?Snapshot\b.*/, - }, - { - name: 'Timeouts', - matchedStatuses: ['broken'], - messageRegex: /.*Exceeded timeout of.*/, - }, - ], - environment: (context) => { - return ({ - 'version.node': process.version, - 'version.jest': require('jest/package.json').version, - 'package.name': context.manifest.name, - 'package.version': context.manifest.version, - }); - }, - testCase: { - name: ({ testCase }) => - [...testCase.ancestorTitles, testCase.title].join(' » '), - labels: { - parentSuite: ({ filePath }) => filePath[0], - suite: ({ filePath }) => filePath.slice(1, 2).join('/'), - subSuite: ({ filePath }) => filePath.slice(2).join('/'), - epic: ({ value }) => value ?? 'Uncategorized', - story: ({ value }) => value ?? 'Untitled story', - feature: ({ value }) => value ?? 'Untitled feature', - package: ({ filePath }) => filePath.slice(0, -1).join('.'), - testClass: ({ filePath }) => filePath.join('.').replace(/\.test\.[jt]s$/, ''), - testMethod: ({ testCase }) => testCase.fullName, - owner: ({ value }) => value ?? 'Unknown', - }, - links: { - issue: ({ value }) => ({ ...value, url: `https://youtrack.jetbrains.com/issue/${value.url}/` }), - }, - }, -}; - -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - // eslint-disable-next-line node/no-unpublished-require,import/no-extraneous-dependencies - preset: 'ts-jest', - reporters: ['default', ['jest-allure2-reporter', jestAllure2ReporterOptions]], - rootDir: './src/programmatic/grouping', - testEnvironment: 'jest-allure2-reporter/environment-node', - testMatch: ['/**/*.test.ts'], -}; diff --git a/e2e/jest.config.js b/e2e/jest.config.js index 2fa67565..54af794d 100644 --- a/e2e/jest.config.js +++ b/e2e/jest.config.js @@ -1,3 +1,65 @@ +// eslint-disable-next-line node/no-extraneous-require,@typescript-eslint/no-var-requires,import/no-extraneous-dependencies +const path = require('node:path'); + +const _ = require('lodash'); const ALLURE_PRESET = process.env.ALLURE_PRESET ?? 'default'; -module.exports = require(`./configs/${ALLURE_PRESET}`); +/** @type {import('jest-allure2-reporter').ReporterOptions} */ +const jestAllure2ReporterOptions = { + resultsDir: `allure-results/${ALLURE_PRESET}`, + categories: [ + { + name: 'Snapshot mismatches', + matchedStatuses: ['failed'], + messageRegex: /.*\btoMatch(?:[A-Za-z]+)?Snapshot\b.*/, + }, + { + name: 'Timeouts', + matchedStatuses: ['broken'], + messageRegex: /.*Exceeded timeout of.*/, + }, + ], + environment: (context) => { + return ({ + 'version.node': process.version, + 'version.jest': require('jest/package.json').version, + 'package.name': context.manifest.name, + 'package.version': context.manifest.version, + }); + }, + testCase: { + name: ({ testCase }) => + [...testCase.ancestorTitles, testCase.title].join(' » '), + labels: { + parentSuite: ({ filePath }) => filePath[0], + suite: ({ filePath }) => filePath.slice(1, 2).join('/'), + subSuite: ({ filePath }) => filePath.slice(2).join('/'), + epic: ({ value }) => value ?? 'Uncategorized', + story: ({ value }) => value ?? 'Untitled story', + feature: ({ value }) => value ?? 'Untitled feature', + package: ({ filePath }) => filePath.slice(0, -1).join('.'), + testClass: ({ filePath }) => filePath.join('.').replace(/\.test\.[jt]s$/, ''), + testMethod: ({ testCase }) => testCase.fullName, + owner: ({ value }) => value ?? 'Unknown', + }, + links: { + issue: ({ value }) => ({ ...value, url: `https://youtrack.jetbrains.com/issue/${value.url}/` }), + }, + }, +}; + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + // eslint-disable-next-line node/no-unpublished-require,import/no-extraneous-dependencies + moduleFileExtensions: ['js', 'ts', 'coffee'], + preset: 'ts-jest', + reporters: ['default', ['jest-allure2-reporter', jestAllure2ReporterOptions]], + rootDir: './src/programmatic/grouping', + transform: { + '^.+\\.coffee$': path.join(__dirname, 'coffee-machine.js'), + }, + testEnvironment: 'jest-allure2-reporter/environment-node', + testMatch: ['/**/*.test.ts', '/**/*.test.coffee'], + + ...require(`./presets/${ALLURE_PRESET}`), +}; diff --git a/e2e/package.json b/e2e/package.json index b22a625a..04b4cffb 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -12,6 +12,7 @@ "jest": "29.x.x", "jest-jasmine2": "29.x.x", "jest-allure2-reporter": "..", + "coffeescript": "^2.7.0", "ts-jest": "29.x.x", "typescript": "5.x.x" } diff --git a/e2e/presets/default.js b/e2e/presets/default.js new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/e2e/presets/default.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/e2e/configs/no-circus.js b/e2e/presets/no-circus.js similarity index 100% rename from e2e/configs/no-circus.js rename to e2e/presets/no-circus.js diff --git a/e2e/configs/no-environment.js b/e2e/presets/no-environment.js similarity index 100% rename from e2e/configs/no-environment.js rename to e2e/presets/no-environment.js diff --git a/e2e/src/programmatic/grouping/coffeescript.test.coffee b/e2e/src/programmatic/grouping/coffeescript.test.coffee new file mode 100644 index 00000000..f299ed54 --- /dev/null +++ b/e2e/src/programmatic/grouping/coffeescript.test.coffee @@ -0,0 +1,17 @@ +###* +# This suite checks that Allure reporter can take metadata from CoffeeScript tests +### +describe 'CoffeeScript', -> + counter = 0 + + ###* + # Increment counter + ### + beforeEach -> + counter++ + + ###* + # @tag coffeescript + ### + it 'should work well with Allure', -> + expect(counter).toBeGreaterThan(0) diff --git a/e2e/src/programmatic/grouping/names.test.ts b/e2e/src/programmatic/grouping/names.test.ts index d0b768a2..914980c2 100644 --- a/e2e/src/programmatic/grouping/names.test.ts +++ b/e2e/src/programmatic/grouping/names.test.ts @@ -9,33 +9,34 @@ import { allure } from 'jest-allure2-reporter/api'; describe('Names', () => { + /* Regular beforeAll */ beforeAll(() => { - /* Regular beforeAll */ }); + /** Docblock beforeAll */ beforeAll(() => { - /** Docblock beforeAll */ }); + beforeAll(() => { allure.displayName('Programmatic beforeAll'); }); + /* Regular afterEach */ afterEach(() => { - /* Regular afterEach */ }); + /** Docblock afterEach */ afterEach(() => { - /** Docblock afterEach */ }); afterEach(() => { allure.displayName('Programmatic afterEach'); }); + /** + * Extra description (docblock) + * @displayName Docblock test (custom) + * @fullName Names - Docblock test (custom) + * @description Even more description (docblock) + */ test('Docblock test', () => { - /** - * Extra description (docblock) - * @displayName Docblock test (custom) - * @fullName Names - Docblock test (custom) - * @description Even more description (docblock) - */ }); test('Programmatic test', () => { diff --git a/index.d.ts b/index.d.ts index 0436ef0e..d46327f0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ -import type { Config, TestCaseResult, TestResult } from '@jest/reporters'; +import type { AggregatedResult, Config, Test, TestCaseResult, TestContext, TestResult } from '@jest/reporters'; import JestMetadataReporter from 'jest-metadata/reporter'; declare module 'jest-allure2-reporter' { @@ -113,6 +113,7 @@ declare module 'jest-allure2-reporter' { export type AttachmentsOptions = { /** * Defines a subdirectory within the {@link ReporterOptions#resultsDir} where attachments will be stored. + * Use absolute path if you want to store attachments outside the {@link ReporterOptions#resultsDir} directory. * @default 'attachments' */ subDir?: string; @@ -128,6 +129,8 @@ declare module 'jest-allure2-reporter' { /** * Specifies default strategy for attaching dynamic content to the report. * Uses simple file writing by default. + * @default 'write' + * @see {@link AllureRuntime#createContentAttachment} */ contentHandler?: BuiltinContentAttachmentHandler | string; }; @@ -138,6 +141,31 @@ declare module 'jest-allure2-reporter' { /** @see {@link AttachmentsOptions#contentHandler} */ export type BuiltinContentAttachmentHandler = 'write'; + /** @see {@link ReporterOptions#docblock} */ + export type DocblockOptions = { + /** + * Specifies where to look for docblocks: inside functions or outside (on top). + * @default 'outside' + */ + location?: 'inside' | 'outside' | 'both'; + }; + + /** @see {@link ReporterOptions#sourceCode} */ + export type SourceCodeOptions = { + /** + * Specifies where to take the source code from: + * - `file` - read the file from the file system + * - `function` - extract the source code from the test function + * @default 'file' + */ + location?: 'file' | 'function'; + /** + * Whether to prettify the source code before attaching it to the report. + * @default false + */ + prettify?: boolean; + }; + /** * Global customizations for how test cases are reported */ @@ -477,9 +505,9 @@ declare module 'jest-allure2-reporter' { */ currentStep?: AllureTestStepPath; /** - * Markdown description of the test case or test file, or plain text description of a test step. + * Parsed docblock: comments, pragmas, and raw content. */ - description?: string[]; + docblock?: DocblockContext; /** * Title of the test case or test step. */ @@ -539,6 +567,13 @@ declare module 'jest-allure2-reporter' { /** @inheritDoc */ export interface AllureTestCaseMetadata extends AllureTestItemMetadata { + /** + * Markdown description of the test case or test file. + */ + description?: string[]; + /** + * Raw HTML description of the test case or test file. + */ descriptionHtml?: string[]; fullName?: string; labels?: Label[]; @@ -555,8 +590,7 @@ declare module 'jest-allure2-reporter' { export interface DocblockContext { comments: string; - pragmas: Record; - raw: string; + pragmas: Record; } export interface GlobalExtractorContextAugmentation { @@ -590,7 +624,7 @@ declare module 'jest-allure2-reporter' { ) => Plugin; export type PluginContext = Readonly<{ - globalConfig: Config.GlobalConfig; + globalConfig: Readonly; }>; export interface Plugin { @@ -600,24 +634,70 @@ declare module 'jest-allure2-reporter' { /** Optional method for deduplicating plugins. Return the instance which you want to keep. */ extend?(previous: Plugin): Plugin; + /** Attach to the reporter lifecycle hook `onRunStart`. */ + onRunStart?(context: PluginHookContexts['onRunStart']): void | Promise; + + /** Attach to the reporter lifecycle hook `onTestFileStart`. */ + onTestFileStart?(context: PluginHookContexts['onTestFileStart']): void | Promise; + + /** Attach to the reporter lifecycle hook `onTestCaseResult`. */ + onTestCaseResult?(context: PluginHookContexts['onTestCaseResult']): void | Promise; + + /** Attach to the reporter lifecycle hook `onTestFileResult`. */ + onTestFileResult?(context: PluginHookContexts['onTestFileResult']): void | Promise; + + /** Attach to the reporter lifecycle hook `onRunComplete`. */ + onRunComplete?(context: PluginHookContexts['onRunComplete']): void | Promise; + /** Method to extend global context. */ - globalContext?(context: GlobalExtractorContext): void | Promise; + globalContext?(context: PluginHookContexts['globalContext']): void | Promise; /** Method to extend test file context. */ - testFileContext?(context: TestFileExtractorContext): void | Promise; + testFileContext?(context: PluginHookContexts['testFileContext']): void | Promise; /** Method to extend test entry context. */ - testCaseContext?(context: TestCaseExtractorContext): void | Promise; + testCaseContext?(context: PluginHookContexts['testCaseContext']): void | Promise; /** Method to extend test step context. */ - testStepContext?(context: TestStepExtractorContext): void | Promise; + testStepContext?(context: PluginHookContexts['testStepContext']): void | Promise; } - export type PluginHookName = - | 'globalContext' - | 'testFileContext' - | 'testCaseContext' - | 'testStepContext'; + export type PluginHookContexts = { + onRunStart: { + aggregatedResult: AggregatedResult; + reporterConfig: ReporterConfig; + }; + onTestFileStart: { + reporterConfig: ReporterConfig; + test: Test; + testFileMetadata: AllureTestFileMetadata; + }; + onTestCaseResult: { + reporterConfig: ReporterConfig; + test: Test; + testFileMetadata: AllureTestFileMetadata; + testCaseMetadata: AllureTestCaseMetadata; + testCaseResult: TestCaseResult; + }; + onTestFileResult: { + aggregatedResult: AggregatedResult; + reporterConfig: ReporterConfig; + test: Test; + testResult: TestResult; + testFileMetadata: AllureTestFileMetadata; + }; + onRunComplete: { + reporterConfig: ReporterConfig; + testContexts: Set; + results: AggregatedResult; + }; + globalContext: GlobalExtractorContext; + testFileContext: TestFileExtractorContext; + testCaseContext: TestCaseExtractorContext; + testStepContext: TestStepExtractorContext; + }; + + export type PluginHookName = keyof PluginHookContexts; //region Allure types diff --git a/src/builtin-plugins/sourceCode.ts b/src/builtin-plugins/sourceCode.ts deleted file mode 100644 index 964f5576..00000000 --- a/src/builtin-plugins/sourceCode.ts +++ /dev/null @@ -1,2 +0,0 @@ -// TODO: implement source code plugin -// It should go over the test steps, and unite the code snippets into a single diff --git a/src/environment/listener.ts b/src/environment/listener.ts index f1a6a014..45b7555f 100644 --- a/src/environment/listener.ts +++ b/src/environment/listener.ts @@ -1,3 +1,4 @@ +import type { AllureTestItemSourceLocation } from 'jest-allure2-reporter'; import type { Circus } from '@jest/types'; import type { EnvironmentListenerFn, @@ -8,37 +9,30 @@ import * as StackTrace from 'stacktrace-js'; import * as api from '../api'; import realm from '../realms'; -import { getStatusDetails, isJestAssertionError } from '../utils'; +import { + getStatusDetails, + isJestAssertionError, + isLibraryPath, +} from '../utils'; const listener: EnvironmentListenerFn = (context) => { context.testEvents - .on( - 'test_environment_setup', - function ({ env }: TestEnvironmentSetupEvent) { - env.global.__ALLURE__ = realm; - const { injectGlobals } = realm.runtimeContext.getReporterConfig(); - if (injectGlobals) { - Object.assign(env.global, api); - } - - realm.runtimeContext - .getFileMetadata() - .set('workerId', process.env.JEST_WORKER_ID); - }, - ) - .on('add_hook', addSourceLocation) + .on('test_environment_setup', injectGlobals) + .on('test_environment_setup', setWorkerId) .on('add_hook', addHookType) + .on('add_hook', addSourceLocation) .on('add_test', addSourceLocation) - .on('test_start', testStart) - .on('test_todo', testSkip) - .on('test_skip', testSkip) - .on('test_done', testDone) + .on('run_start', flush) .on('hook_start', addSourceCode) .on('hook_start', executableStart) .on('hook_failure', executableFailure) .on('hook_failure', flush) .on('hook_success', executableSuccess) .on('hook_success', flush) + .on('test_start', testStart) + .on('test_todo', testSkip) + .on('test_skip', testSkip) + .on('test_done', testDone) .on('test_fn_start', addSourceCode) .on('test_fn_start', executableStart) .on('test_fn_success', executableSuccess) @@ -58,19 +52,39 @@ function addSourceLocation({ Circus.Event & { name: 'add_hook' | 'add_test' } >) { const metadata = realm.runtimeContext.getCurrentMetadata(); - const task = StackTrace.fromError(event.asyncError).then(([frame]) => { - if (frame) { - metadata.set('sourceLocation', { - fileName: frame.fileName, - lineNumber: frame.lineNumber, - columnNumber: frame.columnNumber, - }); + const task = StackTrace.fromError(event.asyncError).then((stackFrames) => { + const first = stackFrames.find((s) => !isLibraryPath(s.fileName)); + if (!first) { + return; } + + const sourceLocation: AllureTestItemSourceLocation = { + fileName: first.fileName, + lineNumber: first.lineNumber, + columnNumber: first.columnNumber, + }; + + metadata.set('sourceLocation', sourceLocation); }); realm.runtimeContext.enqueueTask(task); } +function injectGlobals({ env }: TestEnvironmentSetupEvent) { + env.global.__ALLURE__ = realm; + + const { injectGlobals } = realm.runtimeContext.getReporterConfig(); + if (injectGlobals) { + Object.assign(env.global, api); + } +} + +function setWorkerId() { + realm.runtimeContext + .getFileMetadata() + .set('workerId', process.env.JEST_WORKER_ID); +} + function addHookType({ event, }: TestEnvironmentCircusEvent) { diff --git a/src/metadata/docblock/index.ts b/src/metadata/docblock/index.ts index 3758fa16..69e42e1b 100644 --- a/src/metadata/docblock/index.ts +++ b/src/metadata/docblock/index.ts @@ -1,140 +1 @@ -// import {splitDocblock} from "../../utils"; -// import type { -// AllureTestCaseMetadata, -// AllureTestFileMetadata, -// AllureTestItemMetadata, AllureTestStepMetadata, -// DocblockContext, Label, LabelName, Link, LinkType -// } from "jest-allure2-reporter"; -// -// async function initParser(): Promise { -// try { -// const jestDocblock = await import('jest-docblock'); -// return (snippet) => { -// const [jsdoc] = splitDocblock(snippet); -// const result = jestDocblock.parseWithComments(jsdoc); -// return { -// raw: jsdoc, -// comments: result.comments, -// pragmas: normalize(result.pragmas), -// }; -// }; -// } catch (error: any) { -// // TODO: log warning -// if (error?.code === 'MODULE_NOT_FOUND') { -// return () => void 0; -// } -// -// throw error; -// } -// } -// -// const SPLITTERS: Record string[]> = { -// tag: (string_) => string_.split(/\s*,\s*/), -// }; -// -// function normalize( -// pragmas: Record, -// ): Record { -// const result: Record = {}; -// -// for (const [key, value] of Object.entries(pragmas)) { -// result[key] = Array.isArray(value) ? value : [value]; -// const splitter = SPLITTERS[key]; -// if (splitter) { -// result[key] = result[key].flatMap(splitter); -// } -// } -// -// return result; -// } -// -// function hasDocblockAtStart(string_: string) { -// return /^\s*\/\*\*/.test(string_); -// } -// -// function mergeIntoTestItem( -// metadata: AllureTestItemMetadata, -// comments: string, -// pragmas: Record, -// rawDocblock: string, -// shouldLeaveComments: boolean, -// ) { -// if (comments) { -// metadata.description ??= []; -// metadata.description.push(comments); -// } -// -// if (pragmas.description) { -// metadata.description ??= []; -// metadata.description.push(...pragmas.description); -// } -// -// if (metadata.sourceCode && rawDocblock) { -// const [left, right, ...rest] = metadata.sourceCode.split(rawDocblock); -// const leftTrimmed = left.trimEnd(); -// const replacement = shouldLeaveComments -// ? `/** ${comments.trimStart()} */\n` -// : '\n'; -// const joined = right ? [leftTrimmed, right].join(replacement) : leftTrimmed; -// metadata.sourceCode = [joined, ...rest].join('\n'); -// } -// } -// -// function mergeIntoTestFile( -// metadata: AllureTestFileMetadata, -// docblock: DocblockContext | undefined, -// ) { -// return mergeIntoTestCase(metadata, docblock); -// } -// -// function mergeIntoTestCase( -// metadata: AllureTestCaseMetadata, -// docblock: DocblockContext | undefined, -// ) { -// const { raw = '', comments = '', pragmas = {} } = docblock ?? {}; -// mergeIntoTestItem(metadata, comments, pragmas, raw, false); -// -// const epic = pragmas.epic?.map(createLabelMapper('epic')) ?? []; -// const feature = pragmas.feature?.map(createLabelMapper('feature')) ?? []; -// const owner = pragmas.owner?.map(createLabelMapper('owner')) ?? []; -// const severity = pragmas.severity?.map(createLabelMapper('severity')) ?? []; -// const story = pragmas.story?.map(createLabelMapper('story')) ?? []; -// const tag = pragmas.tag?.map(createLabelMapper('tag')) ?? []; -// const labels = [...epic, ...feature, ...owner, ...severity, ...story, ...tag]; -// if (labels.length > 0) { -// metadata.labels ??= []; -// metadata.labels.push(...labels); -// } -// -// const issue = pragmas.issue?.map(createLinkMapper('issue')) ?? []; -// const tms = pragmas.tms?.map(createLinkMapper('tms')) ?? []; -// const links = [...issue, ...tms]; -// if (links.length > 0) { -// metadata.links ??= []; -// metadata.links.push(...links); -// } -// -// if (pragmas.descriptionHtml) { -// metadata.descriptionHtml ??= []; -// metadata.descriptionHtml.push(...pragmas.descriptionHtml); -// } -// } -// -// function createLabelMapper(name: LabelName) { -// return (value: string): Label => ({ name, value }); -// } -// -// function createLinkMapper(type?: LinkType) { -// return (url: string): Link => ({ type, url, name: url }); -// } -// -// function mergeIntoTestStep( -// metadata: AllureTestStepMetadata, -// docblock: DocblockContext | undefined, -// ) { -// const { raw = '', comments = '', pragmas = {} } = docblock ?? {}; -// mergeIntoTestItem(metadata, comments, pragmas, raw, true); -// } -// -// type DocblockParser = (raw: string) => DocblockContext | undefined; -// +export * from './mapping'; diff --git a/src/metadata/docblock/mapping.ts b/src/metadata/docblock/mapping.ts new file mode 100644 index 00000000..6155bae0 --- /dev/null +++ b/src/metadata/docblock/mapping.ts @@ -0,0 +1,76 @@ +import type { + AllureTestStepMetadata, + AllureTestCaseMetadata, + DocblockContext, + Label, + LabelName, + Link, + LinkType, +} from 'jest-allure2-reporter'; + +export function mapTestStepDocblock({ + comments, +}: DocblockContext): AllureTestStepMetadata { + const metadata: AllureTestStepMetadata = {}; + if (comments) { + metadata.displayName = comments; + } + + return metadata; +} + +export function mapTestCaseDocblock( + context: DocblockContext, +): AllureTestCaseMetadata { + const metadata: AllureTestCaseMetadata = {}; + const { comments, pragmas = {} } = context; + + const labels = ( + ['epic', 'feature', 'owner', 'severity', 'story', 'tag'] as const + ).flatMap((name) => mapMaybeArray(pragmas[name], createLabelMapper(name))); + + if (labels.length > 0) metadata.labels = labels; + + const links = (['issue', 'tms'] as const) + .flatMap((name) => mapMaybeArray(pragmas[name], createLinkMapper(name))) + .filter(Boolean); + + if (links.length > 0) metadata.links = links; + + if (comments || pragmas.description) + metadata.description = [ + ...(comments ? [comments] : []), + ...(pragmas.description || []), + ]; + + if (pragmas.descriptionHtml) { + metadata.descriptionHtml = mapMaybeArray(pragmas.descriptionHtml, (x) => x); + } + + return metadata; +} + +function mapMaybeArray( + value: T | T[] | undefined, + mapper: (value: T) => R, +): R[] { + if (value == null) { + return []; + } + + if (Array.isArray(value)) { + return value.map(mapper); + } + + return [mapper(value)]; +} + +export const mapTestFileDocblock = mapTestCaseDocblock; + +function createLabelMapper(name: LabelName) { + return (value: string): Label => ({ name, value }); +} + +function createLinkMapper(type?: LinkType) { + return (url: string): Link => ({ type, url, name: url }); +} diff --git a/src/metadata/squasher/MetadataSelector.ts b/src/metadata/squasher/MetadataSelector.ts index 2a5ed89c..ec65c594 100644 --- a/src/metadata/squasher/MetadataSelector.ts +++ b/src/metadata/squasher/MetadataSelector.ts @@ -1,4 +1,7 @@ -import type { AllureTestItemMetadata, AllureTestStepMetadata } from 'jest-allure2-reporter'; +import type { + AllureTestItemMetadata, + AllureTestStepMetadata, +} from 'jest-allure2-reporter'; import { weakMemoize } from '../../utils'; diff --git a/src/options/default-options/plugins.ts b/src/options/default-options/plugins.ts index 095fcee6..b7573375 100644 --- a/src/options/default-options/plugins.ts +++ b/src/options/default-options/plugins.ts @@ -1,9 +1,10 @@ import type { PluginContext } from 'jest-allure2-reporter'; -import * as plugins from '../../builtin-plugins'; +import * as plugins from '../../reporter-plugins'; export async function defaultPlugins(context: PluginContext) { return [ + plugins.fallback({}, context), plugins.detect({}, context), plugins.github({}, context), plugins.manifest({}, context), diff --git a/src/builtin-plugins/augs.d.ts b/src/reporter-plugins/augs.d.ts similarity index 100% rename from src/builtin-plugins/augs.d.ts rename to src/reporter-plugins/augs.d.ts diff --git a/src/builtin-plugins/detect.ts b/src/reporter-plugins/detect.ts similarity index 100% rename from src/builtin-plugins/detect.ts rename to src/reporter-plugins/detect.ts diff --git a/src/reporter-plugins/fallback.ts b/src/reporter-plugins/fallback.ts new file mode 100644 index 00000000..1e8190d5 --- /dev/null +++ b/src/reporter-plugins/fallback.ts @@ -0,0 +1,32 @@ +/// + +import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; + +import { ThreadService } from '../reporter/ThreadService'; + +export const fallbackPlugin: PluginConstructor = () => { + const threadService = new ThreadService(); + const plugin: Plugin = { + name: 'jest-allure2-reporter/plugins/fallback', + + onTestFileStart({ test, testFileMetadata }) { + const threadId = threadService.allocateThread(test.path); + testFileMetadata.workerId = String(1 + threadId); + testFileMetadata.start = Date.now(); + }, + + onTestCaseResult({ testCaseMetadata }) { + const stop = testCaseMetadata.stop ?? Number.NaN; + if (Number.isNaN(stop)) { + testCaseMetadata.stop = Date.now(); + } + }, + + onTestFileResult({ test, testFileMetadata }) { + testFileMetadata.stop = Date.now(); + threadService.freeThread(test.path); + }, + }; + + return plugin; +}; diff --git a/src/builtin-plugins/github.ts b/src/reporter-plugins/github.ts similarity index 100% rename from src/builtin-plugins/github.ts rename to src/reporter-plugins/github.ts diff --git a/src/builtin-plugins/index.ts b/src/reporter-plugins/index.ts similarity index 82% rename from src/builtin-plugins/index.ts rename to src/reporter-plugins/index.ts index d9f9300d..e5d91a0f 100644 --- a/src/builtin-plugins/index.ts +++ b/src/reporter-plugins/index.ts @@ -1,4 +1,5 @@ export { detectPlugin as detect } from './detect'; +export { fallbackPlugin as fallback } from './fallback'; export { githubPlugin as github } from './github'; export { manifestPlugin as manifest } from './manifest'; export { prettierPlugin as prettier } from './prettier'; diff --git a/src/builtin-plugins/manifest.ts b/src/reporter-plugins/manifest.ts similarity index 100% rename from src/builtin-plugins/manifest.ts rename to src/reporter-plugins/manifest.ts diff --git a/src/builtin-plugins/prettier.ts b/src/reporter-plugins/prettier.ts similarity index 100% rename from src/builtin-plugins/prettier.ts rename to src/reporter-plugins/prettier.ts diff --git a/src/builtin-plugins/remark.ts b/src/reporter-plugins/remark.ts similarity index 100% rename from src/builtin-plugins/remark.ts rename to src/reporter-plugins/remark.ts diff --git a/src/reporter-plugins/sourceCode.ts b/src/reporter-plugins/sourceCode.ts new file mode 100644 index 00000000..25b5d5d3 --- /dev/null +++ b/src/reporter-plugins/sourceCode.ts @@ -0,0 +1,74 @@ +/// + +import fs from 'node:fs/promises'; + +import type { + AllureTestItemSourceLocation, + Plugin, + PluginConstructor, +} from 'jest-allure2-reporter'; + +export const sourceCodePlugin: PluginConstructor = () => { + const plugin: Plugin = { + name: 'jest-allure2-reporter/plugins/source-code', + + async onTestFileStart({ test, testFileMetadata }) { + testFileMetadata.sourceCode = await fs.readFile(test.path, 'utf8'); + }, + + async onTestCaseResult({ testCaseMetadata }) { + await getSourceCode(testCaseMetadata.sourceLocation); + }, + }; + + let lastFilePath: string | undefined; + let lastFileContent: string | undefined; + + async function getSourceCode( + location?: AllureTestItemSourceLocation, + ): Promise { + if (location && location.fileName && Number.isFinite(location.lineNumber)) { + const fileContent = await getTestFileSourceCode(location.fileName); + if (fileContent) { + return extractByLineAndColumn( + fileContent, + location.lineNumber!, + location.columnNumber, + ); + } + } + + return; + } + + async function getTestFileSourceCode( + testFilePath: string, + ): Promise { + if (lastFilePath !== testFilePath) { + lastFilePath = testFilePath; + if (await fs.access(testFilePath).catch(() => false)) { + lastFileContent = await fs.readFile(testFilePath, 'utf8'); + } + } + + return lastFileContent; + } + + async function extractByLineAndColumn( + fileContent: string, + lineNumber: number, + columnNumber = 1, + ): Promise { + const lines = fileContent.split('\n'); + if (lineNumber > 0 && lineNumber <= lines.length) { + const line = lines[lineNumber - 1]; + if (columnNumber > 0 && columnNumber <= line.length) { + return line.slice(Math.max(0, columnNumber - 1)); + } + } + + return; + } + + return plugin; +}; diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index a976ae9b..a7b3af38 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -28,11 +28,13 @@ import type { import { AllureRuntime } from '@noomorph/allure-js-commons'; import type { AllureGlobalMetadata, + AllureTestCaseMetadata, AllureTestFileMetadata, AllureTestStepMetadata, GlobalExtractorContext, Plugin, PluginHookName, + PluginHookContexts, ReporterConfig, ReporterOptions, TestCaseExtractorContext, @@ -44,14 +46,11 @@ import { resolveOptions } from '../options'; import { AllureMetadataProxy, MetadataSquasher } from '../metadata'; import { md5 } from '../utils'; -import { ThreadService } from './ThreadService'; - export class JestAllure2Reporter extends JestMetadataReporter { private _plugins: readonly Plugin[] = []; private readonly _allure: AllureRuntime; private readonly _config: ReporterConfig; private readonly _globalConfig: Config.GlobalConfig; - private readonly _threadService = new ThreadService(); constructor(globalConfig: Config.GlobalConfig, options: ReporterOptions) { super(globalConfig); @@ -73,59 +72,77 @@ export class JestAllure2Reporter extends JestMetadataReporter { } async onRunStart( - results: AggregatedResult, + aggregatedResult: AggregatedResult, options: ReporterOnStartOptions, ): Promise { this._plugins = await this._config.plugins; - await super.onRunStart(results, options); + await super.onRunStart(aggregatedResult, options); if (this._config.overwrite) { await rimraf(this._config.resultsDir); await fs.mkdir(this._config.resultsDir, { recursive: true }); } + + await this._callPlugins('onRunStart', { + aggregatedResult, + reporterConfig: this._config, + }); } - onTestFileStart(test: Test) { + async onTestFileStart(test: Test) { super.onTestFileStart(test); const testFileMetadata = JestAllure2Reporter.query.test(test); - const threadId = this._threadService.allocateThread(test.path); - const metadataProxy = new AllureMetadataProxy( + const testFileProxy = new AllureMetadataProxy( testFileMetadata, ); - metadataProxy.assign({ - workerId: String(1 + threadId), - start: Date.now(), + await this._callPlugins('onTestFileStart', { + reporterConfig: this._config, + test, + testFileMetadata: testFileProxy.defaults({}).get(), }); } - onTestCaseResult(test: Test, testCaseResult: TestCaseResult) { - const now = Date.now(); + async onTestCaseResult(test: Test, testCaseResult: TestCaseResult) { super.onTestCaseResult(test, testCaseResult); - const metadata = + + const testFileMetadata = JestAllure2Reporter.query.test(test); + const testCaseMetadata = JestAllure2Reporter.query.testCaseResult(testCaseResult).lastInvocation!; - const metadataProxy = new AllureMetadataProxy( - metadata, + const testFileProxy = new AllureMetadataProxy( + testFileMetadata, ); - const stop = metadataProxy.get('stop', Number.NaN); - if (Number.isNaN(stop)) { - metadataProxy.set('stop', now); - } + const testCaseProxy = new AllureMetadataProxy( + testCaseMetadata, + ); + + await this._callPlugins('onTestCaseResult', { + reporterConfig: this._config, + test, + testCaseResult, + testCaseMetadata: testCaseProxy.defaults({}).get(), + testFileMetadata: testFileProxy.defaults({}).get(), + }); } - onTestFileResult( + async onTestFileResult( test: Test, testResult: TestResult, aggregatedResult: AggregatedResult, ) { - this._threadService.freeThread(test.path); - const testFileMetadata = JestAllure2Reporter.query.test(test); - const metadataProxy = new AllureMetadataProxy( + const testFileProxy = new AllureMetadataProxy( testFileMetadata, ); - metadataProxy.set('stop', Date.now()); + + await this._callPlugins('onTestFileResult', { + reporterConfig: this._config, + test, + testResult, + aggregatedResult, + testFileMetadata: testFileProxy.defaults({}).get(), + }); return super.onTestFileResult(test, testResult, aggregatedResult); } @@ -136,6 +153,12 @@ export class JestAllure2Reporter extends JestMetadataReporter { ): Promise { await super.onRunComplete(testContexts, results); + await this._callPlugins('onRunComplete', { + reporterConfig: this._config, + testContexts, + results, + }); + const config = this._config; const globalContext: GlobalExtractorContext = { @@ -427,10 +450,13 @@ export class JestAllure2Reporter extends JestMetadataReporter { } } - async _callPlugins(method: PluginHookName, context: any) { + async _callPlugins( + methodName: K, + context: PluginHookContexts[K], + ) { await Promise.all( this._plugins.map((p) => { - return p[method]?.(context); + return p[methodName]?.(context as any); }), ); } diff --git a/src/runtime-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap b/src/runtime-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap new file mode 100644 index 00000000..3ecc6c89 --- /dev/null +++ b/src/runtime-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`extractJsDoc should extract a broken docblock 1`] = ` +"/** + * This is a broken docblock + * but it's still a docblock +*/" +`; + +exports[`extractJsDoc should extract a multiline docblock 1`] = ` +"/** + * This is a multiline docblock + */" +`; + +exports[`extractJsDoc should extract a single line docblock 1`] = `" /** This is a single line docblock */"`; + +exports[`extractJsDoc should extract a weird two-line docblock 1`] = ` +"/** + * This is a weird two-line docblock */" +`; + +exports[`extractJsDoc should not extract a non-docblock 1`] = `""`; diff --git a/src/runtime-plugins/docblock/extractJsdocAbove.test.ts b/src/runtime-plugins/docblock/extractJsdocAbove.test.ts new file mode 100644 index 00000000..2fae41a6 --- /dev/null +++ b/src/runtime-plugins/docblock/extractJsdocAbove.test.ts @@ -0,0 +1,48 @@ +import { LineNavigator } from '../../runtime/utils'; + +import { extractJsdocAbove as extractJsDocument_ } from './extractJsdocAbove'; + +const FIXTURES = [ + `\ +/** + * This is a multiline docblock + */`, + ' /** This is a single line docblock */', + `/** + * This is a broken docblock + * but it's still a docblock +*/`, + `/** + * This is a weird two-line docblock */`, + `/* + * This is not a docblock + */`, +].map((sourceCode) => sourceCode + '\n' + 'function test() {}\n'); + +describe('extractJsDoc', () => { + const extract = (index: number, line: number) => + extractJsDocument_(new LineNavigator(FIXTURES[index]), line); + + it('should extract a multiline docblock', () => + expect(extract(0, 4)).toMatchSnapshot()); + + it('should extract a single line docblock', () => + expect(extract(1, 2)).toMatchSnapshot()); + + it('should extract a broken docblock', () => + expect(extract(2, 5)).toMatchSnapshot()); + + it('should extract a weird two-line docblock', () => + expect(extract(3, 3)).toMatchSnapshot()); + + it('should not extract a non-docblock', () => + expect(extract(4, 4)).toMatchSnapshot()); + + it('should ignore out of range line index', () => + expect(extract(0, 5)).toBe('')); + + it('should ignore zero line index', () => expect(extract(0, 0)).toBe('')); + + it('should ignore the middle of a docblock', () => + expect(extract(0, 2)).toBe('')); +}); diff --git a/src/runtime-plugins/docblock/extractJsdocAbove.ts b/src/runtime-plugins/docblock/extractJsdocAbove.ts new file mode 100644 index 00000000..cc0849f0 --- /dev/null +++ b/src/runtime-plugins/docblock/extractJsdocAbove.ts @@ -0,0 +1,60 @@ +/* eslint-disable unicorn/prevent-abbreviations */ +import type { LineNavigator } from '../../runtime/utils'; + +export function extractJsdocAbove( + navigator: LineNavigator, + testLineIndex: number, +): string { + if (!navigator.jump(testLineIndex)) return ''; + if (!navigator.prev()) return ''; + + let currentLine = navigator.read(); + const docblockEndIndex = getCommentEnd(currentLine); + if (docblockEndIndex === -1) return ''; + + if (isSingleLineDocblock(currentLine, docblockEndIndex)) { + return currentLine; + } + + const buffer: string[] = []; + buffer.unshift(currentLine.slice(0, Math.max(0, docblockEndIndex + 2))); + + while (navigator.prev()) { + currentLine = navigator.read(); + buffer.unshift(currentLine); + + const start = getCommentStart(currentLine); + if (isDocblockStart(currentLine, start)) { + return buffer.join('\n'); + } + + if (start >= 0) { + break; + } + } + + return ''; +} + +function isSingleLineDocblock(line: string, end: number): boolean { + const start = getCommentStart(line); + if (start < 0) return false; + + return start < end; +} + +function getCommentStart(line: string): number { + const start = line.indexOf('/*'); + if (start <= 0) return start; + + const whitespace = line.slice(0, start); + return whitespace.trim() ? -1 : start; +} + +function getCommentEnd(line: string): number { + return line.lastIndexOf('*/'); +} + +function isDocblockStart(line: string, commentIndex: number): boolean { + return commentIndex >= 0 && line[commentIndex + 2] === '*'; +} diff --git a/src/runtime-plugins/docblock/index.ts b/src/runtime-plugins/docblock/index.ts new file mode 100644 index 00000000..281797cd --- /dev/null +++ b/src/runtime-plugins/docblock/index.ts @@ -0,0 +1,21 @@ +import type { DocblockContext } from 'jest-allure2-reporter'; +import { extract, parseWithComments } from 'jest-docblock'; + +import type { LineNavigator } from '../../runtime/utils'; + +import { extractJsdocAbove } from './extractJsdocAbove'; + +export type DocblockPluginContext = { + navigator: LineNavigator; + lineNumber: number; + columnNumber: number | undefined; +}; + +export function parseJsdoc(context: DocblockPluginContext): DocblockContext { + const { lineNumber, navigator } = context; + const contents = extractJsdocAbove(navigator, lineNumber); + const extracted = extract(contents); + + const { comments, pragmas } = parseWithComments(extracted); + return { comments, pragmas }; +} diff --git a/src/runtime-plugins/index.ts b/src/runtime-plugins/index.ts new file mode 100644 index 00000000..c72749fb --- /dev/null +++ b/src/runtime-plugins/index.ts @@ -0,0 +1 @@ +export * from './docblock'; diff --git a/src/runtime/utils/FileNavigatorCache.ts b/src/runtime/utils/FileNavigatorCache.ts new file mode 100644 index 00000000..c526f09a --- /dev/null +++ b/src/runtime/utils/FileNavigatorCache.ts @@ -0,0 +1,26 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +import { LineNavigator } from './LineNavigator'; + +export class FileNavigatorCache { + #cache = new Map>(); + + async resolve(filePath: string): Promise { + const absolutePath = path.resolve(filePath); + if (!this.#cache.has(absolutePath)) { + this.#cache.set(absolutePath, this.#createNavigator(absolutePath)); + } + + return this.#cache.get(absolutePath)!; + } + + #createNavigator = async (filePath: string) => { + const sourceCode = await fs.readFile(filePath, 'utf8').catch(() => ''); + return new LineNavigator(sourceCode); + }; + + clear() { + this.#cache.clear(); + } +} diff --git a/src/runtime/utils/LineNavigator.test.ts b/src/runtime/utils/LineNavigator.test.ts new file mode 100644 index 00000000..67ef42ea --- /dev/null +++ b/src/runtime/utils/LineNavigator.test.ts @@ -0,0 +1,64 @@ +import { LineNavigator } from './LineNavigator'; + +describe('LineNavigator', () => { + let navigator: LineNavigator; + + beforeAll(() => { + navigator = new LineNavigator('foo\nbar\nbaz'); + }); + + describe('jump', () => { + it('should jump to the first line', () => { + expect(navigator.jump(1)).toBe(true); + expect(navigator.read()).toBe('foo'); + }); + + it('should jump to the second line', () => { + expect(navigator.jump(2)).toBe(true); + expect(navigator.read()).toBe('bar'); + }); + + it('should jump to the third line', () => { + expect(navigator.jump(3)).toBe(true); + expect(navigator.read()).toBe('baz'); + }); + + it('should not jump out of bounds', () => { + expect(navigator.jump(4)).toBe(false); + expect(navigator.read()).toBe('baz'); + }); + }); + + describe('prev/next', () => { + beforeEach(() => navigator.jump(1)); + + it('should go down and up', () => { + expect(navigator.next()).toBe(true); + expect(navigator.read()).toBe('bar'); + expect(navigator.prev()).toBe(true); + expect(navigator.read()).toBe('foo'); + }); + + it('should not go up out of bounds', () => { + expect(navigator.prev()).toBe(false); + expect(navigator.read()).toBe('foo'); + }); + + it('should not go down out of bounds', () => { + expect(navigator.next()).toBe(true); + expect(navigator.next()).toBe(true); + expect(navigator.next()).toBe(false); + expect(navigator.read()).toBe('baz'); + }); + }); + + test('stress test', () => { + // eslint-disable-next-line unicorn/new-for-builtins + const bigString = 'abc\ndef\n'.repeat(500_000); + const navigator = new LineNavigator(bigString); + expect(navigator.jump(1e6)).toBe(true); + expect(navigator.read()).toBe('def'); + expect(navigator.jump(3)).toBe(true); + expect(navigator.read()).toBe('abc'); + }, 200); +}); diff --git a/src/runtime/utils/LineNavigator.ts b/src/runtime/utils/LineNavigator.ts new file mode 100644 index 00000000..81a06fe4 --- /dev/null +++ b/src/runtime/utils/LineNavigator.ts @@ -0,0 +1,62 @@ +export class LineNavigator { + readonly #sourceCode: string; + #cursor = 0; + #line = 1; + #lines: string[] | null = null; + + constructor(sourceCode: string) { + this.#sourceCode = sourceCode; + } + + get sourceCode() { + return this.#sourceCode; + } + + get lines() { + if (this.#lines === null) { + this.#lines = this.#sourceCode.split('\n'); + } + + return this.#lines; + } + + jump(lineIndex: number): boolean { + while (this.#line > lineIndex) { + if (!this.prev()) return false; + } + + while (this.#line < lineIndex) { + if (!this.next()) return false; + } + + return true; + } + + next(): boolean { + const next = this.#sourceCode.indexOf('\n', this.#cursor); + if (next === -1) { + return false; + } + + this.#cursor = next + 1; + this.#line++; + return true; + } + + prev(): boolean { + if (this.#cursor === 0) return false; + + this.#cursor = this.#sourceCode.lastIndexOf('\n', this.#cursor - 2) + 1; + this.#line--; + return true; + } + + read(): string { + const nextIndex = this.#sourceCode.indexOf('\n', this.#cursor); + if (nextIndex === -1) { + return this.#sourceCode.slice(this.#cursor); + } + + return this.#sourceCode.slice(this.#cursor, nextIndex); + } +} diff --git a/src/runtime/utils/index.ts b/src/runtime/utils/index.ts new file mode 100644 index 00000000..cc4e1098 --- /dev/null +++ b/src/runtime/utils/index.ts @@ -0,0 +1,2 @@ +export * from './FileNavigatorCache'; +export * from './LineNavigator'; diff --git a/src/utils/index.ts b/src/utils/index.ts index 444af817..c8aad5c4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,6 +6,7 @@ export * from './hijackFunction'; export * from './isObject'; export * from './isError'; export * from './isJestAssertionError'; +export * from './isLibraryPath'; export * from './isPromiseLike'; export * from './last'; export * from './once'; diff --git a/src/utils/isLibraryPath.test.ts b/src/utils/isLibraryPath.test.ts new file mode 100644 index 00000000..5229f4d5 --- /dev/null +++ b/src/utils/isLibraryPath.test.ts @@ -0,0 +1,23 @@ +import { isLibraryPath } from './isLibraryPath'; + +describe('isLibraryPath', () => { + it('should return true for a node_modules path (POSIX)', () => { + expect(isLibraryPath('/home/x/node_modules/foo/bar')).toBe(true); + }); + + it('should return true for a node_modules path (Windows)', () => { + expect(isLibraryPath('D:\\Project\\node_modules\\foo\\bar')).toBe(true); + }); + + it('should return false for a non-node_modules path', () => { + expect(isLibraryPath('foo/bar')).toBe(false); + }); + + it('should return false for an empty path', () => { + expect(isLibraryPath('')).toBe(false); + }); + + it('should return false for a null path', () => { + expect(isLibraryPath(null as any)).toBe(false); + }); +}); diff --git a/src/utils/isLibraryPath.ts b/src/utils/isLibraryPath.ts new file mode 100644 index 00000000..dedd6507 --- /dev/null +++ b/src/utils/isLibraryPath.ts @@ -0,0 +1,10 @@ +const POSIX_PATTERN = '/node_modules/'; +const WINDOWS_PATTERN = '\\node_modules\\'; + +export function isLibraryPath(filePath: unknown): filePath is string { + if (typeof filePath !== 'string') { + return false; + } + + return filePath.includes(POSIX_PATTERN) || filePath.includes(WINDOWS_PATTERN); +} From 6c830a1eddd2c10cd0adff5f74abb6be9c4ea395 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Mon, 11 Mar 2024 09:00:59 +0200 Subject: [PATCH 05/50] feat: add typescript parsing --- e2e/src/programmatic/grouping/names.test.ts | 17 +++ src/reporter-plugins/sourceCode.ts | 131 +++++++++++++------- 2 files changed, 101 insertions(+), 47 deletions(-) diff --git a/e2e/src/programmatic/grouping/names.test.ts b/e2e/src/programmatic/grouping/names.test.ts index 914980c2..6a172fb9 100644 --- a/e2e/src/programmatic/grouping/names.test.ts +++ b/e2e/src/programmatic/grouping/names.test.ts @@ -45,4 +45,21 @@ describe('Names', () => { allure.description('Extra description (programmatic)'); allure.description('Even more description (programmatic)'); }); + + test.each([ + ['First'], + ['Second'], + ['Third'], + ])('Parametrized test: %s', (name) => { + allure.displayName(`Parametrized test: ${name}`); + }); + + test.each` + name + ${'First'} + ${'Second'} + ${'Third'} + `('Parametrized test 2: $name', ({ name }) => { + allure.displayName(`Parametrized test: ${name}`); + }); }); diff --git a/src/reporter-plugins/sourceCode.ts b/src/reporter-plugins/sourceCode.ts index 25b5d5d3..b21cc5fb 100644 --- a/src/reporter-plugins/sourceCode.ts +++ b/src/reporter-plugins/sourceCode.ts @@ -3,72 +3,109 @@ import fs from 'node:fs/promises'; import type { - AllureTestItemSourceLocation, + AllureTestItemMetadata, Plugin, PluginConstructor, } from 'jest-allure2-reporter'; +// eslint-disable-next-line node/no-unpublished-import +import type ts from 'typescript'; export const sourceCodePlugin: PluginConstructor = () => { - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/source-code', + const promises: Promise[] = []; + // eslint-disable-next-line node/no-unpublished-import,import/no-extraneous-dependencies + const tsPromise = import('typescript').catch(() => null); + const sourceCodeMap = new Map(); + const sourceFileMap = new Map(); - async onTestFileStart({ test, testFileMetadata }) { - testFileMetadata.sourceCode = await fs.readFile(test.path, 'utf8'); - }, + async function ensureSourceCode(filePath: string): Promise { + if (!sourceCodeMap.has(filePath)) { + sourceCodeMap.set(filePath, await fs.readFile(filePath, 'utf8')); + } - async onTestCaseResult({ testCaseMetadata }) { - await getSourceCode(testCaseMetadata.sourceLocation); - }, - }; + return sourceCodeMap.get(filePath)!; + } - let lastFilePath: string | undefined; - let lastFileContent: string | undefined; - - async function getSourceCode( - location?: AllureTestItemSourceLocation, - ): Promise { - if (location && location.fileName && Number.isFinite(location.lineNumber)) { - const fileContent = await getTestFileSourceCode(location.fileName); - if (fileContent) { - return extractByLineAndColumn( - fileContent, - location.lineNumber!, - location.columnNumber, - ); - } + async function ensureSourceFile( + filePath: string, + ): Promise { + if (sourceFileMap.has(filePath)) { + return sourceFileMap.get(filePath); + } + + const ts = await tsPromise; + if (ts) { + const sourceCode = await ensureSourceCode(filePath); + const sourceFile = ts.createSourceFile( + filePath, + sourceCode, + ts.ScriptTarget.Latest, + true, + ); + sourceFileMap.set(filePath, sourceFile); + return sourceFile; } return; } - async function getTestFileSourceCode( - testFilePath: string, - ): Promise { - if (lastFilePath !== testFilePath) { - lastFilePath = testFilePath; - if (await fs.access(testFilePath).catch(() => false)) { - lastFileContent = await fs.readFile(testFilePath, 'utf8'); - } + async function getSourceCode({ + fileName, + lineNumber, + columnNumber = 1, + }: AllureTestItemMetadata['sourceLocation'] = {}) { + if (!fileName || lineNumber == null) { + return; } - return lastFileContent; - } + const ts = await tsPromise; + if (!ts) { + return; + } - async function extractByLineAndColumn( - fileContent: string, - lineNumber: number, - columnNumber = 1, - ): Promise { - const lines = fileContent.split('\n'); - if (lineNumber > 0 && lineNumber <= lines.length) { - const line = lines[lineNumber - 1]; - if (columnNumber > 0 && columnNumber <= line.length) { - return line.slice(Math.max(0, columnNumber - 1)); - } + const sourceFile = await ensureSourceFile(fileName); + if (!sourceFile) { + return; } - return; + const pos = sourceFile.getPositionOfLineAndCharacter( + lineNumber - 1, + columnNumber - 1, + ); + // TODO: find a non-private API for `getTouchingToken` + const token = (ts as any).getTouchingToken(sourceFile, pos) as ts.Node; + let node = token; + while ( + node.kind !== ts.SyntaxKind.ExpressionStatement && + node !== token.parent.parent + ) { + node = node.parent; + } + const expression = node; + const start = expression.getFullStart(); + const end = start + expression.getFullWidth(); + return sourceFile.text.slice(start, end); } + const plugin: Plugin = { + name: 'jest-allure2-reporter/plugins/source-code', + + async onTestFileStart(context) { + promises.push(ensureSourceFile(context.test.path)); + }, + + async onTestCaseResult({ testCaseMetadata }) { + const filePath = testCaseMetadata.sourceLocation?.fileName; + if (!filePath) { + return; + } + + // TODO: promise optimization can be improved + await Promise.allSettled(promises); + testCaseMetadata.sourceCode = + (await getSourceCode(testCaseMetadata.sourceLocation)) ?? + testCaseMetadata.sourceCode; + }, + }; + return plugin; }; From 29773c4e8b8292d9d613236bbe28b1d405211b65 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 15 Mar 2024 21:54:43 +0200 Subject: [PATCH 06/50] refactor: almost there --- index.d.ts | 76 ++++++------ src/environment/listener.ts | 2 +- src/metadata/docblock/mapping.ts | 6 +- src/metadata/squasher/MetadataSquasher.ts | 2 +- .../mergeTestCaseMetadata.test.ts.snap | 40 +++--- .../mergeTestFileMetadata.test.ts.snap | 10 +- src/metadata/squasher/__tests__/fixtures.ts | 2 +- src/metadata/squasher/mergers.ts | 2 +- src/options/default-options/plugins.ts | 8 +- src/options/default-options/testCase.ts | 32 +---- src/options/default-options/testFile.ts | 17 ++- src/reporter-plugins/detect.ts | 34 ----- .../extractJsdocAbove.test.ts.snap | 0 .../docblock/docblockPlugin.ts | 49 ++++++++ .../docblock/extractJsdocAbove.test.ts | 2 +- .../docblock/extractJsdocAbove.ts | 3 +- src/reporter-plugins/docblock/index.ts | 1 + src/reporter-plugins/docblock/parseJsdoc.ts | 17 +++ src/reporter-plugins/fallback.ts | 3 +- src/reporter-plugins/index.ts | 4 +- src/reporter-plugins/remark.ts | 56 +++++---- .../source-code/detectSourceLanguage.ts | 21 ++++ src/reporter-plugins/source-code/index.ts | 1 + .../{ => source-code}/prettier.ts | 10 +- .../source-code/sourceCodePlugin.ts | 117 ++++++++++++++++++ src/reporter-plugins/sourceCode.ts | 111 ----------------- .../utils/FileNavigatorCache.ts | 2 + .../utils/LineNavigator.test.ts | 0 .../utils/LineNavigator.ts | 0 .../utils/ensureTypeScriptAST.ts | 30 +++++ .../utils/extractTypeScriptCode.ts | 54 ++++++++ .../utils/importTypeScript.ts | 11 ++ src/reporter-plugins/utils/index.ts | 5 + src/reporter/JestAllure2Reporter.ts | 40 +++++- src/runtime-plugins/docblock/index.ts | 21 ---- src/runtime-plugins/index.ts | 1 - src/runtime/AllureRuntimeContext.ts | 17 +-- src/runtime/modules/CoreModule.ts | 44 ++++--- src/runtime/utils/index.ts | 2 - src/utils/TaskQueue.ts | 26 ++++ src/utils/index.ts | 3 +- 41 files changed, 533 insertions(+), 349 deletions(-) delete mode 100644 src/reporter-plugins/detect.ts rename src/{runtime-plugins => reporter-plugins}/docblock/__snapshots__/extractJsdocAbove.test.ts.snap (100%) create mode 100644 src/reporter-plugins/docblock/docblockPlugin.ts rename src/{runtime-plugins => reporter-plugins}/docblock/extractJsdocAbove.test.ts (96%) rename src/{runtime-plugins => reporter-plugins}/docblock/extractJsdocAbove.ts (92%) create mode 100644 src/reporter-plugins/docblock/index.ts create mode 100644 src/reporter-plugins/docblock/parseJsdoc.ts create mode 100644 src/reporter-plugins/source-code/detectSourceLanguage.ts create mode 100644 src/reporter-plugins/source-code/index.ts rename src/reporter-plugins/{ => source-code}/prettier.ts (74%) create mode 100644 src/reporter-plugins/source-code/sourceCodePlugin.ts delete mode 100644 src/reporter-plugins/sourceCode.ts rename src/{runtime => reporter-plugins}/utils/FileNavigatorCache.ts (92%) rename src/{runtime => reporter-plugins}/utils/LineNavigator.test.ts (100%) rename src/{runtime => reporter-plugins}/utils/LineNavigator.ts (100%) create mode 100644 src/reporter-plugins/utils/ensureTypeScriptAST.ts create mode 100644 src/reporter-plugins/utils/extractTypeScriptCode.ts create mode 100644 src/reporter-plugins/utils/importTypeScript.ts create mode 100644 src/reporter-plugins/utils/index.ts delete mode 100644 src/runtime-plugins/docblock/index.ts delete mode 100644 src/runtime-plugins/index.ts delete mode 100644 src/runtime/utils/index.ts create mode 100644 src/utils/TaskQueue.ts diff --git a/index.d.ts b/index.d.ts index d46327f0..2c324bd5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -141,31 +141,6 @@ declare module 'jest-allure2-reporter' { /** @see {@link AttachmentsOptions#contentHandler} */ export type BuiltinContentAttachmentHandler = 'write'; - /** @see {@link ReporterOptions#docblock} */ - export type DocblockOptions = { - /** - * Specifies where to look for docblocks: inside functions or outside (on top). - * @default 'outside' - */ - location?: 'inside' | 'outside' | 'both'; - }; - - /** @see {@link ReporterOptions#sourceCode} */ - export type SourceCodeOptions = { - /** - * Specifies where to take the source code from: - * - `file` - read the file from the file system - * - `function` - extract the source code from the test function - * @default 'file' - */ - location?: 'file' | 'function'; - /** - * Whether to prettify the source code before attaching it to the report. - * @default false - */ - prettify?: boolean; - }; - /** * Global customizations for how test cases are reported */ @@ -457,6 +432,7 @@ declare module 'jest-allure2-reporter' { export interface GlobalExtractorContext extends ExtractorContext, GlobalExtractorContextAugmentation { + $: ExtractorHelpers; globalConfig: Config.GlobalConfig; config: ReporterConfig; } @@ -466,7 +442,6 @@ declare module 'jest-allure2-reporter' { TestFileExtractorContextAugmentation { filePath: string[]; testFile: TestResult; - testFileDocblock?: DocblockContext; testFileMetadata: AllureTestFileMetadata; } @@ -474,14 +449,12 @@ declare module 'jest-allure2-reporter' { extends TestFileExtractorContext, TestCaseExtractorContextAugmentation { testCase: TestCaseResult; - testCaseDocblock?: DocblockContext; testCaseMetadata: AllureTestCaseMetadata; } export interface TestStepExtractorContext extends TestCaseExtractorContext, TestStepExtractorContextAugmentation { - testStepDocblock?: DocblockContext; testStepMetadata: AllureTestStepMetadata; } @@ -505,9 +478,9 @@ declare module 'jest-allure2-reporter' { */ currentStep?: AllureTestStepPath; /** - * Parsed docblock: comments, pragmas, and raw content. + * Parsed docblock: comments and pragmas. */ - docblock?: DocblockContext; + docblock?: DocblockExtractorResult; /** * Title of the test case or test step. */ @@ -520,10 +493,6 @@ declare module 'jest-allure2-reporter' { * Key-value pairs to disambiguate test cases or to provide additional information. */ parameters?: Parameter[]; - /** - * Source code of the test case, test step or a hook. - */ - sourceCode?: string; /** * Location (file, line, column) of the test case, test step or a hook. */ @@ -552,6 +521,10 @@ declare module 'jest-allure2-reporter' { * Stop timestamp in milliseconds. */ stop?: number; + /** + * Transformed code of the test case, test step or a hook. + */ + transformedCode?: string; } export type AllureNestedTestStepMetadata = Omit; @@ -588,15 +561,30 @@ declare module 'jest-allure2-reporter' { config: Pick; } - export interface DocblockContext { + export interface DocblockExtractorResult { comments: string; pragmas: Record; } - export interface GlobalExtractorContextAugmentation { - detectLanguage?(contents: string, filePath?: string): string | undefined; - processMarkdown?(markdown: string): Promise; + export type CodeExtractorResult = { + ast?: unknown; + code: string; + language: string; + }; + + export interface ExtractorHelpers extends ExtractorHelpersAugmentation { + extractSourceCode(metadata: AllureTestItemMetadata): CodeExtractorResult | undefined; + extractSourceCodeAsync(metadata: AllureTestItemMetadata): Promise; + extractSourceCodeWithSteps(metadata: AllureTestItemMetadata): CodeExtractorResult[]; + sourceCode2Markdown(sourceCode: Partial | undefined): string; + markdown2html(markdown: string): Promise; + } + + export interface ExtractorHelpersAugmentation { + // This may be extended by plugins + } + export interface GlobalExtractorContextAugmentation { // This may be extended by plugins } @@ -621,7 +609,7 @@ declare module 'jest-allure2-reporter' { export type PluginConstructor = ( options: Record, context: PluginContext, - ) => Plugin; + ) => Plugin | Promise; export type PluginContext = Readonly<{ globalConfig: Readonly; @@ -634,6 +622,11 @@ declare module 'jest-allure2-reporter' { /** Optional method for deduplicating plugins. Return the instance which you want to keep. */ extend?(previous: Plugin): Plugin; + helpers?(helpers: Partial): void | Promise; + + /** Allows to modify the raw metadata before it's processed by the reporter. [UNSTABLE!] */ + rawMetadata?(context: PluginHookContexts['rawMetadata']): void | Promise; + /** Attach to the reporter lifecycle hook `onRunStart`. */ onRunStart?(context: PluginHookContexts['onRunStart']): void | Promise; @@ -663,6 +656,11 @@ declare module 'jest-allure2-reporter' { } export type PluginHookContexts = { + helpers: Partial; + rawMetadata: { + $: Readonly; + metadata: AllureTestItemMetadata; + }; onRunStart: { aggregatedResult: AggregatedResult; reporterConfig: ReporterConfig; diff --git a/src/environment/listener.ts b/src/environment/listener.ts index 45b7555f..5458e4e0 100644 --- a/src/environment/listener.ts +++ b/src/environment/listener.ts @@ -116,7 +116,7 @@ function addSourceCode({ event }: TestEnvironmentCircusEvent) { } if (code) { - realm.runtimeContext.getCurrentMetadata().set('sourceCode', code); + realm.runtimeContext.getCurrentMetadata().set('transformedCode', code); } } diff --git a/src/metadata/docblock/mapping.ts b/src/metadata/docblock/mapping.ts index 6155bae0..0125b093 100644 --- a/src/metadata/docblock/mapping.ts +++ b/src/metadata/docblock/mapping.ts @@ -1,7 +1,7 @@ import type { AllureTestStepMetadata, AllureTestCaseMetadata, - DocblockContext, + DocblockExtractorResult, Label, LabelName, Link, @@ -10,7 +10,7 @@ import type { export function mapTestStepDocblock({ comments, -}: DocblockContext): AllureTestStepMetadata { +}: DocblockExtractorResult): AllureTestStepMetadata { const metadata: AllureTestStepMetadata = {}; if (comments) { metadata.displayName = comments; @@ -20,7 +20,7 @@ export function mapTestStepDocblock({ } export function mapTestCaseDocblock( - context: DocblockContext, + context: DocblockExtractorResult, ): AllureTestCaseMetadata { const metadata: AllureTestCaseMetadata = {}; const { comments, pragmas = {} } = context; diff --git a/src/metadata/squasher/MetadataSquasher.ts b/src/metadata/squasher/MetadataSquasher.ts index adbcbde1..29a6c278 100644 --- a/src/metadata/squasher/MetadataSquasher.ts +++ b/src/metadata/squasher/MetadataSquasher.ts @@ -98,7 +98,6 @@ export class MetadataSquasher { labels: test_vertical.labels, links: test_vertical.links, parameters: test_definition_and_below_direct.parameters, - sourceCode: test_definition_and_below_direct.sourceCode, sourceLocation: test_definition_and_below_direct.sourceLocation, stage: test_invocation_and_below.stage, start: test_invocation_and_below.start, @@ -106,6 +105,7 @@ export class MetadataSquasher { statusDetails: test_invocation_and_below.statusDetails, steps: this._stepSelector.steps(invocation), stop: test_invocation_and_below.stop, + transformedCode: test_definition_and_below_direct.transformedCode, workerId: global_file_and_test_invocation.workerId, }; diff --git a/src/metadata/squasher/__tests__/__snapshots__/mergeTestCaseMetadata.test.ts.snap b/src/metadata/squasher/__tests__/__snapshots__/mergeTestCaseMetadata.test.ts.snap index f78b58ce..733ca6bd 100644 --- a/src/metadata/squasher/__tests__/__snapshots__/mergeTestCaseMetadata.test.ts.snap +++ b/src/metadata/squasher/__tests__/__snapshots__/mergeTestCaseMetadata.test.ts.snap @@ -118,7 +118,6 @@ exports[`mergeTestCaseMetadata belowTestInvocation 1`] = ` "value": "value:hook_invocation", }, ], - "sourceCode": "sourceCode:hook_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_invocation", @@ -132,6 +131,7 @@ exports[`mergeTestCaseMetadata belowTestInvocation 1`] = ` "trace": "trace:hook_invocation", }, "stop": 109, + "transformedCode": "transformedCode:hook_invocation", "workerId": "9", } `; @@ -214,7 +214,6 @@ exports[`mergeTestCaseMetadata globalAndTestFile 1`] = ` "value": "value:file", }, ], - "sourceCode": "sourceCode:file", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:file", @@ -228,6 +227,7 @@ exports[`mergeTestCaseMetadata globalAndTestFile 1`] = ` "trace": "trace:file", }, "stop": 102, + "transformedCode": "transformedCode:file", "workerId": "2", } `; @@ -330,7 +330,6 @@ exports[`mergeTestCaseMetadata globalAndTestFileAndTestInvocation 1`] = ` "value": "value:test_invocation", }, ], - "sourceCode": "sourceCode:test_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:test_invocation", @@ -344,6 +343,7 @@ exports[`mergeTestCaseMetadata globalAndTestFileAndTestInvocation 1`] = ` "trace": "trace:test_invocation", }, "stop": 107, + "transformedCode": "transformedCode:test_invocation", "workerId": "7", } `; @@ -369,7 +369,6 @@ exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` "value": "value:hook_definition", }, ], - "sourceCode": "sourceCode:hook_definition", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_definition", @@ -384,6 +383,7 @@ exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` }, "steps": undefined, "stop": 104, + "transformedCode": "transformedCode:hook_definition", }, { "attachments": [ @@ -402,7 +402,6 @@ exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` "value": "value:hook_definition", }, ], - "sourceCode": "sourceCode:hook_definition", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_definition", @@ -417,6 +416,7 @@ exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` }, "steps": undefined, "stop": 104, + "transformedCode": "transformedCode:hook_definition", }, { "attachments": [ @@ -435,7 +435,6 @@ exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` "value": "value:hook_definition", }, ], - "sourceCode": "sourceCode:hook_definition", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_definition", @@ -450,6 +449,7 @@ exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` }, "steps": undefined, "stop": 104, + "transformedCode": "transformedCode:hook_definition", }, { "attachments": [ @@ -468,7 +468,6 @@ exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` "value": "value:hook_definition", }, ], - "sourceCode": "sourceCode:hook_definition", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_definition", @@ -483,6 +482,7 @@ exports[`mergeTestCaseMetadata steps (no overrides in invocations) 1`] = ` }, "steps": undefined, "stop": 104, + "transformedCode": "transformedCode:hook_definition", }, ] `; @@ -515,7 +515,6 @@ exports[`mergeTestCaseMetadata steps 1`] = ` "value": "value:hook_invocation", }, ], - "sourceCode": "sourceCode:hook_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_invocation", @@ -530,6 +529,7 @@ exports[`mergeTestCaseMetadata steps 1`] = ` }, "steps": undefined, "stop": 109, + "transformedCode": "transformedCode:hook_invocation", }, { "attachments": [ @@ -557,7 +557,6 @@ exports[`mergeTestCaseMetadata steps 1`] = ` "value": "value:hook_invocation", }, ], - "sourceCode": "sourceCode:hook_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_invocation", @@ -572,6 +571,7 @@ exports[`mergeTestCaseMetadata steps 1`] = ` }, "steps": undefined, "stop": 109, + "transformedCode": "transformedCode:hook_invocation", }, { "attachments": [ @@ -599,7 +599,6 @@ exports[`mergeTestCaseMetadata steps 1`] = ` "value": "value:hook_invocation", }, ], - "sourceCode": "sourceCode:hook_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_invocation", @@ -614,6 +613,7 @@ exports[`mergeTestCaseMetadata steps 1`] = ` }, "steps": undefined, "stop": 109, + "transformedCode": "transformedCode:hook_invocation", }, { "attachments": [ @@ -641,7 +641,6 @@ exports[`mergeTestCaseMetadata steps 1`] = ` "value": "value:hook_invocation", }, ], - "sourceCode": "sourceCode:hook_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_invocation", @@ -656,6 +655,7 @@ exports[`mergeTestCaseMetadata steps 1`] = ` }, "steps": undefined, "stop": 109, + "transformedCode": "transformedCode:hook_invocation", }, ] `; @@ -669,7 +669,6 @@ exports[`mergeTestCaseMetadata stepsSelector merge 2`] = ` "displayName": undefined, "hookType": undefined, "parameters": undefined, - "sourceCode": undefined, "sourceLocation": undefined, "stage": undefined, "start": undefined, @@ -677,6 +676,7 @@ exports[`mergeTestCaseMetadata stepsSelector merge 2`] = ` "statusDetails": undefined, "steps": undefined, "stop": undefined, + "transformedCode": undefined, } `; @@ -738,7 +738,6 @@ exports[`mergeTestCaseMetadata testDefinition 1`] = ` "value": "value:test_definition", }, ], - "sourceCode": "sourceCode:test_definition", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:test_definition", @@ -752,6 +751,7 @@ exports[`mergeTestCaseMetadata testDefinition 1`] = ` "trace": "trace:test_definition", }, "stop": 106, + "transformedCode": "transformedCode:test_definition", "workerId": "6", } `; @@ -934,7 +934,6 @@ exports[`mergeTestCaseMetadata testDefinitionAndBelow 1`] = ` "value": "value:hook_invocation", }, ], - "sourceCode": "sourceCode:hook_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_invocation", @@ -948,6 +947,7 @@ exports[`mergeTestCaseMetadata testDefinitionAndBelow 1`] = ` "trace": "trace:hook_invocation", }, "stop": 109, + "transformedCode": "transformedCode:hook_invocation", "workerId": "9", } `; @@ -1050,7 +1050,6 @@ exports[`mergeTestCaseMetadata testDefinitionAndBelowDirect 1`] = ` "value": "value:test_fn_invocation", }, ], - "sourceCode": "sourceCode:test_fn_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:test_fn_invocation", @@ -1064,6 +1063,7 @@ exports[`mergeTestCaseMetadata testDefinitionAndBelowDirect 1`] = ` "trace": "trace:test_fn_invocation", }, "stop": 108, + "transformedCode": "transformedCode:test_fn_invocation", "workerId": "8", } `; @@ -1106,7 +1106,6 @@ exports[`mergeTestCaseMetadata testInvocation 1`] = ` "value": "value:test_invocation", }, ], - "sourceCode": "sourceCode:test_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:test_invocation", @@ -1120,6 +1119,7 @@ exports[`mergeTestCaseMetadata testInvocation 1`] = ` "trace": "trace:test_invocation", }, "stop": 107, + "transformedCode": "transformedCode:test_invocation", "workerId": "7", } `; @@ -1262,7 +1262,6 @@ exports[`mergeTestCaseMetadata testInvocationAndBelow 1`] = ` "value": "value:hook_invocation", }, ], - "sourceCode": "sourceCode:hook_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_invocation", @@ -1276,6 +1275,7 @@ exports[`mergeTestCaseMetadata testInvocationAndBelow 1`] = ` "trace": "trace:hook_invocation", }, "stop": 109, + "transformedCode": "transformedCode:hook_invocation", "workerId": "9", } `; @@ -1338,7 +1338,6 @@ exports[`mergeTestCaseMetadata testInvocationAndBelowDirect 1`] = ` "value": "value:test_fn_invocation", }, ], - "sourceCode": "sourceCode:test_fn_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:test_fn_invocation", @@ -1352,6 +1351,7 @@ exports[`mergeTestCaseMetadata testInvocationAndBelowDirect 1`] = ` "trace": "trace:test_fn_invocation", }, "stop": 108, + "transformedCode": "transformedCode:test_fn_invocation", "workerId": "8", } `; @@ -1534,7 +1534,6 @@ exports[`mergeTestCaseMetadata testVertical (no overrides in step invocations) 1 "value": "value:test_invocation", }, ], - "sourceCode": "sourceCode:test_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:test_invocation", @@ -1548,6 +1547,7 @@ exports[`mergeTestCaseMetadata testVertical (no overrides in step invocations) 1 "trace": "trace:describe_block", }, "stop": 107, + "transformedCode": "transformedCode:test_invocation", "workerId": "7", } `; @@ -1830,7 +1830,6 @@ exports[`mergeTestCaseMetadata testVertical 1`] = ` "value": "value:hook_invocation", }, ], - "sourceCode": "sourceCode:hook_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:hook_invocation", @@ -1844,6 +1843,7 @@ exports[`mergeTestCaseMetadata testVertical 1`] = ` "trace": "trace:hook_invocation", }, "stop": 109, + "transformedCode": "transformedCode:hook_invocation", "workerId": "9", } `; diff --git a/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap b/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap index 78735c0d..bdcf085b 100644 --- a/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap +++ b/src/metadata/squasher/__tests__/__snapshots__/mergeTestFileMetadata.test.ts.snap @@ -58,7 +58,6 @@ exports[`mergeTestFileMetadata getMetadataWithDocblock 1`] = ` "value": "value:file", }, ], - "sourceCode": "sourceCode:file", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:file", @@ -72,6 +71,7 @@ exports[`mergeTestFileMetadata getMetadataWithDocblock 1`] = ` "trace": "trace:file", }, "stop": 102, + "transformedCode": "transformedCode:file", "workerId": "2", } `; @@ -154,7 +154,6 @@ exports[`mergeTestFileMetadata globalAndTestFile 1`] = ` "value": "value:file", }, ], - "sourceCode": "sourceCode:file", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:file", @@ -168,6 +167,7 @@ exports[`mergeTestFileMetadata globalAndTestFile 1`] = ` "trace": "trace:file", }, "stop": 102, + "transformedCode": "transformedCode:file", "workerId": "2", } `; @@ -186,13 +186,13 @@ exports[`mergeTestFileMetadata merge 2`] = ` "labels": undefined, "links": undefined, "parameters": undefined, - "sourceCode": undefined, "sourceLocation": undefined, "stage": undefined, "start": undefined, "status": undefined, "statusDetails": undefined, "stop": undefined, + "transformedCode": undefined, "workerId": undefined, } `; @@ -216,7 +216,6 @@ exports[`mergeTestFileMetadata steps 1`] = ` "value": "value:step_invocation", }, ], - "sourceCode": "sourceCode:step_invocation", "sourceLocation": { "columnNumber": 2, "fileName": "fileName:step_invocation", @@ -236,7 +235,6 @@ exports[`mergeTestFileMetadata steps 1`] = ` "displayName": undefined, "hookType": undefined, "parameters": undefined, - "sourceCode": undefined, "sourceLocation": undefined, "stage": undefined, "start": undefined, @@ -244,9 +242,11 @@ exports[`mergeTestFileMetadata steps 1`] = ` "statusDetails": undefined, "steps": undefined, "stop": undefined, + "transformedCode": undefined, }, ], "stop": 110, + "transformedCode": "transformedCode:step_invocation", }, ] `; diff --git a/src/metadata/squasher/__tests__/fixtures.ts b/src/metadata/squasher/__tests__/fixtures.ts index 28cbc0fd..da17635a 100644 --- a/src/metadata/squasher/__tests__/fixtures.ts +++ b/src/metadata/squasher/__tests__/fixtures.ts @@ -50,12 +50,12 @@ export function createTestItemMetadata( value: `value:${scope}`, }, ], - sourceCode: `sourceCode:${scope}`, sourceLocation: { fileName: `fileName:${scope}`, lineNumber: 1, columnNumber: 2, }, + transformedCode: `transformedCode:${scope}`, stage: castStage(scope), start: castStart(scope), status: castStatus(scope), diff --git a/src/metadata/squasher/mergers.ts b/src/metadata/squasher/mergers.ts index 8ebdda03..792ebb83 100644 --- a/src/metadata/squasher/mergers.ts +++ b/src/metadata/squasher/mergers.ts @@ -58,8 +58,8 @@ function mergeTestItemMetadata( attachments: mergeArrays(a.attachments, b.attachments), currentStep: mergeCurrentStep(a, b), displayName: b.displayName ?? a.displayName, - sourceCode: b.sourceCode ?? a.sourceCode, sourceLocation: b.sourceLocation ?? a.sourceLocation, + transformedCode: b.transformedCode ?? a.transformedCode, parameters: mergeArrays(a.parameters, b.parameters), stage: mergeStage(b.stage, a.stage), start: min(b.start, a.start), diff --git a/src/options/default-options/plugins.ts b/src/options/default-options/plugins.ts index b7573375..f17066c8 100644 --- a/src/options/default-options/plugins.ts +++ b/src/options/default-options/plugins.ts @@ -3,12 +3,12 @@ import type { PluginContext } from 'jest-allure2-reporter'; import * as plugins from '../../reporter-plugins'; export async function defaultPlugins(context: PluginContext) { - return [ + return await Promise.all([ plugins.fallback({}, context), - plugins.detect({}, context), plugins.github({}, context), plugins.manifest({}, context), - plugins.prettier({}, context), plugins.remark({}, context), - ]; + plugins.sourceCode({}, context), + plugins.docblock({}, context), + ]); } diff --git a/src/options/default-options/testCase.ts b/src/options/default-options/testCase.ts index e1644a4f..f31dc0bf 100644 --- a/src/options/default-options/testCase.ts +++ b/src/options/default-options/testCase.ts @@ -2,7 +2,6 @@ import path from 'node:path'; import type { TestCaseResult } from '@jest/reporters'; import type { - AllureTestStepMetadata, ExtractorContext, Label, ResolvedTestCaseCustomizer, @@ -23,18 +22,6 @@ const identity = (context: ExtractorContext) => context.value; const last = (context: ExtractorContext) => context.value?.at(-1); const all = identity; -function extractCode( - steps: AllureTestStepMetadata[] | undefined, -): string | undefined { - return joinCode(steps?.map((step) => step.sourceCode)); -} - -function joinCode( - code: undefined | (string | undefined)[], -): string | undefined { - return code?.filter(Boolean).join('\n\n') || undefined; -} - export const testCase: ResolvedTestCaseCustomizer = { hidden: () => false, historyId: ({ testCase, testCaseMetadata }) => @@ -43,22 +30,11 @@ export const testCase: ResolvedTestCaseCustomizer = { testCaseMetadata.displayName ?? testCase.title, fullName: ({ testCase, testCaseMetadata }) => testCaseMetadata.fullName ?? testCase.fullName, - description: ({ testCaseMetadata }) => { + description: ({ $, testCaseMetadata }) => { const text = testCaseMetadata.description?.join('\n\n') ?? ''; - const before = extractCode( - testCaseMetadata.steps?.filter( - (step) => - step.hookType === 'beforeAll' || step.hookType === 'beforeEach', - ), - ); - const after = extractCode( - testCaseMetadata.steps?.filter( - (step) => step.hookType === 'afterAll' || step.hookType === 'afterEach', - ), - ); - const code = joinCode([before, testCaseMetadata.sourceCode, after]); - const snippet = code ? '```javascript\n' + code + '\n```' : ''; - return [text, snippet].filter(Boolean).join('\n\n'); + const codes = $.extractSourceCodeWithSteps(testCaseMetadata); + const snippets = codes.map($.sourceCode2Markdown); + return [text, ...snippets].filter(Boolean).join('\n\n'); }, descriptionHtml: ({ testCaseMetadata }) => testCaseMetadata.descriptionHtml?.join('\n'), diff --git a/src/options/default-options/testFile.ts b/src/options/default-options/testFile.ts index 4667e41e..4136b420 100644 --- a/src/options/default-options/testFile.ts +++ b/src/options/default-options/testFile.ts @@ -1,20 +1,20 @@ -import fs from 'node:fs'; import path from 'node:path'; import type { ExtractorContext, - TestFileExtractorContext, + Label, + Link, ResolvedTestFileCustomizer, TestCaseCustomizer, + TestFileExtractorContext, } from 'jest-allure2-reporter'; -import type { Label, Link } from 'jest-allure2-reporter'; +import { getStatusDetails } from '../../utils'; import { aggregateLabelCustomizers, composeExtractors, stripStatusDetails, } from '../utils'; -import { getStatusDetails } from '../../utils'; const identity = (context: ExtractorContext) => context.value; const last = (context: ExtractorContext) => context.value?.at(-1); @@ -26,13 +26,10 @@ export const testFile: ResolvedTestFileCustomizer = { name: ({ filePath }) => filePath.join(path.sep), fullName: ({ globalConfig, testFile }) => path.relative(globalConfig.rootDir, testFile.testFilePath), - description: ({ detectLanguage, testFile, testFileMetadata }) => { + description: ({ $, testFileMetadata }) => { const text = testFileMetadata.description?.join('\n') ?? ''; - const contents = fs.readFileSync(testFile.testFilePath, 'utf8'); - const lang = detectLanguage?.(testFile.testFilePath, contents) ?? ''; - const fence = '```'; - const code = `${fence}${lang}\n${contents}\n${fence}`; - return [text, code].filter(Boolean).join('\n\n'); + const code = $.extractSourceCode(testFileMetadata); + return [text, $.sourceCode2Markdown(code)].filter(Boolean).join('\n\n'); }, descriptionHtml: ({ testFileMetadata }) => testFileMetadata.descriptionHtml?.join('\n'), diff --git a/src/reporter-plugins/detect.ts b/src/reporter-plugins/detect.ts deleted file mode 100644 index 0c9d7a71..00000000 --- a/src/reporter-plugins/detect.ts +++ /dev/null @@ -1,34 +0,0 @@ -/// - -import path from 'node:path'; - -import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; - -export const detectPlugin: PluginConstructor = () => { - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/detect', - async globalContext(context) { - context.detectLanguage = (filePath) => { - switch (path.extname(filePath)) { - case '.js': - case '.jsx': - case '.cjs': - case '.mjs': { - return 'javascript'; - } - case '.ts': - case '.tsx': - case '.cts': - case '.mts': { - return 'typescript'; - } - default: { - return ''; - } - } - }; - }, - }; - - return plugin; -}; diff --git a/src/runtime-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap b/src/reporter-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap similarity index 100% rename from src/runtime-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap rename to src/reporter-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap diff --git a/src/reporter-plugins/docblock/docblockPlugin.ts b/src/reporter-plugins/docblock/docblockPlugin.ts new file mode 100644 index 00000000..feb2fea2 --- /dev/null +++ b/src/reporter-plugins/docblock/docblockPlugin.ts @@ -0,0 +1,49 @@ +// eslint-disable-next-line node/no-unpublished-import +import type TypeScript from 'typescript'; +import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; +import { extract, parseWithComments } from 'jest-docblock'; + +import { + extractTypeScriptCode, + FileNavigatorCache, + importTypeScript, +} from '../utils'; + +import { extractJsdocAbove } from './extractJsdocAbove'; + +export const docblockPlugin: PluginConstructor = () => { + const plugin: Plugin = { + name: 'jest-allure2-reporter/plugins/docblock', + + async rawMetadata({ $, metadata }) { + const { fileName, lineNumber, columnNumber } = + metadata.sourceLocation || {}; + const code = await $.extractSourceCodeAsync(metadata); + let extracted: string | undefined; + const ts = await importTypeScript(); + if (ts && code?.ast && lineNumber != null && columnNumber != null) { + const fullCode = await extractTypeScriptCode( + ts, + code.ast as TypeScript.SourceFile, + [lineNumber, columnNumber], + true, + ); + if (fullCode) { + extracted = extract(fullCode); + } + } + + if (!extracted && fileName && lineNumber != null) { + const navigator = await FileNavigatorCache.instance.resolve(fileName); + extracted = extractJsdocAbove(navigator, lineNumber); + } + + if (extracted) { + const { comments, pragmas } = parseWithComments(extracted); + metadata.docblock = { comments, pragmas }; + } + }, + }; + + return plugin; +}; diff --git a/src/runtime-plugins/docblock/extractJsdocAbove.test.ts b/src/reporter-plugins/docblock/extractJsdocAbove.test.ts similarity index 96% rename from src/runtime-plugins/docblock/extractJsdocAbove.test.ts rename to src/reporter-plugins/docblock/extractJsdocAbove.test.ts index 2fae41a6..f1c40ffc 100644 --- a/src/runtime-plugins/docblock/extractJsdocAbove.test.ts +++ b/src/reporter-plugins/docblock/extractJsdocAbove.test.ts @@ -1,4 +1,4 @@ -import { LineNavigator } from '../../runtime/utils'; +import { LineNavigator } from '../utils'; import { extractJsdocAbove as extractJsDocument_ } from './extractJsdocAbove'; diff --git a/src/runtime-plugins/docblock/extractJsdocAbove.ts b/src/reporter-plugins/docblock/extractJsdocAbove.ts similarity index 92% rename from src/runtime-plugins/docblock/extractJsdocAbove.ts rename to src/reporter-plugins/docblock/extractJsdocAbove.ts index cc0849f0..617f0a51 100644 --- a/src/runtime-plugins/docblock/extractJsdocAbove.ts +++ b/src/reporter-plugins/docblock/extractJsdocAbove.ts @@ -1,5 +1,4 @@ -/* eslint-disable unicorn/prevent-abbreviations */ -import type { LineNavigator } from '../../runtime/utils'; +import type { LineNavigator } from '../utils'; export function extractJsdocAbove( navigator: LineNavigator, diff --git a/src/reporter-plugins/docblock/index.ts b/src/reporter-plugins/docblock/index.ts new file mode 100644 index 00000000..00a9b5f6 --- /dev/null +++ b/src/reporter-plugins/docblock/index.ts @@ -0,0 +1 @@ +export * from './docblockPlugin'; diff --git a/src/reporter-plugins/docblock/parseJsdoc.ts b/src/reporter-plugins/docblock/parseJsdoc.ts new file mode 100644 index 00000000..645dccf9 --- /dev/null +++ b/src/reporter-plugins/docblock/parseJsdoc.ts @@ -0,0 +1,17 @@ +import type { DocblockExtractorResult } from 'jest-allure2-reporter'; +import { extract, parseWithComments } from 'jest-docblock'; + +import type { LineNavigator } from '../utils'; + +import { extractJsdocAbove } from './extractJsdocAbove'; + +export function parseJsdoc( + navigator: LineNavigator, + lineNumber: number, +): DocblockExtractorResult { + const contents = extractJsdocAbove(navigator, lineNumber); + const extracted = extract(contents); + + const { comments, pragmas } = parseWithComments(extracted); + return { comments, pragmas }; +} diff --git a/src/reporter-plugins/fallback.ts b/src/reporter-plugins/fallback.ts index 1e8190d5..357d8c06 100644 --- a/src/reporter-plugins/fallback.ts +++ b/src/reporter-plugins/fallback.ts @@ -11,8 +11,9 @@ export const fallbackPlugin: PluginConstructor = () => { onTestFileStart({ test, testFileMetadata }) { const threadId = threadService.allocateThread(test.path); - testFileMetadata.workerId = String(1 + threadId); + testFileMetadata.sourceLocation = { fileName: test.path }; testFileMetadata.start = Date.now(); + testFileMetadata.workerId = String(1 + threadId); }, onTestCaseResult({ testCaseMetadata }) { diff --git a/src/reporter-plugins/index.ts b/src/reporter-plugins/index.ts index e5d91a0f..579f262e 100644 --- a/src/reporter-plugins/index.ts +++ b/src/reporter-plugins/index.ts @@ -1,6 +1,6 @@ -export { detectPlugin as detect } from './detect'; +export { docblockPlugin as docblock } from './docblock'; export { fallbackPlugin as fallback } from './fallback'; export { githubPlugin as github } from './github'; export { manifestPlugin as manifest } from './manifest'; -export { prettierPlugin as prettier } from './prettier'; export { remarkPlugin as remark } from './remark'; +export { sourceCodePlugin as sourceCode } from './source-code'; diff --git a/src/reporter-plugins/remark.ts b/src/reporter-plugins/remark.ts index 1930f593..dbde8a35 100644 --- a/src/reporter-plugins/remark.ts +++ b/src/reporter-plugins/remark.ts @@ -2,36 +2,40 @@ import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; -export const remarkPlugin: PluginConstructor = () => { - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/remark', - async globalContext(context) { - const remark = await import('remark'); - const [ - remarkGfm, - remarkRehype, - rehypeSanitize, - rehypeStringify, - rehypeHighlight, - ] = await Promise.all([ - import('remark-gfm'), - import('remark-rehype'), - import('rehype-sanitize'), - import('rehype-stringify'), - import('rehype-highlight'), - ]); +export const remarkPlugin: PluginConstructor = async () => { + const remark = await import('remark'); + const [ + remarkGfm, + remarkRehype, + rehypeSanitize, + rehypeStringify, + rehypeHighlight, + ] = await Promise.all([ + import('remark-gfm'), + import('remark-rehype'), + import('rehype-sanitize'), + import('rehype-stringify'), + import('rehype-highlight'), + ]); - const processor = remark - .remark() - .use(remarkGfm.default) - .use(remarkRehype.default) - .use(rehypeSanitize.default) - .use(rehypeHighlight.default) - .use(rehypeStringify.default); + const processor = remark + .remark() + .use(remarkGfm.default) + .use(remarkRehype.default) + .use(rehypeSanitize.default) + .use(rehypeHighlight.default) + .use(rehypeStringify.default); - context.processMarkdown = async (markdown: string) => { + const plugin: Plugin = { + name: 'jest-allure2-reporter/plugins/remark', + helpers($) { + $.markdown2html = async (markdown: string) => { return processor.process(markdown).then((result) => result.toString()); }; + + $.sourceCode2Markdown = ({ code, language = '' } = {}) => { + return code ? '```' + language + '\n' + code + '\n```' : ''; + }; }, }; diff --git a/src/reporter-plugins/source-code/detectSourceLanguage.ts b/src/reporter-plugins/source-code/detectSourceLanguage.ts new file mode 100644 index 00000000..32174448 --- /dev/null +++ b/src/reporter-plugins/source-code/detectSourceLanguage.ts @@ -0,0 +1,21 @@ +import path from 'node:path'; + +export function detectSourceLanguage(fileName: string): string { + switch (path.extname(fileName)) { + case '.js': + case '.jsx': + case '.cjs': + case '.mjs': { + return 'javascript'; + } + case '.ts': + case '.tsx': + case '.cts': + case '.mts': { + return 'typescript'; + } + default: { + return ''; + } + } +} diff --git a/src/reporter-plugins/source-code/index.ts b/src/reporter-plugins/source-code/index.ts new file mode 100644 index 00000000..eab7ed14 --- /dev/null +++ b/src/reporter-plugins/source-code/index.ts @@ -0,0 +1 @@ +export * from './sourceCodePlugin'; diff --git a/src/reporter-plugins/prettier.ts b/src/reporter-plugins/source-code/prettier.ts similarity index 74% rename from src/reporter-plugins/prettier.ts rename to src/reporter-plugins/source-code/prettier.ts index 60e53266..89ae0b68 100644 --- a/src/reporter-plugins/prettier.ts +++ b/src/reporter-plugins/source-code/prettier.ts @@ -1,4 +1,4 @@ -/// +/// import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; import type { Options } from 'prettier'; @@ -21,18 +21,18 @@ export const prettierPlugin: PluginConstructor = ( }; }, async testCaseContext(context) { - const code = context.testCaseMetadata.sourceCode; + const code = context.testCaseMetadata.transformedCode; if (code) { - context.testCaseMetadata.sourceCode = await prettier.format( + context.testCaseMetadata.transformedCode = await prettier.format( code.trim(), prettierConfig, ); } }, async testStepContext(context) { - const code = context.testStepMetadata.sourceCode; + const code = context.testStepMetadata.transformedCode; if (code) { - context.testStepMetadata.sourceCode = await prettier.format( + context.testStepMetadata.transformedCode = await prettier.format( code.trim(), prettierConfig, ); diff --git a/src/reporter-plugins/source-code/sourceCodePlugin.ts b/src/reporter-plugins/source-code/sourceCodePlugin.ts new file mode 100644 index 00000000..41edf7a9 --- /dev/null +++ b/src/reporter-plugins/source-code/sourceCodePlugin.ts @@ -0,0 +1,117 @@ +/// + +import type { + AllureTestItemSourceLocation, + AllureNestedTestStepMetadata, + CodeExtractorResult, + Plugin, + PluginConstructor, +} from 'jest-allure2-reporter'; + +import { + ensureTypeScriptAST, + extractTypeScriptCode, + FileNavigatorCache, + importTypeScript, +} from '../utils'; + +import { detectSourceLanguage } from './detectSourceLanguage'; + +function isBeforeHook({ hookType }: AllureNestedTestStepMetadata) { + return hookType === 'beforeAll' || hookType === 'beforeEach'; +} + +function isAfterHook({ hookType }: AllureNestedTestStepMetadata) { + return hookType === 'afterAll' || hookType === 'afterEach'; +} + +function isDefined(value: T | undefined): value is T { + return value !== undefined; +} + +export const sourceCodePlugin: PluginConstructor = async () => { + const ts = await importTypeScript(); + + const promiseCache = new WeakMap< + AllureTestItemSourceLocation, + Promise + >(); + const cache = new WeakMap< + AllureTestItemSourceLocation, + CodeExtractorResult + >(); + const extractAndCache = async ( + sourceLocation: AllureTestItemSourceLocation | undefined, + ) => { + if (!sourceLocation?.fileName) { + return; + } + + await FileNavigatorCache.instance.resolve(sourceLocation.fileName); + const language = detectSourceLanguage(sourceLocation.fileName); + if ((language === 'typescript' || language === 'javascript') && ts) { + const ast = await ensureTypeScriptAST(ts, sourceLocation.fileName); + const location = [ + sourceLocation.lineNumber, + sourceLocation.columnNumber, + ] as const; + const code = await extractTypeScriptCode(ts, ast, location); + if (code) { + cache.set(sourceLocation, { + code, + language, + ast, + }); + } + } + + return cache.get(sourceLocation); + }; + + const plugin: Plugin = { + name: 'jest-allure2-reporter/plugins/source-code', + + async helpers($) { + const extractSourceCode = (metadata: { + sourceLocation?: AllureTestItemSourceLocation; + }) => { + return metadata.sourceLocation + ? cache.get(metadata.sourceLocation) + : undefined; + }; + + $.extractSourceCode = extractSourceCode; + + $.extractSourceCodeAsync = async (metadata) => { + if (!metadata.sourceLocation) { + return; + } + + if (!promiseCache.has(metadata.sourceLocation)) { + promiseCache.set( + metadata.sourceLocation, + extractAndCache(metadata.sourceLocation), + ); + } + + return promiseCache.get(metadata.sourceLocation); + }; + + $.extractSourceCodeWithSteps = (metadata) => { + const test = extractSourceCode(metadata); + const before = + metadata.steps?.filter(isBeforeHook)?.map(extractSourceCode) ?? []; + const after = + metadata.steps?.filter(isAfterHook)?.map(extractSourceCode) ?? []; + + return [...before, test, ...after].filter(isDefined); + }; + }, + + async rawMetadata(context) { + await context.$.extractSourceCodeAsync(context.metadata); + }, + }; + + return plugin; +}; diff --git a/src/reporter-plugins/sourceCode.ts b/src/reporter-plugins/sourceCode.ts deleted file mode 100644 index b21cc5fb..00000000 --- a/src/reporter-plugins/sourceCode.ts +++ /dev/null @@ -1,111 +0,0 @@ -/// - -import fs from 'node:fs/promises'; - -import type { - AllureTestItemMetadata, - Plugin, - PluginConstructor, -} from 'jest-allure2-reporter'; -// eslint-disable-next-line node/no-unpublished-import -import type ts from 'typescript'; - -export const sourceCodePlugin: PluginConstructor = () => { - const promises: Promise[] = []; - // eslint-disable-next-line node/no-unpublished-import,import/no-extraneous-dependencies - const tsPromise = import('typescript').catch(() => null); - const sourceCodeMap = new Map(); - const sourceFileMap = new Map(); - - async function ensureSourceCode(filePath: string): Promise { - if (!sourceCodeMap.has(filePath)) { - sourceCodeMap.set(filePath, await fs.readFile(filePath, 'utf8')); - } - - return sourceCodeMap.get(filePath)!; - } - - async function ensureSourceFile( - filePath: string, - ): Promise { - if (sourceFileMap.has(filePath)) { - return sourceFileMap.get(filePath); - } - - const ts = await tsPromise; - if (ts) { - const sourceCode = await ensureSourceCode(filePath); - const sourceFile = ts.createSourceFile( - filePath, - sourceCode, - ts.ScriptTarget.Latest, - true, - ); - sourceFileMap.set(filePath, sourceFile); - return sourceFile; - } - - return; - } - - async function getSourceCode({ - fileName, - lineNumber, - columnNumber = 1, - }: AllureTestItemMetadata['sourceLocation'] = {}) { - if (!fileName || lineNumber == null) { - return; - } - - const ts = await tsPromise; - if (!ts) { - return; - } - - const sourceFile = await ensureSourceFile(fileName); - if (!sourceFile) { - return; - } - - const pos = sourceFile.getPositionOfLineAndCharacter( - lineNumber - 1, - columnNumber - 1, - ); - // TODO: find a non-private API for `getTouchingToken` - const token = (ts as any).getTouchingToken(sourceFile, pos) as ts.Node; - let node = token; - while ( - node.kind !== ts.SyntaxKind.ExpressionStatement && - node !== token.parent.parent - ) { - node = node.parent; - } - const expression = node; - const start = expression.getFullStart(); - const end = start + expression.getFullWidth(); - return sourceFile.text.slice(start, end); - } - - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/source-code', - - async onTestFileStart(context) { - promises.push(ensureSourceFile(context.test.path)); - }, - - async onTestCaseResult({ testCaseMetadata }) { - const filePath = testCaseMetadata.sourceLocation?.fileName; - if (!filePath) { - return; - } - - // TODO: promise optimization can be improved - await Promise.allSettled(promises); - testCaseMetadata.sourceCode = - (await getSourceCode(testCaseMetadata.sourceLocation)) ?? - testCaseMetadata.sourceCode; - }, - }; - - return plugin; -}; diff --git a/src/runtime/utils/FileNavigatorCache.ts b/src/reporter-plugins/utils/FileNavigatorCache.ts similarity index 92% rename from src/runtime/utils/FileNavigatorCache.ts rename to src/reporter-plugins/utils/FileNavigatorCache.ts index c526f09a..548af87d 100644 --- a/src/runtime/utils/FileNavigatorCache.ts +++ b/src/reporter-plugins/utils/FileNavigatorCache.ts @@ -23,4 +23,6 @@ export class FileNavigatorCache { clear() { this.#cache.clear(); } + + static readonly instance = new FileNavigatorCache(); } diff --git a/src/runtime/utils/LineNavigator.test.ts b/src/reporter-plugins/utils/LineNavigator.test.ts similarity index 100% rename from src/runtime/utils/LineNavigator.test.ts rename to src/reporter-plugins/utils/LineNavigator.test.ts diff --git a/src/runtime/utils/LineNavigator.ts b/src/reporter-plugins/utils/LineNavigator.ts similarity index 100% rename from src/runtime/utils/LineNavigator.ts rename to src/reporter-plugins/utils/LineNavigator.ts diff --git a/src/reporter-plugins/utils/ensureTypeScriptAST.ts b/src/reporter-plugins/utils/ensureTypeScriptAST.ts new file mode 100644 index 00000000..6fc64f7e --- /dev/null +++ b/src/reporter-plugins/utils/ensureTypeScriptAST.ts @@ -0,0 +1,30 @@ +// eslint-disable-next-line node/no-unpublished-import +import type TypeScript from 'typescript'; + +import { FileNavigatorCache } from '../utils'; + +const sourceFileMap = new Map< + string, + Promise +>(); + +export function ensureTypeScriptAST( + ts: typeof TypeScript, + filePath: string, +): Promise { + if (!sourceFileMap.has(filePath)) { + sourceFileMap.set(filePath, parseFile(ts, filePath)); + } + + return sourceFileMap.get(filePath)!; +} + +async function parseFile( + ts: typeof TypeScript, + filePath: string, +): Promise { + const { sourceCode } = await FileNavigatorCache.instance.resolve(filePath); + return sourceCode + ? ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true) + : undefined; +} diff --git a/src/reporter-plugins/utils/extractTypeScriptCode.ts b/src/reporter-plugins/utils/extractTypeScriptCode.ts new file mode 100644 index 00000000..a728ba33 --- /dev/null +++ b/src/reporter-plugins/utils/extractTypeScriptCode.ts @@ -0,0 +1,54 @@ +// eslint-disable-next-line node/no-unpublished-import +import type TypeScript from 'typescript'; + +export async function extractTypeScriptCode( + ts: typeof TypeScript, + ast: TypeScript.SourceFile | undefined, + [lineNumber, columnNumber]: readonly [number | undefined, number | undefined], + includeComments = false, +): Promise { + if (lineNumber == null || columnNumber == null || ast == null) { + return; + } + + const pos = ast.getPositionOfLineAndCharacter( + lineNumber - 1, + columnNumber - 1, + ); + + // TODO: find a non-private API for `getTouchingToken` + const token = (ts as any).getTouchingToken(ast, pos) as TypeScript.Node; + let node = token; + while ( + node.kind !== ts.SyntaxKind.ExpressionStatement && + node !== token.parent.parent + ) { + node = node.parent; + } + const expression = node; + if (includeComments) { + const start = expression.getFullStart(); + return ast.text.slice(start, start + expression.getFullWidth()); + } else { + return autoIndent( + ast.text.slice(expression.getStart(), expression.getEnd()), + ); + } +} + +function autoIndent(text: string) { + const [first, ...rest] = text.split('\n'); + const indent = detectIndent(rest); + if (indent > 0) { + return [first, ...rest.map((line) => line.slice(indent))].join('\n'); + } + + return text; +} + +function detectIndent(lines: string[]) { + return lines.reduce((indent, line) => { + const size = line.length - line.trimStart().length; + return size < indent ? size : indent; + }, Number.POSITIVE_INFINITY); +} diff --git a/src/reporter-plugins/utils/importTypeScript.ts b/src/reporter-plugins/utils/importTypeScript.ts new file mode 100644 index 00000000..5c4a72f8 --- /dev/null +++ b/src/reporter-plugins/utils/importTypeScript.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-extraneous-dependencies,node/no-unpublished-import */ +import type TypeScript from 'typescript'; + +export async function importTypeScript(): Promise { + const ts = await import('typescript').catch(() => null); + if (ts) { + return ts.default; + } + + return ts; +} diff --git a/src/reporter-plugins/utils/index.ts b/src/reporter-plugins/utils/index.ts new file mode 100644 index 00000000..15332e10 --- /dev/null +++ b/src/reporter-plugins/utils/index.ts @@ -0,0 +1,5 @@ +export * from './ensureTypeScriptAST'; +export * from './extractTypeScriptCode'; +export * from './importTypeScript'; +export * from './FileNavigatorCache'; +export * from './LineNavigator'; diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index a7b3af38..aefcb3b1 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -10,7 +10,9 @@ import type { TestContext, TestResult, } from '@jest/reporters'; +import type { Metadata } from 'jest-metadata'; import { state } from 'jest-metadata'; +import { metadataRegistryEvents } from 'jest-metadata/debug'; import JestMetadataReporter from 'jest-metadata/reporter'; import rimraf from 'rimraf'; import type { @@ -31,6 +33,7 @@ import type { AllureTestCaseMetadata, AllureTestFileMetadata, AllureTestStepMetadata, + ExtractorHelpers, GlobalExtractorContext, Plugin, PluginHookName, @@ -48,9 +51,11 @@ import { md5 } from '../utils'; export class JestAllure2Reporter extends JestMetadataReporter { private _plugins: readonly Plugin[] = []; + private readonly _$: Partial = {}; private readonly _allure: AllureRuntime; private readonly _config: ReporterConfig; private readonly _globalConfig: Config.GlobalConfig; + private readonly _newMetadata: Metadata[] = []; constructor(globalConfig: Config.GlobalConfig, options: ReporterOptions) { super(globalConfig); @@ -62,6 +67,8 @@ export class JestAllure2Reporter extends JestMetadataReporter { resultsDir: this._config.resultsDir, }); + metadataRegistryEvents.on('register_metadata', this._registerMetadata); + const globalMetadata = new AllureMetadataProxy(state); globalMetadata.set('config', { resultsDir: this._config.resultsDir, @@ -84,6 +91,8 @@ export class JestAllure2Reporter extends JestMetadataReporter { await fs.mkdir(this._config.resultsDir, { recursive: true }); } + await this._callPlugins('helpers', this._$); + await this._callPlugins('onRunStart', { aggregatedResult, reporterConfig: this._config, @@ -153,6 +162,8 @@ export class JestAllure2Reporter extends JestMetadataReporter { ): Promise { await super.onRunComplete(testContexts, results); + metadataRegistryEvents.off('register_metadata', this._registerMetadata); + await this._callPlugins('onRunComplete', { reporterConfig: this._config, testContexts, @@ -165,6 +176,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { globalConfig: this._globalConfig, config, value: undefined, + $: this._$ as ExtractorHelpers, }; await this._callPlugins('globalContext', globalContext); @@ -184,6 +196,8 @@ export class JestAllure2Reporter extends JestMetadataReporter { this._allure.writeCategoriesDefinitions(categories as Category[]); } + await this._postProcessMetadata(); // Run before squashing + const docblockParser: any = { find: () => void 0 }; // TODO: await initParser(); const squasher = new MetadataSquasher({ getDocblockMetadata: (metadata) => @@ -386,10 +400,10 @@ export class JestAllure2Reporter extends JestMetadataReporter { context: GlobalExtractorContext, test: AllurePayloadTest, ) { - if (test.description && context.processMarkdown) { + if (test.description) { test.descriptionHtml = (test.descriptionHtml ? test.descriptionHtml + '\n' : '') + - (await context.processMarkdown(test.description)); + (await context.$.markdown2html(test.description)); } } @@ -450,7 +464,21 @@ export class JestAllure2Reporter extends JestMetadataReporter { } } - async _callPlugins( + private async _postProcessMetadata() { + const newBatch = this._newMetadata.splice(0, this._newMetadata.length); + + await Promise.all( + newBatch.map(async (metadata) => { + const allureProxy = new AllureMetadataProxy(metadata); + await this._callPlugins('rawMetadata', { + $: this._$ as ExtractorHelpers, + metadata: allureProxy.assign({}).get(), + }); + }), + ); + } + + private async _callPlugins( methodName: K, context: PluginHookContexts[K], ) { @@ -461,7 +489,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { ); } - _relativizeAttachment = (attachment: Attachment) => { + private _relativizeAttachment = (attachment: Attachment) => { const source = path.relative(this._config.resultsDir, attachment.source); if (source.startsWith('..')) { return attachment; @@ -472,6 +500,10 @@ export class JestAllure2Reporter extends JestMetadataReporter { source, }; }; + + private readonly _registerMetadata = (event: { metadata: Metadata }) => { + this._newMetadata.push(event.metadata); + }; } type AllurePayload = { diff --git a/src/runtime-plugins/docblock/index.ts b/src/runtime-plugins/docblock/index.ts deleted file mode 100644 index 281797cd..00000000 --- a/src/runtime-plugins/docblock/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { DocblockContext } from 'jest-allure2-reporter'; -import { extract, parseWithComments } from 'jest-docblock'; - -import type { LineNavigator } from '../../runtime/utils'; - -import { extractJsdocAbove } from './extractJsdocAbove'; - -export type DocblockPluginContext = { - navigator: LineNavigator; - lineNumber: number; - columnNumber: number | undefined; -}; - -export function parseJsdoc(context: DocblockPluginContext): DocblockContext { - const { lineNumber, navigator } = context; - const contents = extractJsdocAbove(navigator, lineNumber); - const extracted = extract(contents); - - const { comments, pragmas } = parseWithComments(extracted); - return { comments, pragmas }; -} diff --git a/src/runtime-plugins/index.ts b/src/runtime-plugins/index.ts deleted file mode 100644 index c72749fb..00000000 --- a/src/runtime-plugins/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './docblock'; diff --git a/src/runtime/AllureRuntimeContext.ts b/src/runtime/AllureRuntimeContext.ts index 26ee6a0b..35c8c2a8 100644 --- a/src/runtime/AllureRuntimeContext.ts +++ b/src/runtime/AllureRuntimeContext.ts @@ -3,7 +3,7 @@ import type { AllureTestFileMetadata, } from 'jest-allure2-reporter'; -import { type MaybeFunction, once } from '../utils'; +import { type MaybeFunction, once, TaskQueue } from '../utils'; import { AllureMetadataProxy, AllureTestItemMetadataProxy } from '../metadata'; import type { AllureRuntimeConfig } from './AllureRuntimeConfig'; @@ -48,12 +48,15 @@ export class AllureRuntimeContext { this.getGlobalMetadata = () => new AllureMetadataProxy(config.getGlobalMetadata()); - let idle: Promise = Promise.resolve(); - this.flush = () => idle; - this.enqueueTask = (task) => { - idle = - typeof task === 'function' ? idle.then(task) : idle.then(() => task); - }; + const taskQueue = new TaskQueue({ + logError(error) { + // TODO: print Bunyamin warning + throw error; + }, + }); + + this.flush = taskQueue.flush; + this.enqueueTask = taskQueue.enqueueTask; Object.defineProperty(this.contentAttachmentHandlers, 'default', { get: () => { diff --git a/src/runtime/modules/CoreModule.ts b/src/runtime/modules/CoreModule.ts index 0dbd2d7d..1a4586f2 100644 --- a/src/runtime/modules/CoreModule.ts +++ b/src/runtime/modules/CoreModule.ts @@ -24,6 +24,28 @@ export class CoreModule { }); } + // region Universal (test, hook, step) metadata + + displayName(value: string) { + this.context.metadata.set('displayName', value); + } + + parameter(parameter: Parameter) { + this.context.metadata.push('parameters', [parameter]); + } + + status(status: Status) { + this.context.metadata.set('status', status); + } + + statusDetails(statusDetails: StatusDetails) { + this.context.metadata.set('statusDetails', statusDetails); + } + + // endregion + + // region Test-only metadata + description(value: string) { this.context.metadata.$bind(null).push('description', [value]); } @@ -32,12 +54,8 @@ export class CoreModule { this.context.metadata.$bind(null).push('descriptionHtml', [value]); } - displayName(value: string) { - this.context.metadata.set('displayName', value); - } - fullName(value: string) { - this.context.metadata.set('fullName', value); + this.context.metadata.$bind(null).set('fullName', value); } historyId(value: string) { @@ -45,22 +63,12 @@ export class CoreModule { } label(name: LabelName | string, value: string) { - this.context.metadata.push('labels', [{ name, value }]); + this.context.metadata.$bind(null).push('labels', [{ name, value }]); } link(link: Link) { - this.context.metadata.push('links', [link]); + this.context.metadata.$bind(null).push('links', [link]); } - parameter(parameter: Parameter) { - this.context.metadata.push('parameters', [parameter]); - } - - status(status: Status) { - this.context.metadata.set('status', status); - } - - statusDetails(statusDetails: StatusDetails) { - this.context.metadata.set('statusDetails', statusDetails); - } + // endregion } diff --git a/src/runtime/utils/index.ts b/src/runtime/utils/index.ts deleted file mode 100644 index cc4e1098..00000000 --- a/src/runtime/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './FileNavigatorCache'; -export * from './LineNavigator'; diff --git a/src/utils/TaskQueue.ts b/src/utils/TaskQueue.ts new file mode 100644 index 00000000..39fb720c --- /dev/null +++ b/src/utils/TaskQueue.ts @@ -0,0 +1,26 @@ +import type { MaybeFunction } from './types'; + +export interface TaskQueueConfig { + readonly logError: (error: unknown) => void; +} + +export class TaskQueue { + #idle: Promise = Promise.resolve(); + #logError: (error: unknown) => void; + + constructor(config: TaskQueueConfig) { + this.#logError = config.logError; + } + + readonly flush = () => this.#idle; + + readonly enqueueTask = (task: MaybeFunction>) => { + this.#idle = + typeof task === 'function' + ? this.#idle.then(task) + : this.#idle.then(() => task); + + this.#idle = this.#idle.catch(this.#logError); + return this.#idle; + }; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index c8aad5c4..79e795f1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -14,6 +14,7 @@ export * from './md5'; export * from './processMaybePromise'; export * from './shallowEqualArrays'; export * from './splitDocblock'; +export * from './TaskQueue'; +export * from './types'; export * from './weakMemoize'; export * from './wrapFunction'; -export * from './types'; From 5bb0d0f3746024f3196c3443b64f3d2f730f5d0c Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 15 Mar 2024 21:56:32 +0200 Subject: [PATCH 07/50] refactor: start work on docblock --- src/metadata/docblock/index.ts | 1 - .../docblock/mapping.ts | 0 src/utils/index.ts | 1 - src/utils/splitDocblock.test.ts | 33 ------------------- src/utils/splitDocblock.ts | 13 -------- 5 files changed, 48 deletions(-) delete mode 100644 src/metadata/docblock/index.ts rename src/{metadata => reporter-plugins}/docblock/mapping.ts (100%) delete mode 100644 src/utils/splitDocblock.test.ts delete mode 100644 src/utils/splitDocblock.ts diff --git a/src/metadata/docblock/index.ts b/src/metadata/docblock/index.ts deleted file mode 100644 index 69e42e1b..00000000 --- a/src/metadata/docblock/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './mapping'; diff --git a/src/metadata/docblock/mapping.ts b/src/reporter-plugins/docblock/mapping.ts similarity index 100% rename from src/metadata/docblock/mapping.ts rename to src/reporter-plugins/docblock/mapping.ts diff --git a/src/utils/index.ts b/src/utils/index.ts index 79e795f1..648ec204 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,7 +13,6 @@ export * from './once'; export * from './md5'; export * from './processMaybePromise'; export * from './shallowEqualArrays'; -export * from './splitDocblock'; export * from './TaskQueue'; export * from './types'; export * from './weakMemoize'; diff --git a/src/utils/splitDocblock.test.ts b/src/utils/splitDocblock.test.ts deleted file mode 100644 index 9fae2dc5..00000000 --- a/src/utils/splitDocblock.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* eslint-disable unicorn/prevent-abbreviations */ - -import { splitDocblock } from './splitDocblock'; - -describe('splitDocblock', () => { - it('should split a docblock from code', function testFunction() { - /** - * @severity blocker - * @issue 123 - */ - - const [docblock, code] = splitDocblock(testFunction.toString()); - const docblockLines = docblock - .split('\n') - .map((s) => s.trimStart()) - .filter(Boolean); - - expect(docblockLines).toEqual([ - '/**', - '* @severity blocker', - '* @issue 123', - '*/', - ]); - - expect(code.includes(docblock)).toBe(false); - }); - - it('should return an empty docblock if code has no such', function testFunction() { - const [docblock, code] = splitDocblock(testFunction.toString()); - expect(docblock).toBe(''); - expect(code).toBe(testFunction.toString()); - }); -}); diff --git a/src/utils/splitDocblock.ts b/src/utils/splitDocblock.ts deleted file mode 100644 index 5a9cef66..00000000 --- a/src/utils/splitDocblock.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable unicorn/prevent-abbreviations */ - -const DOCBLOCK_REGEXP = /\s*\/\*\*[\S\s]*?\*\//m; - -export function splitDocblock(rawCode: string): [string, string] { - let docblock = ''; - const code = rawCode.replace(DOCBLOCK_REGEXP, (match) => { - docblock = match.trim(); - return ''; - }); - - return [docblock, code]; -} From aa02155f0bdddce2caf341ee696d5bd1184b89a4 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 15 Mar 2024 22:11:24 +0200 Subject: [PATCH 08/50] refactor: move autoIndent --- src/reporter-plugins/utils/autoIndent.ts | 16 ++++++++++++++++ .../utils/extractTypeScriptCode.ts | 19 ++----------------- 2 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 src/reporter-plugins/utils/autoIndent.ts diff --git a/src/reporter-plugins/utils/autoIndent.ts b/src/reporter-plugins/utils/autoIndent.ts new file mode 100644 index 00000000..fbbdf7e4 --- /dev/null +++ b/src/reporter-plugins/utils/autoIndent.ts @@ -0,0 +1,16 @@ +export function autoIndent(text: string) { + const [first, ...rest] = text.split('\n'); + const indent = detectIndent(rest); + if (indent > 0) { + return [first, ...rest.map((line) => line.slice(indent))].join('\n'); + } + + return text; +} + +function detectIndent(lines: string[]) { + return lines.reduce((indent, line) => { + const size = line.length - line.trimStart().length; + return size < indent ? size : indent; + }, Number.POSITIVE_INFINITY); +} diff --git a/src/reporter-plugins/utils/extractTypeScriptCode.ts b/src/reporter-plugins/utils/extractTypeScriptCode.ts index a728ba33..34cc40f4 100644 --- a/src/reporter-plugins/utils/extractTypeScriptCode.ts +++ b/src/reporter-plugins/utils/extractTypeScriptCode.ts @@ -1,6 +1,8 @@ // eslint-disable-next-line node/no-unpublished-import import type TypeScript from 'typescript'; +import { autoIndent } from './autoIndent'; + export async function extractTypeScriptCode( ts: typeof TypeScript, ast: TypeScript.SourceFile | undefined, @@ -35,20 +37,3 @@ export async function extractTypeScriptCode( ); } } - -function autoIndent(text: string) { - const [first, ...rest] = text.split('\n'); - const indent = detectIndent(rest); - if (indent > 0) { - return [first, ...rest.map((line) => line.slice(indent))].join('\n'); - } - - return text; -} - -function detectIndent(lines: string[]) { - return lines.reduce((indent, line) => { - const size = line.length - line.trimStart().length; - return size < indent ? size : indent; - }, Number.POSITIVE_INFINITY); -} From 83a81bd7c9ce1d8402f67ff709854de706a7e47e Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 16 Mar 2024 10:50:44 +0200 Subject: [PATCH 09/50] refactor: simplify plugins system --- index.d.ts | 156 ++++++------ package.json | 1 + src/environment/listener.ts | 5 +- src/options/default-options/plugins.ts | 3 +- src/options/default-options/testCase.ts | 4 +- src/options/default-options/testFile.ts | 4 +- .../utils/aggregateLabelCustomizers.ts | 13 +- src/options/utils/composeExtractors.ts | 2 +- src/reporter-plugins/augs.d.ts | 18 +- .../ci/BuildkiteInfoProvider.test.ts | 38 +++ .../ci/BuildkiteInfoProvider.ts | 36 +++ .../ci/ExecutorInfoProvider.ts | 6 + .../ci/GitHubInfoProvider.test.ts | 140 +++++++++++ src/reporter-plugins/ci/GitHubInfoProvider.ts | 103 ++++++++ .../ci/LocalInfoProvider.test.ts | 13 + src/reporter-plugins/ci/LocalInfoProvider.ts | 17 ++ src/reporter-plugins/ci/index.ts | 38 +++ src/reporter-plugins/ci/utils/getOSDetails.ts | 5 + src/reporter-plugins/ci/utils/index.ts | 1 + .../docblock/docblockPlugin.ts | 17 +- .../docblock/extractJsdocAbove.test.ts | 2 +- .../docblock/extractJsdocAbove.ts | 2 +- src/reporter-plugins/docblock/mapping.ts | 6 +- src/reporter-plugins/docblock/parseJsdoc.ts | 6 +- src/reporter-plugins/fallback.ts | 33 --- src/reporter-plugins/github.ts | 58 ----- src/reporter-plugins/index.ts | 3 +- src/reporter-plugins/manifest.ts | 30 --- src/reporter-plugins/manifest/index.ts | 25 ++ .../manifest/manifest.test.ts | 32 +++ src/reporter-plugins/manifest/manifest.ts | 71 ++++++ src/reporter-plugins/source-code/index.ts | 7 + src/reporter-plugins/source-code/prettier.ts | 44 ---- .../source-code/sourceCodePlugin.ts | 82 +++---- .../utils/FileNavigatorCache.ts | 0 .../utils/LineNavigator.test.ts | 0 .../{ => source-code}/utils/LineNavigator.ts | 0 .../utils/extractTypeScriptCode.ts | 2 +- .../utils/extractTypescriptAST.ts} | 2 +- .../utils/importTypeScript.ts | 0 .../{ => source-code}/utils/index.ts | 2 +- src/reporter/JestAllure2Reporter.ts | 229 +++++++----------- src/reporter/{ => fallback}/ThreadService.ts | 0 src/reporter/fallback/index.ts | 41 ++++ .../utils/autoIndent.ts | 0 src/utils/flatMapAsync.test.ts | 30 +++ src/utils/flatMapAsync.ts | 7 + src/utils/index.ts | 1 + src/utils/weakMemoize.test.ts | 10 +- src/utils/weakMemoize.ts | 19 +- 50 files changed, 873 insertions(+), 491 deletions(-) create mode 100644 src/reporter-plugins/ci/BuildkiteInfoProvider.test.ts create mode 100644 src/reporter-plugins/ci/BuildkiteInfoProvider.ts create mode 100644 src/reporter-plugins/ci/ExecutorInfoProvider.ts create mode 100644 src/reporter-plugins/ci/GitHubInfoProvider.test.ts create mode 100644 src/reporter-plugins/ci/GitHubInfoProvider.ts create mode 100644 src/reporter-plugins/ci/LocalInfoProvider.test.ts create mode 100644 src/reporter-plugins/ci/LocalInfoProvider.ts create mode 100644 src/reporter-plugins/ci/index.ts create mode 100644 src/reporter-plugins/ci/utils/getOSDetails.ts create mode 100644 src/reporter-plugins/ci/utils/index.ts delete mode 100644 src/reporter-plugins/fallback.ts delete mode 100644 src/reporter-plugins/github.ts delete mode 100644 src/reporter-plugins/manifest.ts create mode 100644 src/reporter-plugins/manifest/index.ts create mode 100644 src/reporter-plugins/manifest/manifest.test.ts create mode 100644 src/reporter-plugins/manifest/manifest.ts delete mode 100644 src/reporter-plugins/source-code/prettier.ts rename src/reporter-plugins/{ => source-code}/utils/FileNavigatorCache.ts (100%) rename src/reporter-plugins/{ => source-code}/utils/LineNavigator.test.ts (100%) rename src/reporter-plugins/{ => source-code}/utils/LineNavigator.ts (100%) rename src/reporter-plugins/{ => source-code}/utils/extractTypeScriptCode.ts (96%) rename src/reporter-plugins/{utils/ensureTypeScriptAST.ts => source-code/utils/extractTypescriptAST.ts} (95%) rename src/reporter-plugins/{ => source-code}/utils/importTypeScript.ts (100%) rename src/reporter-plugins/{ => source-code}/utils/index.ts (78%) rename src/reporter/{ => fallback}/ThreadService.ts (100%) create mode 100644 src/reporter/fallback/index.ts rename src/{reporter-plugins => }/utils/autoIndent.ts (100%) create mode 100644 src/utils/flatMapAsync.test.ts create mode 100644 src/utils/flatMapAsync.ts diff --git a/index.d.ts b/index.d.ts index 2c324bd5..c691d060 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,6 +4,8 @@ import type { AggregatedResult, Config, Test, TestCaseResult, TestContext, TestR import JestMetadataReporter from 'jest-metadata/reporter'; declare module 'jest-allure2-reporter' { + // region Config + /** * Configuration options for the `jest-allure2-reporter` package. * These options are used in your Jest config. @@ -141,6 +143,44 @@ declare module 'jest-allure2-reporter' { /** @see {@link AttachmentsOptions#contentHandler} */ export type BuiltinContentAttachmentHandler = 'write'; + // endregion + + // region Allure Test Data + + export interface AllureTestCaseResult { + hidden: boolean; + historyId: string; + name: string; + fullName: string; + start: number; + stop: number; + description: string; + descriptionHtml: string; + stage: Stage; + status: Status; + statusDetails: StatusDetails; + labels: Label[]; + links: Link[]; + attachments: Attachment[]; + parameters: Parameter[]; + } + + export interface AllureTestStepResult { + name: string; + start: number; + stop: number; + stage: Stage; + status: Status; + statusDetails: StatusDetails; + steps: AllureTestStepResult[]; + attachments: Attachment[]; + parameters: Parameter[]; + } + + // endregion + + // region Customizers + /** * Global customizations for how test cases are reported */ @@ -399,7 +439,7 @@ declare module 'jest-allure2-reporter' { T = unknown, C extends ExtractorContext = ExtractorContext, R = T, - > = (context: Readonly) => R | undefined; + > = (context: Readonly) => R | undefined | Promise; export type GlobalExtractor = Extractor< T, @@ -458,13 +498,26 @@ declare module 'jest-allure2-reporter' { testStepMetadata: AllureTestStepMetadata; } - export interface AllureTestItemSourceLocation { - fileName?: string; - lineNumber?: number; - columnNumber?: number; + export interface ExtractorHelpers extends ExtractorHelpersAugmentation { + extractSourceCode(metadata: AllureTestItemMetadata): Promise; + extractSourceCodeWithSteps(metadata: AllureTestItemMetadata): Promise; + sourceCode2Markdown(sourceCode: Partial | undefined): string; + markdown2html(markdown: string): Promise; } - export type AllureTestStepPath = string[]; + export type ExtractorHelperSourceCode = { + fileName: string; + code: string; + language: string; + }; + + // endregion + + // region Custom Metadata + + export interface AllureGlobalMetadata { + config: Pick; + } export interface AllureTestItemMetadata { /** @@ -480,7 +533,7 @@ declare module 'jest-allure2-reporter' { /** * Parsed docblock: comments and pragmas. */ - docblock?: DocblockExtractorResult; + docblock?: AllureTestItemDocblock; /** * Title of the test case or test step. */ @@ -527,6 +580,8 @@ declare module 'jest-allure2-reporter' { transformedCode?: string; } + export type AllureTestStepPath = string[]; + export type AllureNestedTestStepMetadata = Omit; /** @inheritDoc */ @@ -557,28 +612,20 @@ declare module 'jest-allure2-reporter' { /** @inheritDoc */ export interface AllureTestFileMetadata extends AllureTestCaseMetadata {} - export interface AllureGlobalMetadata { - config: Pick; + export interface AllureTestItemSourceLocation { + fileName?: string; + lineNumber?: number; + columnNumber?: number; } - export interface DocblockExtractorResult { + export interface AllureTestItemDocblock { comments: string; pragmas: Record; } - export type CodeExtractorResult = { - ast?: unknown; - code: string; - language: string; - }; + // endregion - export interface ExtractorHelpers extends ExtractorHelpersAugmentation { - extractSourceCode(metadata: AllureTestItemMetadata): CodeExtractorResult | undefined; - extractSourceCodeAsync(metadata: AllureTestItemMetadata): Promise; - extractSourceCodeWithSteps(metadata: AllureTestItemMetadata): CodeExtractorResult[]; - sourceCode2Markdown(sourceCode: Partial | undefined): string; - markdown2html(markdown: string): Promise; - } + // region Plugins export interface ExtractorHelpersAugmentation { // This may be extended by plugins @@ -625,78 +672,21 @@ declare module 'jest-allure2-reporter' { helpers?(helpers: Partial): void | Promise; /** Allows to modify the raw metadata before it's processed by the reporter. [UNSTABLE!] */ - rawMetadata?(context: PluginHookContexts['rawMetadata']): void | Promise; - - /** Attach to the reporter lifecycle hook `onRunStart`. */ - onRunStart?(context: PluginHookContexts['onRunStart']): void | Promise; - - /** Attach to the reporter lifecycle hook `onTestFileStart`. */ - onTestFileStart?(context: PluginHookContexts['onTestFileStart']): void | Promise; - - /** Attach to the reporter lifecycle hook `onTestCaseResult`. */ - onTestCaseResult?(context: PluginHookContexts['onTestCaseResult']): void | Promise; - - /** Attach to the reporter lifecycle hook `onTestFileResult`. */ - onTestFileResult?(context: PluginHookContexts['onTestFileResult']): void | Promise; - - /** Attach to the reporter lifecycle hook `onRunComplete`. */ - onRunComplete?(context: PluginHookContexts['onRunComplete']): void | Promise; - - /** Method to extend global context. */ - globalContext?(context: PluginHookContexts['globalContext']): void | Promise; - - /** Method to extend test file context. */ - testFileContext?(context: PluginHookContexts['testFileContext']): void | Promise; - - /** Method to extend test entry context. */ - testCaseContext?(context: PluginHookContexts['testCaseContext']): void | Promise; - - /** Method to extend test step context. */ - testStepContext?(context: PluginHookContexts['testStepContext']): void | Promise; + postProcessMetadata?(context: PluginHookContexts['postProcessMetadata']): void | Promise; } export type PluginHookContexts = { helpers: Partial; - rawMetadata: { + postProcessMetadata: { $: Readonly; metadata: AllureTestItemMetadata; }; - onRunStart: { - aggregatedResult: AggregatedResult; - reporterConfig: ReporterConfig; - }; - onTestFileStart: { - reporterConfig: ReporterConfig; - test: Test; - testFileMetadata: AllureTestFileMetadata; - }; - onTestCaseResult: { - reporterConfig: ReporterConfig; - test: Test; - testFileMetadata: AllureTestFileMetadata; - testCaseMetadata: AllureTestCaseMetadata; - testCaseResult: TestCaseResult; - }; - onTestFileResult: { - aggregatedResult: AggregatedResult; - reporterConfig: ReporterConfig; - test: Test; - testResult: TestResult; - testFileMetadata: AllureTestFileMetadata; - }; - onRunComplete: { - reporterConfig: ReporterConfig; - testContexts: Set; - results: AggregatedResult; - }; - globalContext: GlobalExtractorContext; - testFileContext: TestFileExtractorContext; - testCaseContext: TestCaseExtractorContext; - testStepContext: TestStepExtractorContext; }; export type PluginHookName = keyof PluginHookContexts; + // endregion + //region Allure types export interface Attachment { diff --git a/package.json b/package.json index 00378f19..fcdb8b76 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "dependencies": { "@noomorph/allure-js-commons": "^2.3.0", "ci-info": "^3.8.0", + "import-from": "^4.0.0", "jest-metadata": "^1.4.1", "lodash.snakecase": "^4.1.1", "node-fetch": "^2.6.7", diff --git a/src/environment/listener.ts b/src/environment/listener.ts index 5458e4e0..a37a6e17 100644 --- a/src/environment/listener.ts +++ b/src/environment/listener.ts @@ -10,6 +10,7 @@ import * as StackTrace from 'stacktrace-js'; import * as api from '../api'; import realm from '../realms'; import { + autoIndent, getStatusDetails, isJestAssertionError, isLibraryPath, @@ -116,7 +117,9 @@ function addSourceCode({ event }: TestEnvironmentCircusEvent) { } if (code) { - realm.runtimeContext.getCurrentMetadata().set('transformedCode', code); + realm.runtimeContext + .getCurrentMetadata() + .set('transformedCode', autoIndent(code)); } } diff --git a/src/options/default-options/plugins.ts b/src/options/default-options/plugins.ts index f17066c8..a86ce822 100644 --- a/src/options/default-options/plugins.ts +++ b/src/options/default-options/plugins.ts @@ -4,8 +4,7 @@ import * as plugins from '../../reporter-plugins'; export async function defaultPlugins(context: PluginContext) { return await Promise.all([ - plugins.fallback({}, context), - plugins.github({}, context), + plugins.ci({}, context), plugins.manifest({}, context), plugins.remark({}, context), plugins.sourceCode({}, context), diff --git a/src/options/default-options/testCase.ts b/src/options/default-options/testCase.ts index f31dc0bf..4efc175a 100644 --- a/src/options/default-options/testCase.ts +++ b/src/options/default-options/testCase.ts @@ -30,9 +30,9 @@ export const testCase: ResolvedTestCaseCustomizer = { testCaseMetadata.displayName ?? testCase.title, fullName: ({ testCase, testCaseMetadata }) => testCaseMetadata.fullName ?? testCase.fullName, - description: ({ $, testCaseMetadata }) => { + description: async ({ $, testCaseMetadata }) => { const text = testCaseMetadata.description?.join('\n\n') ?? ''; - const codes = $.extractSourceCodeWithSteps(testCaseMetadata); + const codes = await $.extractSourceCodeWithSteps(testCaseMetadata); const snippets = codes.map($.sourceCode2Markdown); return [text, ...snippets].filter(Boolean).join('\n\n'); }, diff --git a/src/options/default-options/testFile.ts b/src/options/default-options/testFile.ts index 4136b420..cf1b2157 100644 --- a/src/options/default-options/testFile.ts +++ b/src/options/default-options/testFile.ts @@ -26,9 +26,9 @@ export const testFile: ResolvedTestFileCustomizer = { name: ({ filePath }) => filePath.join(path.sep), fullName: ({ globalConfig, testFile }) => path.relative(globalConfig.rootDir, testFile.testFilePath), - description: ({ $, testFileMetadata }) => { + description: async ({ $, testFileMetadata }) => { const text = testFileMetadata.description?.join('\n') ?? ''; - const code = $.extractSourceCode(testFileMetadata); + const code = await $.extractSourceCode(testFileMetadata); return [text, $.sourceCode2Markdown(code)].filter(Boolean).join('\n\n'); }, descriptionHtml: ({ testFileMetadata }) => diff --git a/src/options/utils/aggregateLabelCustomizers.ts b/src/options/utils/aggregateLabelCustomizers.ts index f45ed99c..8ab9181f 100644 --- a/src/options/utils/aggregateLabelCustomizers.ts +++ b/src/options/utils/aggregateLabelCustomizers.ts @@ -7,6 +7,8 @@ import type { } from 'jest-allure2-reporter'; import type { Label } from 'jest-allure2-reporter'; +import { flatMapAsync } from '../../utils/flatMapAsync'; + import { asExtractor } from './asExtractor'; type Customizer = TestFileCustomizer | TestCaseCustomizer; @@ -30,7 +32,7 @@ export function aggregateLabelCustomizers( const names = Object.keys(extractors); - return (context: ExtractorContext): Label[] | undefined => { + const combined: Extractor = async (context) => { const other: Label[] = []; const found = names.reduce( (found, key) => { @@ -52,19 +54,22 @@ export function aggregateLabelCustomizers( const result = [ ...other, - ...names.flatMap((name) => { + ...(await flatMapAsync(names, async (name) => { const extractor = extractors[name]; const aContext: ExtractorContext = { ...context, value: asArray(found[name]), }; - const value = asArray(extractor(aContext)); + const extracted = await extractor(aContext); + const value = asArray(extracted); return value ? value.map((value) => ({ name, value }) as Label) : []; - }), + })), ]; return result; }; + + return combined; } function asArray( diff --git a/src/options/utils/composeExtractors.ts b/src/options/utils/composeExtractors.ts index ddaea468..044812c8 100644 --- a/src/options/utils/composeExtractors.ts +++ b/src/options/utils/composeExtractors.ts @@ -4,5 +4,5 @@ export function composeExtractors>( a: Extractor | undefined, b: Extractor, ): Extractor { - return a ? (context) => a({ ...context, value: b(context) }) : b; + return a ? async (context) => a({ ...context, value: await b(context) }) : b; } diff --git a/src/reporter-plugins/augs.d.ts b/src/reporter-plugins/augs.d.ts index 01f73247..5227d4df 100644 --- a/src/reporter-plugins/augs.d.ts +++ b/src/reporter-plugins/augs.d.ts @@ -1,12 +1,18 @@ declare module 'jest-allure2-reporter' { - interface GlobalExtractorContextAugmentation { + import type { ExecutorInfo } from '@noomorph/allure-js-commons'; + + import type { ManifestHelper } from './manifest'; + + interface ExtractorHelpersAugmentation { /** * The contents of the `package.json` file if it exists. */ - manifest?: { - name: string; - version: string; - [key: string]: unknown; - } | null; + manifest: ManifestHelper; + + /** + * Information about the current executor + */ + getExecutorInfo(): Promise; + getExecutorInfo(includeLocal: true): Promise; } } diff --git a/src/reporter-plugins/ci/BuildkiteInfoProvider.test.ts b/src/reporter-plugins/ci/BuildkiteInfoProvider.test.ts new file mode 100644 index 00000000..27b27918 --- /dev/null +++ b/src/reporter-plugins/ci/BuildkiteInfoProvider.test.ts @@ -0,0 +1,38 @@ +import { BuildkiteInfoProvider } from './BuildkiteInfoProvider'; + +describe('BuildkiteInfoProvider', () => { + const environment = { + BUILDKITE: 'true', + BUILDKITE_AGENT_NAME: 'Buildkite Agent', + BUILDKITE_BUILD_URL: 'https://buildkite.com/owner/repo/builds/123', + BUILDKITE_BUILD_NUMBER: '123', + }; + + it('should return Buildkite info when enabled', async () => { + const provider = new BuildkiteInfoProvider(environment); + expect(provider.enabled).toBe(true); + + const info = await provider.getExecutorInfo(); + expect(info).toEqual({ + name: expect.stringContaining('Buildkite Agent'), + type: 'buildkite', + buildUrl: 'https://buildkite.com/owner/repo/builds/123', + buildOrder: 123, + buildName: '#123', + }); + }); + + it('should return default info when not enabled', async () => { + const provider = new BuildkiteInfoProvider({}); + expect(provider.enabled).toBe(false); + + const info = await provider.getExecutorInfo(); + expect(info).toEqual({ + name: expect.stringContaining('Buildkite'), + type: 'buildkite', + buildUrl: undefined, + buildOrder: Number.NaN, + buildName: '#undefined', + }); + }); +}); diff --git a/src/reporter-plugins/ci/BuildkiteInfoProvider.ts b/src/reporter-plugins/ci/BuildkiteInfoProvider.ts new file mode 100644 index 00000000..3bca9b41 --- /dev/null +++ b/src/reporter-plugins/ci/BuildkiteInfoProvider.ts @@ -0,0 +1,36 @@ +import type { ExecutorInfo } from 'jest-allure2-reporter'; + +import type { ExecutorInfoProvider } from './ExecutorInfoProvider'; +import { getOSDetails } from './utils'; + +export interface BuildkiteEnvironment { + BUILDKITE?: string; + BUILDKITE_AGENT_NAME?: string; + BUILDKITE_BUILD_URL?: string; + BUILDKITE_BUILD_NUMBER?: string; +} + +export class BuildkiteInfoProvider implements ExecutorInfoProvider { + constructor(private readonly environment: Partial) {} + + get enabled() { + return !!this.environment.BUILDKITE; + } + + async getExecutorInfo(): Promise { + const { + BUILDKITE_AGENT_NAME, + BUILDKITE_BUILD_URL, + BUILDKITE_BUILD_NUMBER, + } = this.environment; + const agentName = BUILDKITE_AGENT_NAME || 'Buildkite'; + + return { + name: `${agentName} (${getOSDetails()})`, + type: 'buildkite', + buildUrl: BUILDKITE_BUILD_URL, + buildOrder: Number(BUILDKITE_BUILD_NUMBER), + buildName: `#${BUILDKITE_BUILD_NUMBER}`, + }; + } +} diff --git a/src/reporter-plugins/ci/ExecutorInfoProvider.ts b/src/reporter-plugins/ci/ExecutorInfoProvider.ts new file mode 100644 index 00000000..8abfe6ae --- /dev/null +++ b/src/reporter-plugins/ci/ExecutorInfoProvider.ts @@ -0,0 +1,6 @@ +import type { ExecutorInfo } from 'jest-allure2-reporter'; + +export interface ExecutorInfoProvider { + readonly enabled: boolean; + getExecutorInfo(): Promise; +} diff --git a/src/reporter-plugins/ci/GitHubInfoProvider.test.ts b/src/reporter-plugins/ci/GitHubInfoProvider.test.ts new file mode 100644 index 00000000..5e44b1d5 --- /dev/null +++ b/src/reporter-plugins/ci/GitHubInfoProvider.test.ts @@ -0,0 +1,140 @@ +/* eslint-disable @typescript-eslint/consistent-type-imports */ +jest.mock('node-fetch'); + +import { + type GitHubEnvironment, + GitHubInfoProvider, +} from './GitHubInfoProvider'; + +describe('GitHubInfoProvider', () => { + const apiUrl = + 'https://api.github.com/repos/owner/repo/actions/runs/123/attempts/1/jobs'; + + let fetch: jest.MockedFunction; + let environment: Partial; + + beforeEach(() => { + environment = { + GITHUB_ACTIONS: 'true', + GITHUB_JOB: 'test_job', + GITHUB_REPOSITORY: 'owner/repo', + GITHUB_RUN_ATTEMPT: '1', + GITHUB_RUN_ID: '123', + GITHUB_RUN_NUMBER: '10', + GITHUB_SERVER_URL: 'https://github.com', + GITHUB_TOKEN: 'my_token', + RUNNER_NAME: 'GitHub Runner', + }; + }); + + beforeEach(() => { + fetch = jest.requireMock('node-fetch'); + fetch.mockClear(); + }); + + const mockSuccessResponse = (jobs: any[]) => ({ + ok: true, + json: jest.fn().mockResolvedValue({ jobs }), + }); + + it('should be disabled if GITHUB_ACTIONS env var is undefined', () => { + const provider = new GitHubInfoProvider({ + ...environment, + GITHUB_ACTIONS: '', + }); + + expect(provider.enabled).toBe(false); + }); + + it('should be enabled if GITHUB_ACTIONS env var is defined', () => { + const provider = new GitHubInfoProvider({ + ...environment, + GITHUB_ACTIONS: '', + }); + + expect(provider.enabled).toBe(false); + }); + + it('should return executor info when API request is successful', async () => { + const jobInfo = { + id: 'job_id', + name: 'Test Job', + html_url: 'https://github.com/owner/repo/actions/runs/123#test_job', + }; + + fetch.mockResolvedValueOnce(mockSuccessResponse([jobInfo]) as any); + + const provider = new GitHubInfoProvider(environment); + const info = await provider.getExecutorInfo(); + + expect(info).toEqual({ + name: expect.stringContaining('GitHub Runner'), + type: 'github', + buildUrl: jobInfo.html_url, + buildOrder: 101, + buildName: '123#test_job', + }); + }); + + it('should return executor info with fallback URL when job is not found', async () => { + fetch.mockResolvedValueOnce(mockSuccessResponse([]) as any); + + const provider = new GitHubInfoProvider(environment); + const info = await provider.getExecutorInfo(); + + expect(info).toEqual({ + name: expect.stringContaining('GitHub Runner'), + type: 'github', + buildUrl: 'https://github.com/owner/repo/actions/runs/123', + buildOrder: 101, + buildName: '123#test_job', + }); + }); + + it('should return executor info with fallback URL when API request fails', async () => { + const mockError = new Error('API error'); + fetch.mockRejectedValueOnce(mockError); + jest.spyOn(console, 'error').mockReturnValueOnce(); + + const provider = new GitHubInfoProvider(environment); + const info = await provider.getExecutorInfo(); + + expect(info).toEqual({ + name: expect.stringContaining('GitHub Runner'), + type: 'github', + buildUrl: 'https://github.com/owner/repo/actions/runs/123', + buildOrder: 101, + buildName: '123#test_job', + }); + }); + + it('should return executor info with fallback URL when token is not provided', async () => { + delete environment.GITHUB_TOKEN; + + const provider = new GitHubInfoProvider(environment); + const info = await provider.getExecutorInfo(); + + expect(info).toEqual({ + name: expect.stringContaining('GitHub Runner'), + type: 'github', + buildUrl: 'https://github.com/owner/repo/actions/runs/123', + buildOrder: 101, + buildName: '123#test_job', + }); + expect(fetch).not.toHaveBeenCalled(); + }); + + it('should include authorization header when githubToken is provided', async () => { + fetch.mockResolvedValueOnce(mockSuccessResponse([]) as any); + + const provider = new GitHubInfoProvider(environment); + await provider.getExecutorInfo(); + + expect(fetch).toHaveBeenCalledWith(apiUrl, { + headers: { + Accept: 'application/vnd.github.v3+json', + Authorization: 'token my_token', + }, + }); + }); +}); diff --git a/src/reporter-plugins/ci/GitHubInfoProvider.ts b/src/reporter-plugins/ci/GitHubInfoProvider.ts new file mode 100644 index 00000000..9849f827 --- /dev/null +++ b/src/reporter-plugins/ci/GitHubInfoProvider.ts @@ -0,0 +1,103 @@ +import fetch from 'node-fetch'; +import snakeCase from 'lodash.snakecase'; +import type { ExecutorInfo } from 'jest-allure2-reporter'; + +import type { ExecutorInfoProvider } from './ExecutorInfoProvider'; +import { getOSDetails } from './utils'; + +export interface GitHubEnvironment { + GITHUB_ACTIONS: string; + GITHUB_JOB: string; + GITHUB_REPOSITORY: string; + GITHUB_RUN_ATTEMPT: string; + GITHUB_RUN_ID: string; + GITHUB_RUN_NUMBER: string; + GITHUB_SERVER_URL: string; + GITHUB_TOKEN: string; + RUNNER_NAME?: string; +} + +type Job = { + id: string; + name: string; + html_url: string; +}; + +export class GitHubInfoProvider implements ExecutorInfoProvider { + constructor(private readonly environment: Partial) {} + + get enabled() { + return !!this.environment.GITHUB_ACTIONS; + } + + async getExecutorInfo(): Promise { + const job = await this._fetchJob(); + + const { + GITHUB_RUN_ATTEMPT, + GITHUB_RUN_ID, + GITHUB_RUN_NUMBER, + GITHUB_JOB, + RUNNER_NAME, + GITHUB_SERVER_URL, + GITHUB_REPOSITORY, + } = this.environment; + + const runnerName = RUNNER_NAME || 'GitHub Actions'; + const buildUrl = job + ? job.html_url + : `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; + + return { + name: `${runnerName} (${getOSDetails()})`, + type: 'github', + buildUrl, + buildOrder: Number(GITHUB_RUN_NUMBER) * 10 + Number(GITHUB_RUN_ATTEMPT), + buildName: `${GITHUB_RUN_ID}#${GITHUB_JOB}`, + }; + } + + private async _fetchJob(): Promise { + const url = this._buildApiUrl(); + + try { + const data = await this._fetchJobs(url); + return this._findJob(data.jobs); + } catch (error: unknown) { + console.error(`Failed to fetch job ID from: ${url}\nReason:`, error); + return; + } + } + + private async _fetchJobs(url: string) { + if (!this.environment.GITHUB_TOKEN) { + return { jobs: [] }; + } + + const headers = { + Accept: 'application/vnd.github.v3+json', + Authorization: `token ${this.environment.GITHUB_TOKEN}`, + }; + + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } + + return await response.json(); + } + + private _buildApiUrl(): string { + const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT } = + this.environment; + return `https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}/jobs`; + } + + private _findJob(jobs: Job[]): Job | undefined { + const { GITHUB_JOB } = this.environment; + + return jobs.length === 1 + ? jobs[0] + : jobs.find((job) => snakeCase(job.name) === GITHUB_JOB); + } +} diff --git a/src/reporter-plugins/ci/LocalInfoProvider.test.ts b/src/reporter-plugins/ci/LocalInfoProvider.test.ts new file mode 100644 index 00000000..a79aa950 --- /dev/null +++ b/src/reporter-plugins/ci/LocalInfoProvider.test.ts @@ -0,0 +1,13 @@ +import { LocalInfoProvider } from './LocalInfoProvider'; + +describe('LocalInfoProvider', () => { + it('should return local info', async () => { + const provider = new LocalInfoProvider(true); + const info = await provider.getExecutorInfo(); + + expect(info).toEqual({ + name: expect.stringMatching(/.* \(.*\/.*\)/), + type: expect.any(String), + }); + }); +}); diff --git a/src/reporter-plugins/ci/LocalInfoProvider.ts b/src/reporter-plugins/ci/LocalInfoProvider.ts new file mode 100644 index 00000000..a5205e2a --- /dev/null +++ b/src/reporter-plugins/ci/LocalInfoProvider.ts @@ -0,0 +1,17 @@ +import os from 'node:os'; + +import type { ExecutorInfo } from 'jest-allure2-reporter'; + +import type { ExecutorInfoProvider } from './ExecutorInfoProvider'; +import { getOSDetails } from './utils'; + +export class LocalInfoProvider implements ExecutorInfoProvider { + constructor(public readonly enabled: boolean) {} + + async getExecutorInfo(): Promise { + return { + name: `${os.hostname()} (${getOSDetails()})`, + type: `${os.platform()}-${os.arch()}`, + }; + } +} diff --git a/src/reporter-plugins/ci/index.ts b/src/reporter-plugins/ci/index.ts new file mode 100644 index 00000000..e87890dd --- /dev/null +++ b/src/reporter-plugins/ci/index.ts @@ -0,0 +1,38 @@ +/// + +import type { + ExecutorInfo, + Plugin, + PluginConstructor, +} from 'jest-allure2-reporter'; + +import { GitHubInfoProvider } from './GitHubInfoProvider'; +import { BuildkiteInfoProvider } from './BuildkiteInfoProvider'; +import { LocalInfoProvider } from './LocalInfoProvider'; +import type { ExecutorInfoProvider } from './ExecutorInfoProvider'; + +export const ciPlugin: PluginConstructor = () => { + const environment = process.env as Record; + const providers: ExecutorInfoProvider[] = [ + new BuildkiteInfoProvider(environment), + new GitHubInfoProvider(environment), + ]; + + const isEnabled = (provider: ExecutorInfoProvider) => provider.enabled; + + async function getExecutorInfo( + includeLocal: boolean, + ): Promise { + const local: ExecutorInfoProvider = new LocalInfoProvider(includeLocal); + return [...providers, local].find(isEnabled)?.getExecutorInfo(); + } + + const plugin: Plugin = { + name: 'jest-allure2-reporter/plugins/ci', + async helpers($) { + Object.assign($, { getExecutorInfo }); + }, + }; + + return plugin; +}; diff --git a/src/reporter-plugins/ci/utils/getOSDetails.ts b/src/reporter-plugins/ci/utils/getOSDetails.ts new file mode 100644 index 00000000..7a7397a4 --- /dev/null +++ b/src/reporter-plugins/ci/utils/getOSDetails.ts @@ -0,0 +1,5 @@ +import os from 'node:os'; + +export function getOSDetails() { + return `${os.type()} ${os.release()}/${os.arch()}`; +} diff --git a/src/reporter-plugins/ci/utils/index.ts b/src/reporter-plugins/ci/utils/index.ts new file mode 100644 index 00000000..a6ba1076 --- /dev/null +++ b/src/reporter-plugins/ci/utils/index.ts @@ -0,0 +1 @@ +export * from './getOSDetails'; diff --git a/src/reporter-plugins/docblock/docblockPlugin.ts b/src/reporter-plugins/docblock/docblockPlugin.ts index feb2fea2..41e5a152 100644 --- a/src/reporter-plugins/docblock/docblockPlugin.ts +++ b/src/reporter-plugins/docblock/docblockPlugin.ts @@ -1,13 +1,13 @@ // eslint-disable-next-line node/no-unpublished-import -import type TypeScript from 'typescript'; import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; import { extract, parseWithComments } from 'jest-docblock'; import { + extractTypescriptAST, extractTypeScriptCode, FileNavigatorCache, importTypeScript, -} from '../utils'; +} from '../source-code'; import { extractJsdocAbove } from './extractJsdocAbove'; @@ -15,16 +15,21 @@ export const docblockPlugin: PluginConstructor = () => { const plugin: Plugin = { name: 'jest-allure2-reporter/plugins/docblock', - async rawMetadata({ $, metadata }) { + async postProcessMetadata({ metadata }) { const { fileName, lineNumber, columnNumber } = metadata.sourceLocation || {}; - const code = await $.extractSourceCodeAsync(metadata); let extracted: string | undefined; const ts = await importTypeScript(); - if (ts && code?.ast && lineNumber != null && columnNumber != null) { + if ( + ts && + fileName != null && + lineNumber != null && + columnNumber != null + ) { + const ast = await extractTypescriptAST(ts, fileName); const fullCode = await extractTypeScriptCode( ts, - code.ast as TypeScript.SourceFile, + ast, [lineNumber, columnNumber], true, ); diff --git a/src/reporter-plugins/docblock/extractJsdocAbove.test.ts b/src/reporter-plugins/docblock/extractJsdocAbove.test.ts index f1c40ffc..0f101c86 100644 --- a/src/reporter-plugins/docblock/extractJsdocAbove.test.ts +++ b/src/reporter-plugins/docblock/extractJsdocAbove.test.ts @@ -1,4 +1,4 @@ -import { LineNavigator } from '../utils'; +import { LineNavigator } from '../source-code'; import { extractJsdocAbove as extractJsDocument_ } from './extractJsdocAbove'; diff --git a/src/reporter-plugins/docblock/extractJsdocAbove.ts b/src/reporter-plugins/docblock/extractJsdocAbove.ts index 617f0a51..08c41ac6 100644 --- a/src/reporter-plugins/docblock/extractJsdocAbove.ts +++ b/src/reporter-plugins/docblock/extractJsdocAbove.ts @@ -1,4 +1,4 @@ -import type { LineNavigator } from '../utils'; +import type { LineNavigator } from '../source-code'; export function extractJsdocAbove( navigator: LineNavigator, diff --git a/src/reporter-plugins/docblock/mapping.ts b/src/reporter-plugins/docblock/mapping.ts index 0125b093..6c4cf5d4 100644 --- a/src/reporter-plugins/docblock/mapping.ts +++ b/src/reporter-plugins/docblock/mapping.ts @@ -1,7 +1,7 @@ import type { AllureTestStepMetadata, AllureTestCaseMetadata, - DocblockExtractorResult, + AllureTestItemDocblock, Label, LabelName, Link, @@ -10,7 +10,7 @@ import type { export function mapTestStepDocblock({ comments, -}: DocblockExtractorResult): AllureTestStepMetadata { +}: AllureTestItemDocblock): AllureTestStepMetadata { const metadata: AllureTestStepMetadata = {}; if (comments) { metadata.displayName = comments; @@ -20,7 +20,7 @@ export function mapTestStepDocblock({ } export function mapTestCaseDocblock( - context: DocblockExtractorResult, + context: AllureTestItemDocblock, ): AllureTestCaseMetadata { const metadata: AllureTestCaseMetadata = {}; const { comments, pragmas = {} } = context; diff --git a/src/reporter-plugins/docblock/parseJsdoc.ts b/src/reporter-plugins/docblock/parseJsdoc.ts index 645dccf9..0b0f0a22 100644 --- a/src/reporter-plugins/docblock/parseJsdoc.ts +++ b/src/reporter-plugins/docblock/parseJsdoc.ts @@ -1,14 +1,14 @@ -import type { DocblockExtractorResult } from 'jest-allure2-reporter'; +import type { AllureTestItemDocblock } from 'jest-allure2-reporter'; import { extract, parseWithComments } from 'jest-docblock'; -import type { LineNavigator } from '../utils'; +import type { LineNavigator } from '../source-code'; import { extractJsdocAbove } from './extractJsdocAbove'; export function parseJsdoc( navigator: LineNavigator, lineNumber: number, -): DocblockExtractorResult { +): AllureTestItemDocblock { const contents = extractJsdocAbove(navigator, lineNumber); const extracted = extract(contents); diff --git a/src/reporter-plugins/fallback.ts b/src/reporter-plugins/fallback.ts deleted file mode 100644 index 357d8c06..00000000 --- a/src/reporter-plugins/fallback.ts +++ /dev/null @@ -1,33 +0,0 @@ -/// - -import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; - -import { ThreadService } from '../reporter/ThreadService'; - -export const fallbackPlugin: PluginConstructor = () => { - const threadService = new ThreadService(); - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/fallback', - - onTestFileStart({ test, testFileMetadata }) { - const threadId = threadService.allocateThread(test.path); - testFileMetadata.sourceLocation = { fileName: test.path }; - testFileMetadata.start = Date.now(); - testFileMetadata.workerId = String(1 + threadId); - }, - - onTestCaseResult({ testCaseMetadata }) { - const stop = testCaseMetadata.stop ?? Number.NaN; - if (Number.isNaN(stop)) { - testCaseMetadata.stop = Date.now(); - } - }, - - onTestFileResult({ test, testFileMetadata }) { - testFileMetadata.stop = Date.now(); - threadService.freeThread(test.path); - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/github.ts b/src/reporter-plugins/github.ts deleted file mode 100644 index 30855cce..00000000 --- a/src/reporter-plugins/github.ts +++ /dev/null @@ -1,58 +0,0 @@ -import fetch from 'node-fetch'; -import snakeCase from 'lodash.snakecase'; -import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; - -export const githubPlugin: PluginConstructor = () => { - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/github', - async globalContext() { - const { - GITHUB_ACTIONS, - GITHUB_JOB, - GITHUB_REPOSITORY, - GITHUB_RUN_ATTEMPT, - GITHUB_RUN_ID, - GITHUB_SERVER_URL, - GITHUB_TOKEN, - } = process.env; - - if (!GITHUB_ACTIONS) { - return; - } - - const apiUrl = `https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}/jobs`; - const headers: HeadersInit = { - Accept: 'application/vnd.github.v3+json', - }; - if (GITHUB_TOKEN) { - headers.Authorization = `token ${GITHUB_TOKEN}`; - } - - try { - const response = await fetch(apiUrl, { headers }); - if (!response.ok) { - // convert to http error - throw new Error(`HTTP ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - const job = - data.jobs.length === 1 - ? data.jobs[0] - : data.jobs.find((job: any) => snakeCase(job.name) === GITHUB_JOB); - - if (job) { - process.env.ALLURE_GITHUB_JOB_ID = job.id; - process.env.ALLURE_GITHUB_URL = job.html_url; - } - } catch (error: unknown) { - // TODO: migrate to bunyamin - console.error(`Failed to fetch job ID from: ${apiUrl}\nReason:`, error); - } - - process.env.ALLURE_GITHUB_URL ||= `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/index.ts b/src/reporter-plugins/index.ts index 579f262e..3d1f19e2 100644 --- a/src/reporter-plugins/index.ts +++ b/src/reporter-plugins/index.ts @@ -1,6 +1,5 @@ +export { ciPlugin as ci } from './ci'; export { docblockPlugin as docblock } from './docblock'; -export { fallbackPlugin as fallback } from './fallback'; -export { githubPlugin as github } from './github'; export { manifestPlugin as manifest } from './manifest'; export { remarkPlugin as remark } from './remark'; export { sourceCodePlugin as sourceCode } from './source-code'; diff --git a/src/reporter-plugins/manifest.ts b/src/reporter-plugins/manifest.ts deleted file mode 100644 index 6f0b0d23..00000000 --- a/src/reporter-plugins/manifest.ts +++ /dev/null @@ -1,30 +0,0 @@ -/// - -import fs from 'node:fs'; - -import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; -import pkgUp from 'pkg-up'; - -export const manifestPlugin: PluginConstructor = (_1, { globalConfig }) => { - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/manifest', - async globalContext(context) { - const manifestPath = await pkgUp({ - cwd: globalConfig.rootDir, - }); - - context.manifest = null; - if (manifestPath) { - try { - context.manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - } catch { - console.warn( - `[${plugin.name}] Failed to read package.json from ${manifestPath}`, - ); - } - } - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/manifest/index.ts b/src/reporter-plugins/manifest/index.ts new file mode 100644 index 00000000..fbcdd203 --- /dev/null +++ b/src/reporter-plugins/manifest/index.ts @@ -0,0 +1,25 @@ +/// + +import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; +import importFrom from 'import-from'; + +import { ManifestResolver } from './manifest'; + +export type { ManifestExtractorCallback, ManifestHelper } from './manifest'; + +export const manifestPlugin: PluginConstructor = (_1, { globalConfig }) => { + const cwd = globalConfig.rootDir; + const resolver = new ManifestResolver( + cwd, + (modulePath) => importFrom(cwd, modulePath) as Record, + ); + + const plugin: Plugin = { + name: 'jest-allure2-reporter/plugins/manifest', + async helpers($) { + $.manifest = resolver.extract; + }, + }; + + return plugin; +}; diff --git a/src/reporter-plugins/manifest/manifest.test.ts b/src/reporter-plugins/manifest/manifest.test.ts new file mode 100644 index 00000000..59931452 --- /dev/null +++ b/src/reporter-plugins/manifest/manifest.test.ts @@ -0,0 +1,32 @@ +import { ManifestResolver } from './manifest'; + +describe('manifest', () => { + const manifestResolver = new ManifestResolver( + process.cwd(), + jest.requireActual, + ); + + const manifest = manifestResolver.extract; + + it('should return the entire package.json content of the current package when called without arguments', async () => { + const result = await manifest(); + expect(result).toHaveProperty('version'); + expect(result).toHaveProperty('name', 'jest-allure2-reporter'); + }); + + it('should return the entire package.json content of the specified package when called with a string argument', async () => { + const result = await manifest('lodash'); + expect(result).toHaveProperty('version'); + expect(result).toHaveProperty('name', 'lodash'); + }); + + it('should return a specific property of the package.json content of the current package when called with a callback', async () => { + const version = await manifest((m) => m.version); + expect(typeof version).toBe('string'); + }); + + it('should return a specific property of the package.json content of the specified package when called with a string argument and a callback', async () => { + const version = await manifest('lodash', (m) => m.version); + expect(typeof version).toBe('string'); + }); +}); diff --git a/src/reporter-plugins/manifest/manifest.ts b/src/reporter-plugins/manifest/manifest.ts new file mode 100644 index 00000000..739395f8 --- /dev/null +++ b/src/reporter-plugins/manifest/manifest.ts @@ -0,0 +1,71 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import pkgUp from 'pkg-up'; + +export type ManifestExtractorCallback = (manifest: Record) => T; + +export interface ManifestHelper { + (): Promise | undefined>; + (packageName: string): Promise | undefined>; + (callback: ManifestExtractorCallback): Promise; + (packageName: string, callback: ManifestExtractorCallback): Promise; +} + +export type ImportModuleFunction = ( + path: string, +) => Record | Promise>; + +export class ManifestResolver { + private readonly cwd: string; + private readonly importFn: ImportModuleFunction; + + constructor(cwd: string, importFunction: ImportModuleFunction) { + this.cwd = cwd; + this.importFn = importFunction; + } + + public extract: ManifestHelper = ( + packageNameOrCallback?: string | ManifestExtractorCallback, + callback?: ManifestExtractorCallback, + ) => { + if (!packageNameOrCallback) { + return this.manifestImpl(); + } else if (this.isManifestExtractorCallback(packageNameOrCallback)) { + return this.manifestImpl(undefined, packageNameOrCallback); + } else if (callback) { + return this.manifestImpl(packageNameOrCallback, callback); + } else { + return this.manifestImpl(packageNameOrCallback); + } + }; + + private isManifestExtractorCallback( + value: unknown, + ): value is ManifestExtractorCallback { + return typeof value === 'function'; + } + + private async manifestImpl( + packageName?: string, + callback?: ManifestExtractorCallback, + ): Promise { + const manifestPath = await this.resolveManifestPath(packageName); + if (!manifestPath) { + // TODO: log warning + return; + } + + try { + const manifest = await this.importFn(manifestPath); + return callback ? callback(manifest as any) : manifest; + } catch { + // TODO: log error + return; + } + } + + private async resolveManifestPath(packageName?: string) { + return packageName + ? require.resolve(packageName + '/package.json') + : await pkgUp({ cwd: this.cwd }); + } +} diff --git a/src/reporter-plugins/source-code/index.ts b/src/reporter-plugins/source-code/index.ts index eab7ed14..c17b8775 100644 --- a/src/reporter-plugins/source-code/index.ts +++ b/src/reporter-plugins/source-code/index.ts @@ -1 +1,8 @@ export * from './sourceCodePlugin'; +export { + LineNavigator, + FileNavigatorCache, + importTypeScript, + extractTypeScriptCode, + extractTypescriptAST, +} from './utils'; diff --git a/src/reporter-plugins/source-code/prettier.ts b/src/reporter-plugins/source-code/prettier.ts deleted file mode 100644 index 89ae0b68..00000000 --- a/src/reporter-plugins/source-code/prettier.ts +++ /dev/null @@ -1,44 +0,0 @@ -/// - -import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; -import type { Options } from 'prettier'; - -export const prettierPlugin: PluginConstructor = ( - overrides, - { globalConfig }, -) => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const prettier = require('prettier'); - let prettierConfig: Options; - - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/prettier', - async globalContext() { - prettierConfig = { - parser: 'acorn', - ...(await prettier.resolveConfig(globalConfig.rootDir)), - ...overrides, - }; - }, - async testCaseContext(context) { - const code = context.testCaseMetadata.transformedCode; - if (code) { - context.testCaseMetadata.transformedCode = await prettier.format( - code.trim(), - prettierConfig, - ); - } - }, - async testStepContext(context) { - const code = context.testStepMetadata.transformedCode; - if (code) { - context.testStepMetadata.transformedCode = await prettier.format( - code.trim(), - prettierConfig, - ); - } - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/source-code/sourceCodePlugin.ts b/src/reporter-plugins/source-code/sourceCodePlugin.ts index 41edf7a9..a32d0fb9 100644 --- a/src/reporter-plugins/source-code/sourceCodePlugin.ts +++ b/src/reporter-plugins/source-code/sourceCodePlugin.ts @@ -1,20 +1,21 @@ /// +import type { ExtractorHelperSourceCode } from 'jest-allure2-reporter'; import type { AllureTestItemSourceLocation, AllureNestedTestStepMetadata, - CodeExtractorResult, Plugin, PluginConstructor, } from 'jest-allure2-reporter'; +import { weakMemoize } from '../../utils'; + import { - ensureTypeScriptAST, + extractTypescriptAST, extractTypeScriptCode, FileNavigatorCache, importTypeScript, -} from '../utils'; - +} from './utils'; import { detectSourceLanguage } from './detectSourceLanguage'; function isBeforeHook({ hookType }: AllureNestedTestStepMetadata) { @@ -32,17 +33,9 @@ function isDefined(value: T | undefined): value is T { export const sourceCodePlugin: PluginConstructor = async () => { const ts = await importTypeScript(); - const promiseCache = new WeakMap< - AllureTestItemSourceLocation, - Promise - >(); - const cache = new WeakMap< - AllureTestItemSourceLocation, - CodeExtractorResult - >(); - const extractAndCache = async ( + async function doExtract( sourceLocation: AllureTestItemSourceLocation | undefined, - ) => { + ): Promise { if (!sourceLocation?.fileName) { return; } @@ -50,66 +43,49 @@ export const sourceCodePlugin: PluginConstructor = async () => { await FileNavigatorCache.instance.resolve(sourceLocation.fileName); const language = detectSourceLanguage(sourceLocation.fileName); if ((language === 'typescript' || language === 'javascript') && ts) { - const ast = await ensureTypeScriptAST(ts, sourceLocation.fileName); + const ast = await extractTypescriptAST(ts, sourceLocation.fileName); const location = [ sourceLocation.lineNumber, sourceLocation.columnNumber, ] as const; const code = await extractTypeScriptCode(ts, ast, location); if (code) { - cache.set(sourceLocation, { + return { code, language, - ast, - }); + fileName: sourceLocation.fileName, + }; } } - return cache.get(sourceLocation); - }; + return; + } const plugin: Plugin = { name: 'jest-allure2-reporter/plugins/source-code', async helpers($) { - const extractSourceCode = (metadata: { - sourceLocation?: AllureTestItemSourceLocation; - }) => { - return metadata.sourceLocation - ? cache.get(metadata.sourceLocation) - : undefined; - }; - - $.extractSourceCode = extractSourceCode; - - $.extractSourceCodeAsync = async (metadata) => { - if (!metadata.sourceLocation) { - return; - } - - if (!promiseCache.has(metadata.sourceLocation)) { - promiseCache.set( - metadata.sourceLocation, - extractAndCache(metadata.sourceLocation), - ); - } - - return promiseCache.get(metadata.sourceLocation); - }; - - $.extractSourceCodeWithSteps = (metadata) => { - const test = extractSourceCode(metadata); + const extractSourceCode = weakMemoize(async (metadata) => { + return doExtract(metadata.sourceLocation); + }); + + const extractSourceCodeWithSteps = weakMemoize(async (metadata) => { + const test = await doExtract(metadata); const before = - metadata.steps?.filter(isBeforeHook)?.map(extractSourceCode) ?? []; - const after = - metadata.steps?.filter(isAfterHook)?.map(extractSourceCode) ?? []; + metadata.steps?.filter(isBeforeHook)?.map(doExtract) ?? []; + const after = metadata.steps?.filter(isAfterHook)?.map(doExtract) ?? []; return [...before, test, ...after].filter(isDefined); - }; + }); + + Object.assign($, { + extractSourceCode, + extractSourceCodeWithSteps, + }); }, - async rawMetadata(context) { - await context.$.extractSourceCodeAsync(context.metadata); + async postProcessMetadata(context) { + await context.$.extractSourceCode(context.metadata); }, }; diff --git a/src/reporter-plugins/utils/FileNavigatorCache.ts b/src/reporter-plugins/source-code/utils/FileNavigatorCache.ts similarity index 100% rename from src/reporter-plugins/utils/FileNavigatorCache.ts rename to src/reporter-plugins/source-code/utils/FileNavigatorCache.ts diff --git a/src/reporter-plugins/utils/LineNavigator.test.ts b/src/reporter-plugins/source-code/utils/LineNavigator.test.ts similarity index 100% rename from src/reporter-plugins/utils/LineNavigator.test.ts rename to src/reporter-plugins/source-code/utils/LineNavigator.test.ts diff --git a/src/reporter-plugins/utils/LineNavigator.ts b/src/reporter-plugins/source-code/utils/LineNavigator.ts similarity index 100% rename from src/reporter-plugins/utils/LineNavigator.ts rename to src/reporter-plugins/source-code/utils/LineNavigator.ts diff --git a/src/reporter-plugins/utils/extractTypeScriptCode.ts b/src/reporter-plugins/source-code/utils/extractTypeScriptCode.ts similarity index 96% rename from src/reporter-plugins/utils/extractTypeScriptCode.ts rename to src/reporter-plugins/source-code/utils/extractTypeScriptCode.ts index 34cc40f4..9fd39f73 100644 --- a/src/reporter-plugins/utils/extractTypeScriptCode.ts +++ b/src/reporter-plugins/source-code/utils/extractTypeScriptCode.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line node/no-unpublished-import import type TypeScript from 'typescript'; -import { autoIndent } from './autoIndent'; +import { autoIndent } from '../../../utils'; export async function extractTypeScriptCode( ts: typeof TypeScript, diff --git a/src/reporter-plugins/utils/ensureTypeScriptAST.ts b/src/reporter-plugins/source-code/utils/extractTypescriptAST.ts similarity index 95% rename from src/reporter-plugins/utils/ensureTypeScriptAST.ts rename to src/reporter-plugins/source-code/utils/extractTypescriptAST.ts index 6fc64f7e..431463ab 100644 --- a/src/reporter-plugins/utils/ensureTypeScriptAST.ts +++ b/src/reporter-plugins/source-code/utils/extractTypescriptAST.ts @@ -8,7 +8,7 @@ const sourceFileMap = new Map< Promise >(); -export function ensureTypeScriptAST( +export function extractTypescriptAST( ts: typeof TypeScript, filePath: string, ): Promise { diff --git a/src/reporter-plugins/utils/importTypeScript.ts b/src/reporter-plugins/source-code/utils/importTypeScript.ts similarity index 100% rename from src/reporter-plugins/utils/importTypeScript.ts rename to src/reporter-plugins/source-code/utils/importTypeScript.ts diff --git a/src/reporter-plugins/utils/index.ts b/src/reporter-plugins/source-code/utils/index.ts similarity index 78% rename from src/reporter-plugins/utils/index.ts rename to src/reporter-plugins/source-code/utils/index.ts index 15332e10..d98fa544 100644 --- a/src/reporter-plugins/utils/index.ts +++ b/src/reporter-plugins/source-code/utils/index.ts @@ -1,4 +1,4 @@ -export * from './ensureTypeScriptAST'; +export * from './extractTypescriptAST'; export * from './extractTypeScriptCode'; export * from './importTypeScript'; export * from './FileNavigatorCache'; diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index aefcb3b1..ef6d263d 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -10,9 +10,7 @@ import type { TestContext, TestResult, } from '@jest/reporters'; -import type { Metadata } from 'jest-metadata'; import { state } from 'jest-metadata'; -import { metadataRegistryEvents } from 'jest-metadata/debug'; import JestMetadataReporter from 'jest-metadata/reporter'; import rimraf from 'rimraf'; import type { @@ -20,24 +18,22 @@ import type { Attachment, Category, ExecutableItemWrapper, - Label, - Link, - Parameter, Stage, Status, - StatusDetails, } from '@noomorph/allure-js-commons'; import { AllureRuntime } from '@noomorph/allure-js-commons'; import type { AllureGlobalMetadata, AllureTestCaseMetadata, + AllureTestCaseResult, AllureTestFileMetadata, AllureTestStepMetadata, + AllureTestStepResult, ExtractorHelpers, GlobalExtractorContext, Plugin, - PluginHookName, PluginHookContexts, + PluginHookName, ReporterConfig, ReporterOptions, TestCaseExtractorContext, @@ -49,13 +45,14 @@ import { resolveOptions } from '../options'; import { AllureMetadataProxy, MetadataSquasher } from '../metadata'; import { md5 } from '../utils'; +import * as fallbackHooks from './fallback'; + export class JestAllure2Reporter extends JestMetadataReporter { private _plugins: readonly Plugin[] = []; private readonly _$: Partial = {}; private readonly _allure: AllureRuntime; private readonly _config: ReporterConfig; private readonly _globalConfig: Config.GlobalConfig; - private readonly _newMetadata: Metadata[] = []; constructor(globalConfig: Config.GlobalConfig, options: ReporterOptions) { super(globalConfig); @@ -67,8 +64,6 @@ export class JestAllure2Reporter extends JestMetadataReporter { resultsDir: this._config.resultsDir, }); - metadataRegistryEvents.on('register_metadata', this._registerMetadata); - const globalMetadata = new AllureMetadataProxy(state); globalMetadata.set('config', { resultsDir: this._config.resultsDir, @@ -92,47 +87,27 @@ export class JestAllure2Reporter extends JestMetadataReporter { } await this._callPlugins('helpers', this._$); - - await this._callPlugins('onRunStart', { - aggregatedResult, - reporterConfig: this._config, - }); } async onTestFileStart(test: Test) { super.onTestFileStart(test); - const testFileMetadata = JestAllure2Reporter.query.test(test); - const testFileProxy = new AllureMetadataProxy( - testFileMetadata, + const postProcessMetadata = JestAllure2Reporter.query.test(test); + const testFileMetadata = new AllureMetadataProxy( + postProcessMetadata, ); - await this._callPlugins('onTestFileStart', { - reporterConfig: this._config, - test, - testFileMetadata: testFileProxy.defaults({}).get(), - }); + + fallbackHooks.onTestFileStart(test, testFileMetadata); } async onTestCaseResult(test: Test, testCaseResult: TestCaseResult) { super.onTestCaseResult(test, testCaseResult); - const testFileMetadata = JestAllure2Reporter.query.test(test); - const testCaseMetadata = - JestAllure2Reporter.query.testCaseResult(testCaseResult).lastInvocation!; - const testFileProxy = new AllureMetadataProxy( - testFileMetadata, - ); - const testCaseProxy = new AllureMetadataProxy( - testCaseMetadata, + const testCaseMetadata = new AllureMetadataProxy( + JestAllure2Reporter.query.testCaseResult(testCaseResult).lastInvocation!, ); - await this._callPlugins('onTestCaseResult', { - reporterConfig: this._config, - test, - testCaseResult, - testCaseMetadata: testCaseProxy.defaults({}).get(), - testFileMetadata: testFileProxy.defaults({}).get(), - }); + fallbackHooks.onTestCaseResult(testCaseMetadata); } async onTestFileResult( @@ -140,18 +115,11 @@ export class JestAllure2Reporter extends JestMetadataReporter { testResult: TestResult, aggregatedResult: AggregatedResult, ) { - const testFileMetadata = JestAllure2Reporter.query.test(test); - const testFileProxy = new AllureMetadataProxy( - testFileMetadata, + const testFileMetadata = new AllureMetadataProxy( + JestAllure2Reporter.query.test(test), ); - await this._callPlugins('onTestFileResult', { - reporterConfig: this._config, - test, - testResult, - aggregatedResult, - testFileMetadata: testFileProxy.defaults({}).get(), - }); + fallbackHooks.onTestFileResult(test, testFileMetadata); return super.onTestFileResult(test, testResult, aggregatedResult); } @@ -162,14 +130,6 @@ export class JestAllure2Reporter extends JestMetadataReporter { ): Promise { await super.onRunComplete(testContexts, results); - metadataRegistryEvents.off('register_metadata', this._registerMetadata); - - await this._callPlugins('onRunComplete', { - reporterConfig: this._config, - testContexts, - results, - }); - const config = this._config; const globalContext: GlobalExtractorContext = { @@ -179,14 +139,12 @@ export class JestAllure2Reporter extends JestMetadataReporter { $: this._$ as ExtractorHelpers, }; - await this._callPlugins('globalContext', globalContext); - - const environment = config.environment(globalContext); + const environment = await config.environment(globalContext); if (environment) { this._allure.writeEnvironmentInfo(environment); } - const executor = config.executor(globalContext); + const executor = await config.executor(globalContext); if (executor) { this._allure.writeExecutorInfo(executor); } @@ -216,28 +174,30 @@ export class JestAllure2Reporter extends JestMetadataReporter { ), }; - await this._callPlugins('testFileContext', testFileContext); - if (!config.testFile.hidden(testFileContext)) { // pseudo-test entity, used for reporting file-level errors and other obscure purposes + const attachments = await config.testFile.attachments(testFileContext); const allureFileTest: AllurePayloadTest = { - name: config.testFile.name(testFileContext), - start: config.testFile.start(testFileContext), - stop: config.testFile.stop(testFileContext), - historyId: config.testFile.historyId(testFileContext), - fullName: config.testFile.fullName(testFileContext), - description: config.testFile.description(testFileContext), - descriptionHtml: config.testFile.descriptionHtml(testFileContext), + name: await config.testFile.name(testFileContext), + start: await config.testFile.start(testFileContext), + stop: await config.testFile.stop(testFileContext), + historyId: await config.testFile.historyId(testFileContext), + fullName: await config.testFile.fullName(testFileContext), + description: await config.testFile.description(testFileContext), + descriptionHtml: + await config.testFile.descriptionHtml(testFileContext), // TODO: merge @noomorph/allure-js-commons into this package and remove casting - stage: config.testFile.stage(testFileContext) as string as Stage, - status: config.testFile.status(testFileContext) as string as Status, - statusDetails: config.testFile.statusDetails(testFileContext), - links: config.testFile.links(testFileContext), - labels: config.testFile.labels(testFileContext), - parameters: config.testFile.parameters(testFileContext), - attachments: config.testFile - .attachments(testFileContext) - ?.map(this._relativizeAttachment), + stage: (await config.testFile.stage( + testFileContext, + )) as string as Stage, + status: (await config.testFile.status( + testFileContext, + )) as string as Status, + statusDetails: await config.testFile.statusDetails(testFileContext), + links: await config.testFile.links(testFileContext), + labels: await config.testFile.labels(testFileContext), + parameters: await config.testFile.parameters(testFileContext), + attachments: attachments?.map(this._relativizeAttachment), }; await this._renderHtmlDescription(testFileContext, allureFileTest); @@ -263,8 +223,6 @@ export class JestAllure2Reporter extends JestMetadataReporter { testCaseMetadata, }; - await this._callPlugins('testCaseContext', testCaseContext); - if (config.testCase.hidden(testCaseContext)) { continue; } @@ -290,48 +248,52 @@ export class JestAllure2Reporter extends JestMetadataReporter { let allureSteps: AllurePayloadStep[] = await Promise.all( visibleTestStepContexts.map(async (testStepContext) => { - await this._callPlugins('testStepContext', testStepContext); - + const attachments = + await config.testStep.attachments(testStepContext); const result: AllurePayloadStep = { hookType: testStepContext.testStepMetadata.hookType, - name: config.testStep.name(testStepContext), - start: config.testStep.start(testStepContext), - stop: config.testStep.stop(testStepContext), - stage: config.testStep.stage( + name: await config.testStep.name(testStepContext), + start: await config.testStep.start(testStepContext), + stop: await config.testStep.stop(testStepContext), + stage: (await config.testStep.stage( testStepContext, - ) as string as Stage, - status: config.testStep.status( + )) as string as Stage, + status: (await config.testStep.status( testStepContext, - ) as string as Status, - statusDetails: config.testStep.statusDetails(testStepContext), - parameters: config.testStep.parameters(testStepContext), - attachments: config.testStep - .attachments(testStepContext) - ?.map(this._relativizeAttachment), + )) as string as Status, + statusDetails: + await config.testStep.statusDetails(testStepContext), + parameters: await config.testStep.parameters(testStepContext), + attachments: attachments?.map(this._relativizeAttachment), }; return result; }), ); + const attachments = + await config.testCase.attachments(testCaseContext); const allureTest: AllurePayloadTest = { - name: config.testCase.name(testCaseContext), - start: config.testCase.start(testCaseContext), - stop: config.testCase.stop(testCaseContext), - historyId: config.testCase.historyId(testCaseContext), - fullName: config.testCase.fullName(testCaseContext), - description: config.testCase.description(testCaseContext), - descriptionHtml: config.testCase.descriptionHtml(testCaseContext), + name: await config.testCase.name(testCaseContext), + start: await config.testCase.start(testCaseContext), + stop: await config.testCase.stop(testCaseContext), + historyId: await config.testCase.historyId(testCaseContext), + fullName: await config.testCase.fullName(testCaseContext), + description: await config.testCase.description(testCaseContext), + descriptionHtml: + await config.testCase.descriptionHtml(testCaseContext), // TODO: merge @noomorph/allure-js-commons into this package and remove casting - stage: config.testCase.stage(testCaseContext) as string as Stage, - status: config.testCase.status(testCaseContext) as string as Status, - statusDetails: config.testCase.statusDetails(testCaseContext), - links: config.testCase.links(testCaseContext), - labels: config.testCase.labels(testCaseContext), - parameters: config.testCase.parameters(testCaseContext), - attachments: config.testCase - .attachments(testCaseContext) - ?.map(this._relativizeAttachment), + stage: (await config.testCase.stage( + testCaseContext, + )) as string as Stage, + status: (await config.testCase.status( + testCaseContext, + )) as string as Status, + statusDetails: await config.testCase.statusDetails(testCaseContext), + links: await config.testCase.links(testCaseContext), + labels: await config.testCase.labels(testCaseContext), + parameters: await config.testCase.parameters(testCaseContext), + attachments: attachments?.map(this._relativizeAttachment), steps: allureSteps.filter((step) => !step.hookType), }; @@ -440,10 +402,10 @@ export class JestAllure2Reporter extends JestMetadataReporter { executable.wrappedItem.stop = step.stop; } if (step.stage !== undefined) { - executable.stage = step.stage; + executable.stage = step.stage as string as Stage; } if (step.status !== undefined) { - executable.status = step.status; + executable.status = step.status as string as Status; } if (step.statusDetails !== undefined) { executable.statusDetails = step.statusDetails; @@ -465,12 +427,16 @@ export class JestAllure2Reporter extends JestMetadataReporter { } private async _postProcessMetadata() { - const newBatch = this._newMetadata.splice(0, this._newMetadata.length); + const batch = state.testFiles.flatMap((testFile) => [ + testFile, + ...testFile.allDescribeBlocks(), + ...testFile.allTestEntries(), + ]); await Promise.all( - newBatch.map(async (metadata) => { + batch.map(async (metadata) => { const allureProxy = new AllureMetadataProxy(metadata); - await this._callPlugins('rawMetadata', { + await this._callPlugins('postProcessMetadata', { $: this._$ as ExtractorHelpers, metadata: allureProxy.assign({}).get(), }); @@ -500,10 +466,6 @@ export class JestAllure2Reporter extends JestMetadataReporter { source, }; }; - - private readonly _registerMetadata = (event: { metadata: Metadata }) => { - this._newMetadata.push(event.metadata); - }; } type AllurePayload = { @@ -512,27 +474,12 @@ type AllurePayload = { steps?: AllurePayloadStep[]; }; -type AllurePayloadStep = Partial<{ +interface AllurePayloadStep + extends Partial> { hookType?: 'beforeAll' | 'beforeEach' | 'afterEach' | 'afterAll'; + steps?: AllurePayloadStep[]; +} - name: string; - start: number; - stop: number; - status: Status; - statusDetails: StatusDetails; - stage: Stage; - steps: AllurePayloadStep[]; - attachments: Attachment[]; - parameters: Parameter[]; -}>; - -type AllurePayloadTest = Partial<{ - hookType?: never; - historyId: string; - fullName: string; - description: string; - descriptionHtml: string; - labels: Label[]; - links: Link[]; -}> & - AllurePayloadStep; +interface AllurePayloadTest extends Partial { + steps?: AllurePayloadStep[]; +} diff --git a/src/reporter/ThreadService.ts b/src/reporter/fallback/ThreadService.ts similarity index 100% rename from src/reporter/ThreadService.ts rename to src/reporter/fallback/ThreadService.ts diff --git a/src/reporter/fallback/index.ts b/src/reporter/fallback/index.ts new file mode 100644 index 00000000..3d177efd --- /dev/null +++ b/src/reporter/fallback/index.ts @@ -0,0 +1,41 @@ +import type { Test } from '@jest/reporters'; +import type { + AllureTestItemMetadata, + AllureTestFileMetadata, +} from 'jest-allure2-reporter'; + +import type { AllureMetadataProxy } from '../../metadata'; + +import { ThreadService } from './ThreadService'; + +const threadService = new ThreadService(); + +export async function onTestFileStart( + test: Test, + testFileMetadata: AllureMetadataProxy, +) { + const threadId = threadService.allocateThread(test.path); + + testFileMetadata.assign({ + sourceLocation: { fileName: test.path }, + start: Date.now(), + workerId: String(1 + threadId), + }); +} + +export async function onTestCaseResult( + testCaseMetadata: AllureMetadataProxy, +) { + const stop = testCaseMetadata.get('stop') ?? Number.NaN; + if (Number.isNaN(stop)) { + testCaseMetadata.set('stop', Date.now()); + } +} + +export async function onTestFileResult( + test: Test, + testFileMetadata: AllureMetadataProxy, +) { + testFileMetadata.set('stop', Date.now()); + threadService.freeThread(test.path); +} diff --git a/src/reporter-plugins/utils/autoIndent.ts b/src/utils/autoIndent.ts similarity index 100% rename from src/reporter-plugins/utils/autoIndent.ts rename to src/utils/autoIndent.ts diff --git a/src/utils/flatMapAsync.test.ts b/src/utils/flatMapAsync.test.ts new file mode 100644 index 00000000..62f816e2 --- /dev/null +++ b/src/utils/flatMapAsync.test.ts @@ -0,0 +1,30 @@ +import { flatMapAsync } from './flatMapAsync'; + +describe('flatMapAsync', () => { + it('should flatten and map arrays asynchronously', async () => { + const input = [1, 2, 3]; + const callback = async (value: number) => [value, value * 2]; + + const result = await flatMapAsync(input, callback); + + expect(result).toEqual([1, 2, 2, 4, 3, 6]); + }); + + it('should handle empty arrays', async () => { + const input: number[] = []; + const callback = async (value: number) => [value]; + + const result = await flatMapAsync(input, callback); + + expect(result).toEqual([]); + }); + + it('should handle callbacks returning empty arrays', async () => { + const input = [1, 2, 3]; + const callback = async () => []; + + const result = await flatMapAsync(input, callback); + + expect(result).toEqual([]); + }); +}); diff --git a/src/utils/flatMapAsync.ts b/src/utils/flatMapAsync.ts new file mode 100644 index 00000000..1e020913 --- /dev/null +++ b/src/utils/flatMapAsync.ts @@ -0,0 +1,7 @@ +export async function flatMapAsync( + array: T[], + callback: (value: T, index: number, array: T[]) => Promise, +): Promise { + const mappedArrays = await Promise.all(array.map(callback)); + return mappedArrays.flat(); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 648ec204..88f101fa 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './attempt'; +export * from './autoIndent'; export * from './constant'; export * from './formatString'; export * from './getStatusDetails'; diff --git a/src/utils/weakMemoize.test.ts b/src/utils/weakMemoize.test.ts index 612559cb..e5cef506 100644 --- a/src/utils/weakMemoize.test.ts +++ b/src/utils/weakMemoize.test.ts @@ -1,18 +1,18 @@ import { weakMemoize } from './weakMemoize'; describe('weakMemoize', () => { - let random: (object?: object | null) => number; + let random: (object?: object | null | undefined) => number; beforeEach(() => { random = weakMemoize(() => Math.random()); }); - it('should not memoize on undefined', () => { - expect(random()).not.toBe(random()); + it('should memoize on undefined', () => { + expect(random()).toBe(random(void 0)); }); - it('should not memoize on null', () => { - expect(random(null)).not.toBe(random(null)); + it('should memoize on null', () => { + expect(random(null)).toBe(random(null)); }); it('should not memoize on different objects', () => { diff --git a/src/utils/weakMemoize.ts b/src/utils/weakMemoize.ts index 3a82d545..33223d22 100644 --- a/src/utils/weakMemoize.ts +++ b/src/utils/weakMemoize.ts @@ -1,18 +1,23 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export function weakMemoize any>(function_: F): F { - const cache = new WeakMap(); + const nullCache = new Map(); + const weakCache = new WeakMap(); const memoizedFunction = ((argument: any) => { if (argument == null) { - return function_(argument); - } + if (!nullCache.has(argument)) { + nullCache.set(argument, function_(argument)); + } - if (!cache.has(argument)) { - cache.set(argument, function_(argument)); - } + return nullCache.get(argument)!; + } else { + if (!weakCache.has(argument)) { + weakCache.set(argument, function_(argument)); + } - return cache.get(argument)!; + return weakCache.get(argument)!; + } }) as F; return memoizedFunction; From 94ca8387a9413757bc72a31229954ba86078bb65 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 16 Mar 2024 11:10:15 +0200 Subject: [PATCH 10/50] more fixes --- e2e/jest.config.js | 8 ++++---- .../{manifest.test.ts => ManifestResolver.test.ts} | 2 +- .../manifest/{manifest.ts => ManifestResolver.ts} | 0 src/reporter-plugins/manifest/index.ts | 7 +++++-- src/reporter/JestAllure2Reporter.ts | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) rename src/reporter-plugins/manifest/{manifest.test.ts => ManifestResolver.test.ts} (95%) rename src/reporter-plugins/manifest/{manifest.ts => ManifestResolver.ts} (100%) diff --git a/e2e/jest.config.js b/e2e/jest.config.js index 54af794d..0f903553 100644 --- a/e2e/jest.config.js +++ b/e2e/jest.config.js @@ -19,12 +19,12 @@ const jestAllure2ReporterOptions = { messageRegex: /.*Exceeded timeout of.*/, }, ], - environment: (context) => { + environment: async ({ $ }) => { return ({ 'version.node': process.version, - 'version.jest': require('jest/package.json').version, - 'package.name': context.manifest.name, - 'package.version': context.manifest.version, + 'version.jest': await $.manifest('jest', jest => jest.version), + 'package.name': await $.manifest(pkg => pkg.name), + 'package.version': await $.manifest(pkg => pkg.version), }); }, testCase: { diff --git a/src/reporter-plugins/manifest/manifest.test.ts b/src/reporter-plugins/manifest/ManifestResolver.test.ts similarity index 95% rename from src/reporter-plugins/manifest/manifest.test.ts rename to src/reporter-plugins/manifest/ManifestResolver.test.ts index 59931452..ef3a835b 100644 --- a/src/reporter-plugins/manifest/manifest.test.ts +++ b/src/reporter-plugins/manifest/ManifestResolver.test.ts @@ -1,4 +1,4 @@ -import { ManifestResolver } from './manifest'; +import { ManifestResolver } from './ManifestResolver'; describe('manifest', () => { const manifestResolver = new ManifestResolver( diff --git a/src/reporter-plugins/manifest/manifest.ts b/src/reporter-plugins/manifest/ManifestResolver.ts similarity index 100% rename from src/reporter-plugins/manifest/manifest.ts rename to src/reporter-plugins/manifest/ManifestResolver.ts diff --git a/src/reporter-plugins/manifest/index.ts b/src/reporter-plugins/manifest/index.ts index fbcdd203..834fe03e 100644 --- a/src/reporter-plugins/manifest/index.ts +++ b/src/reporter-plugins/manifest/index.ts @@ -3,9 +3,12 @@ import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; import importFrom from 'import-from'; -import { ManifestResolver } from './manifest'; +import { ManifestResolver } from './ManifestResolver'; -export type { ManifestExtractorCallback, ManifestHelper } from './manifest'; +export type { + ManifestExtractorCallback, + ManifestHelper, +} from './ManifestResolver'; export const manifestPlugin: PluginConstructor = (_1, { globalConfig }) => { const cwd = globalConfig.rootDir; diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index ef6d263d..9b1cc8a1 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -149,7 +149,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { this._allure.writeExecutorInfo(executor); } - const categories = config.categories(globalContext); + const categories = await config.categories(globalContext); if (categories) { this._allure.writeCategoriesDefinitions(categories as Category[]); } From a973c564164c2c9651f77a97c81c5fcccd4c6890 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 16 Mar 2024 12:55:52 +0200 Subject: [PATCH 11/50] minor fixes --- index.d.ts | 4 +- .../source-code/sourceCodePlugin.ts | 37 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/index.d.ts b/index.d.ts index c691d060..92029121 100644 --- a/index.d.ts +++ b/index.d.ts @@ -619,8 +619,8 @@ declare module 'jest-allure2-reporter' { } export interface AllureTestItemDocblock { - comments: string; - pragmas: Record; + comments?: string; + pragmas?: Record; } // endregion diff --git a/src/reporter-plugins/source-code/sourceCodePlugin.ts b/src/reporter-plugins/source-code/sourceCodePlugin.ts index a32d0fb9..393b1d53 100644 --- a/src/reporter-plugins/source-code/sourceCodePlugin.ts +++ b/src/reporter-plugins/source-code/sourceCodePlugin.ts @@ -2,8 +2,8 @@ import type { ExtractorHelperSourceCode } from 'jest-allure2-reporter'; import type { - AllureTestItemSourceLocation, AllureNestedTestStepMetadata, + AllureTestItemSourceLocation, Plugin, PluginConstructor, } from 'jest-allure2-reporter'; @@ -65,18 +65,29 @@ export const sourceCodePlugin: PluginConstructor = async () => { name: 'jest-allure2-reporter/plugins/source-code', async helpers($) { - const extractSourceCode = weakMemoize(async (metadata) => { - return doExtract(metadata.sourceLocation); - }); - - const extractSourceCodeWithSteps = weakMemoize(async (metadata) => { - const test = await doExtract(metadata); - const before = - metadata.steps?.filter(isBeforeHook)?.map(doExtract) ?? []; - const after = metadata.steps?.filter(isAfterHook)?.map(doExtract) ?? []; - - return [...before, test, ...after].filter(isDefined); - }); + const extractSourceCode = weakMemoize( + async ( + metadata: AllureNestedTestStepMetadata, + ): Promise => { + return doExtract(metadata.sourceLocation); + }, + ); + + const extractSourceCodeWithSteps = weakMemoize( + async ( + metadata: AllureNestedTestStepMetadata, + ): Promise => { + const test = await extractSourceCode(metadata); + const before = await Promise.all( + metadata.steps?.filter(isBeforeHook)?.map(extractSourceCode) ?? [], + ); + const after = await Promise.all( + metadata.steps?.filter(isAfterHook)?.map(extractSourceCode) ?? [], + ); + + return [...before, test, ...after].filter(isDefined); + }, + ); Object.assign($, { extractSourceCode, From 142c2421b7fe17e8274e7ea05d9fd78f5b3ea86d Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 16 Mar 2024 21:23:09 +0200 Subject: [PATCH 12/50] resurrect docblock :happy: --- e2e/src/programmatic/grouping/names.test.ts | 4 +- src/api/annotations/$Label.ts | 2 +- src/metadata/constants.ts | 1 + .../docblockMapping.ts} | 64 ++++++++++++------- src/metadata/squasher/MetadataSquasher.ts | 28 ++++---- .../utils/aggregateLabelCustomizers.ts | 15 ++--- .../docblock/docblockPlugin.ts | 8 ++- src/reporter/JestAllure2Reporter.ts | 24 ++++--- src/utils/asArray.ts | 7 ++ src/utils/index.ts | 1 + 10 files changed, 92 insertions(+), 62 deletions(-) rename src/{reporter-plugins/docblock/mapping.ts => metadata/docblockMapping.ts} (56%) create mode 100644 src/utils/asArray.ts diff --git a/e2e/src/programmatic/grouping/names.test.ts b/e2e/src/programmatic/grouping/names.test.ts index 6a172fb9..ac70e57b 100644 --- a/e2e/src/programmatic/grouping/names.test.ts +++ b/e2e/src/programmatic/grouping/names.test.ts @@ -9,7 +9,7 @@ import { allure } from 'jest-allure2-reporter/api'; describe('Names', () => { - /* Regular beforeAll */ + // Regular beforeAll beforeAll(() => { }); /** Docblock beforeAll */ @@ -20,7 +20,7 @@ describe('Names', () => { allure.displayName('Programmatic beforeAll'); }); - /* Regular afterEach */ + // Regular afterEach afterEach(() => { }); /** Docblock afterEach */ diff --git a/src/api/annotations/$Label.ts b/src/api/annotations/$Label.ts index 8048f01f..9aa39260 100644 --- a/src/api/annotations/$Label.ts +++ b/src/api/annotations/$Label.ts @@ -3,5 +3,5 @@ import type { LabelName } from 'jest-allure2-reporter'; import { LABELS } from '../../metadata/constants'; -export const $Label = (name: LabelName, ...values: unknown[]) => +export const $Label = (name: LabelName | string, ...values: unknown[]) => $Push(LABELS, ...values.map((value) => ({ name, value: `${value}` }))); diff --git a/src/metadata/constants.ts b/src/metadata/constants.ts index 9d519e83..c1460284 100644 --- a/src/metadata/constants.ts +++ b/src/metadata/constants.ts @@ -1,5 +1,6 @@ export const PREFIX = 'allure2' as const; +export const DOCBLOCK = [PREFIX, 'docblock'] as const; export const CURRENT_STEP = [PREFIX, 'currentStep'] as const; export const DESCRIPTION = [PREFIX, 'description'] as const; export const DESCRIPTION_HTML = [PREFIX, 'descriptionHtml'] as const; diff --git a/src/reporter-plugins/docblock/mapping.ts b/src/metadata/docblockMapping.ts similarity index 56% rename from src/reporter-plugins/docblock/mapping.ts rename to src/metadata/docblockMapping.ts index 6c4cf5d4..766f494b 100644 --- a/src/reporter-plugins/docblock/mapping.ts +++ b/src/metadata/docblockMapping.ts @@ -8,6 +8,33 @@ import type { LinkType, } from 'jest-allure2-reporter'; +import { asArray } from '../utils'; + +const ALL_LABELS = Object.keys( + assertType>({ + epic: 0, + feature: 0, + owner: 0, + package: 0, + parentSuite: 0, + severity: 0, + story: 0, + subSuite: 0, + suite: 0, + tag: 0, + testClass: 0, + testMethod: 0, + thread: 0, + }), +) as LabelName[]; + +const ALL_LINKS = Object.keys( + assertType>({ + issue: 0, + tms: 0, + }), +) as LinkType[]; + export function mapTestStepDocblock({ comments, }: AllureTestItemDocblock): AllureTestStepMetadata { @@ -25,46 +52,31 @@ export function mapTestCaseDocblock( const metadata: AllureTestCaseMetadata = {}; const { comments, pragmas = {} } = context; - const labels = ( - ['epic', 'feature', 'owner', 'severity', 'story', 'tag'] as const - ).flatMap((name) => mapMaybeArray(pragmas[name], createLabelMapper(name))); + const labels = ALL_LABELS.flatMap((name) => + asArray(pragmas[name]).map(createLabelMapper(name)), + ); if (labels.length > 0) metadata.labels = labels; - const links = (['issue', 'tms'] as const) - .flatMap((name) => mapMaybeArray(pragmas[name], createLinkMapper(name))) - .filter(Boolean); + const links = ALL_LINKS.flatMap((name) => + asArray(pragmas[name]).map(createLinkMapper(name)), + ).filter(Boolean); if (links.length > 0) metadata.links = links; if (comments || pragmas.description) metadata.description = [ - ...(comments ? [comments] : []), - ...(pragmas.description || []), + ...asArray(comments), + ...asArray(pragmas.description), ]; if (pragmas.descriptionHtml) { - metadata.descriptionHtml = mapMaybeArray(pragmas.descriptionHtml, (x) => x); + metadata.descriptionHtml = asArray(pragmas.descriptionHtml); } return metadata; } -function mapMaybeArray( - value: T | T[] | undefined, - mapper: (value: T) => R, -): R[] { - if (value == null) { - return []; - } - - if (Array.isArray(value)) { - return value.map(mapper); - } - - return [mapper(value)]; -} - export const mapTestFileDocblock = mapTestCaseDocblock; function createLabelMapper(name: LabelName) { @@ -74,3 +86,7 @@ function createLabelMapper(name: LabelName) { function createLinkMapper(type?: LinkType) { return (url: string): Link => ({ type, url, name: url }); } + +function assertType(value: T) { + return value; +} diff --git a/src/metadata/squasher/MetadataSquasher.ts b/src/metadata/squasher/MetadataSquasher.ts index 29a6c278..45371749 100644 --- a/src/metadata/squasher/MetadataSquasher.ts +++ b/src/metadata/squasher/MetadataSquasher.ts @@ -4,13 +4,14 @@ import type { TestInvocationMetadata, } from 'jest-metadata'; import type { + AllureTestItemDocblock, AllureNestedTestStepMetadata, AllureTestCaseMetadata, AllureTestFileMetadata, - AllureTestItemMetadata, } from 'jest-allure2-reporter'; -import { PREFIX } from '../constants'; +import { DOCBLOCK, PREFIX } from '../constants'; +import * as docblock from '../docblockMapping'; import { MetadataSelector } from './MetadataSelector'; import { @@ -19,12 +20,6 @@ import { mergeTestStepMetadata, } from './mergers'; -export type MetadataSquasherConfig = { - getDocblockMetadata: ( - metadata: Metadata | undefined, - ) => T | undefined; -}; - export class MetadataSquasher { protected readonly _fileSelector: MetadataSelector< Metadata, @@ -41,24 +36,33 @@ export class MetadataSquasher { AllureNestedTestStepMetadata >; - constructor(config: MetadataSquasherConfig) { + constructor() { this._fileSelector = new MetadataSelector({ empty: () => ({}), - getDocblock: config.getDocblockMetadata, + getDocblock: (metadata) => + docblock.mapTestFileDocblock( + metadata.get(DOCBLOCK, {}), + ), getMetadata: (metadata) => metadata.get(PREFIX), mergeUnsafe: mergeTestFileMetadata, }); this._testSelector = new MetadataSelector({ empty: () => ({}), - getDocblock: config.getDocblockMetadata, + getDocblock: (metadata) => + docblock.mapTestCaseDocblock( + metadata.get(DOCBLOCK, {}), + ), getMetadata: (metadata) => metadata.get(PREFIX), mergeUnsafe: mergeTestCaseMetadata, }); this._stepSelector = new MetadataSelector({ empty: () => ({}), - getDocblock: config.getDocblockMetadata, + getDocblock: (metadata) => + docblock.mapTestStepDocblock( + metadata.get(DOCBLOCK, {}), + ), getMetadata: (metadata) => metadata.get(PREFIX), mergeUnsafe: mergeTestStepMetadata, diff --git a/src/options/utils/aggregateLabelCustomizers.ts b/src/options/utils/aggregateLabelCustomizers.ts index 8ab9181f..fb169f20 100644 --- a/src/options/utils/aggregateLabelCustomizers.ts +++ b/src/options/utils/aggregateLabelCustomizers.ts @@ -8,6 +8,7 @@ import type { import type { Label } from 'jest-allure2-reporter'; import { flatMapAsync } from '../../utils/flatMapAsync'; +import { asArray } from '../../utils'; import { asExtractor } from './asExtractor'; @@ -62,7 +63,9 @@ export function aggregateLabelCustomizers( }; const extracted = await extractor(aContext); const value = asArray(extracted); - return value ? value.map((value) => ({ name, value }) as Label) : []; + return value.length > 0 + ? value.map((value) => ({ name, value }) as Label) + : []; })), ]; @@ -71,13 +74,3 @@ export function aggregateLabelCustomizers( return combined; } - -function asArray( - value: T | T[] | undefined, -): T[] | undefined { - if (Array.isArray(value)) { - return value.length > 0 ? value : undefined; - } else { - return value ? [value] : []; - } -} diff --git a/src/reporter-plugins/docblock/docblockPlugin.ts b/src/reporter-plugins/docblock/docblockPlugin.ts index 41e5a152..51ea7b53 100644 --- a/src/reporter-plugins/docblock/docblockPlugin.ts +++ b/src/reporter-plugins/docblock/docblockPlugin.ts @@ -38,9 +38,13 @@ export const docblockPlugin: PluginConstructor = () => { } } - if (!extracted && fileName && lineNumber != null) { + if (!extracted && fileName) { const navigator = await FileNavigatorCache.instance.resolve(fileName); - extracted = extractJsdocAbove(navigator, lineNumber); + + extracted = + lineNumber == null + ? extract(navigator.sourceCode) + : extractJsdocAbove(navigator, lineNumber); } if (extracted) { diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index 9b1cc8a1..3c7e1cf6 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -156,11 +156,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { await this._postProcessMetadata(); // Run before squashing - const docblockParser: any = { find: () => void 0 }; // TODO: await initParser(); - const squasher = new MetadataSquasher({ - getDocblockMetadata: (metadata) => - metadata && docblockParser.find(metadata), - }); + const squasher = new MetadataSquasher(); for (const testResult of results.testResults) { const testFileContext: TestFileExtractorContext = { @@ -427,11 +423,19 @@ export class JestAllure2Reporter extends JestMetadataReporter { } private async _postProcessMetadata() { - const batch = state.testFiles.flatMap((testFile) => [ - testFile, - ...testFile.allDescribeBlocks(), - ...testFile.allTestEntries(), - ]); + const batch = state.testFiles.flatMap((testFile) => { + const allDescribeBlocks = [...testFile.allDescribeBlocks()]; + const allHooks = allDescribeBlocks.flatMap((describeBlock) => [ + ...describeBlock.hookDefinitions(), + ]); + + return [ + testFile, + ...allDescribeBlocks, + ...allHooks, + ...testFile.allTestEntries(), + ]; + }); await Promise.all( batch.map(async (metadata) => { diff --git a/src/utils/asArray.ts b/src/utils/asArray.ts new file mode 100644 index 00000000..7f9d71ea --- /dev/null +++ b/src/utils/asArray.ts @@ -0,0 +1,7 @@ +export function asArray(value: T | T[] | undefined): T[] { + if (Array.isArray(value)) { + return value.length > 0 ? value : []; + } else { + return value ? [value] : []; + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 88f101fa..584585ae 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from './asArray'; export * from './attempt'; export * from './autoIndent'; export * from './constant'; From c1b1abea823d68a68f9768f3698469e2a0de95a7 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 22 Mar 2024 13:19:17 +0200 Subject: [PATCH 13/50] demolishing plugins... --- index.d.ts | 400 ++++++++++++------ src/api/index.ts | 6 +- src/helpers/index.ts | 1 + src/helpers/stripAnsi.test.ts | 20 + src/helpers/stripAnsi.ts | 23 + .../aggregateHelpersCustomizers.ts | 8 + .../aggregateLabelCustomizers.ts | 15 +- .../aggregateLinkCustomizers.ts | 37 ++ src/options/compose-options/index.ts | 50 +-- src/options/compose-options/plugins.ts | 39 -- .../compose-options/reporterOptions.ts | 50 +++ src/options/compose-options/testCase.ts | 31 +- src/options/compose-options/testFile.ts | 22 - src/options/compose-options/testStep.ts | 9 +- src/options/default-options/executor.ts | 57 --- src/options/default-options/helpers.ts | 0 src/options/default-options/index.ts | 13 +- src/options/default-options/testCase.ts | 23 +- src/options/default-options/testFile.ts | 16 +- src/options/default-options/testRun.ts | 39 ++ src/options/default-options/testStep.ts | 11 +- src/options/extractors/asExtractor.ts | 54 +++ src/options/extractors/composeExtractors.ts | 23 + src/options/extractors/extractors.test.ts | 30 ++ src/options/extractors/index.ts | 2 + src/options/utils/aggregateLinkCustomizers.ts | 26 -- src/options/utils/asExtractor.ts | 35 -- src/options/utils/composeExtractors.ts | 8 - src/options/utils/index.ts | 6 - src/options/utils/resolvePlugins.ts | 42 -- src/options/utils/stripStatusDetails.ts | 16 - src/realms/AllureRealm.ts | 4 +- src/reporter-plugins/augs.d.ts | 18 - .../ci/BuildkiteInfoProvider.test.ts | 38 -- .../ci/BuildkiteInfoProvider.ts | 36 -- .../ci/ExecutorInfoProvider.ts | 6 - .../ci/GitHubInfoProvider.test.ts | 140 ------ src/reporter-plugins/ci/GitHubInfoProvider.ts | 103 ----- .../ci/LocalInfoProvider.test.ts | 13 - src/reporter-plugins/ci/LocalInfoProvider.ts | 17 - src/reporter-plugins/ci/index.ts | 38 -- src/reporter-plugins/ci/utils/getOSDetails.ts | 5 - src/reporter-plugins/ci/utils/index.ts | 1 - .../extractJsdocAbove.test.ts.snap | 23 - .../docblock/docblockPlugin.ts | 58 --- .../docblock/extractJsdocAbove.test.ts | 48 --- .../docblock/extractJsdocAbove.ts | 59 --- src/reporter-plugins/docblock/index.ts | 1 - src/reporter-plugins/docblock/parseJsdoc.ts | 17 - src/reporter-plugins/index.ts | 5 - .../manifest/ManifestResolver.test.ts | 32 -- .../manifest/ManifestResolver.ts | 71 ---- src/reporter-plugins/manifest/index.ts | 28 -- src/reporter-plugins/remark.ts | 43 -- .../source-code/detectSourceLanguage.ts | 21 - src/reporter-plugins/source-code/index.ts | 8 - .../source-code/sourceCodePlugin.ts | 104 ----- .../source-code/utils/FileNavigatorCache.ts | 28 -- .../source-code/utils/LineNavigator.test.ts | 64 --- .../source-code/utils/LineNavigator.ts | 62 --- .../utils/extractTypeScriptCode.ts | 39 -- .../source-code/utils/extractTypescriptAST.ts | 30 -- .../source-code/utils/importTypeScript.ts | 11 - .../source-code/utils/index.ts | 5 - src/reporter/JestAllure2Reporter.ts | 36 +- src/runtime/AllureRuntimeContext.ts | 2 +- ...time.ts => AllureRuntimeImplementation.ts} | 41 +- ...time.test.ts.snap => runtime.test.ts.snap} | 0 src/runtime/index.ts | 2 +- src/runtime/modules/AttachmentsModule.ts | 32 +- src/runtime/modules/StepsDecorator.ts | 4 +- ...{AllureRuntime.test.ts => runtime.test.ts} | 12 +- src/runtime/types.ts | 18 +- src/utils/TaskQueue.ts | 10 +- src/utils/hijackFunction.ts | 5 +- src/utils/processMaybePromise.test.ts | 11 +- src/utils/processMaybePromise.ts | 17 +- 77 files changed, 720 insertions(+), 1758 deletions(-) create mode 100644 src/helpers/index.ts create mode 100644 src/helpers/stripAnsi.test.ts create mode 100644 src/helpers/stripAnsi.ts create mode 100644 src/options/compose-options/aggregateHelpersCustomizers.ts rename src/options/{utils => compose-options}/aggregateLabelCustomizers.ts (85%) create mode 100644 src/options/compose-options/aggregateLinkCustomizers.ts delete mode 100644 src/options/compose-options/plugins.ts create mode 100644 src/options/compose-options/reporterOptions.ts delete mode 100644 src/options/compose-options/testFile.ts delete mode 100644 src/options/default-options/executor.ts create mode 100644 src/options/default-options/helpers.ts create mode 100644 src/options/default-options/testRun.ts create mode 100644 src/options/extractors/asExtractor.ts create mode 100644 src/options/extractors/composeExtractors.ts create mode 100644 src/options/extractors/extractors.test.ts create mode 100644 src/options/extractors/index.ts delete mode 100644 src/options/utils/aggregateLinkCustomizers.ts delete mode 100644 src/options/utils/asExtractor.ts delete mode 100644 src/options/utils/composeExtractors.ts delete mode 100644 src/options/utils/index.ts delete mode 100644 src/options/utils/resolvePlugins.ts delete mode 100644 src/options/utils/stripStatusDetails.ts delete mode 100644 src/reporter-plugins/augs.d.ts delete mode 100644 src/reporter-plugins/ci/BuildkiteInfoProvider.test.ts delete mode 100644 src/reporter-plugins/ci/BuildkiteInfoProvider.ts delete mode 100644 src/reporter-plugins/ci/ExecutorInfoProvider.ts delete mode 100644 src/reporter-plugins/ci/GitHubInfoProvider.test.ts delete mode 100644 src/reporter-plugins/ci/GitHubInfoProvider.ts delete mode 100644 src/reporter-plugins/ci/LocalInfoProvider.test.ts delete mode 100644 src/reporter-plugins/ci/LocalInfoProvider.ts delete mode 100644 src/reporter-plugins/ci/index.ts delete mode 100644 src/reporter-plugins/ci/utils/getOSDetails.ts delete mode 100644 src/reporter-plugins/ci/utils/index.ts delete mode 100644 src/reporter-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap delete mode 100644 src/reporter-plugins/docblock/docblockPlugin.ts delete mode 100644 src/reporter-plugins/docblock/extractJsdocAbove.test.ts delete mode 100644 src/reporter-plugins/docblock/extractJsdocAbove.ts delete mode 100644 src/reporter-plugins/docblock/index.ts delete mode 100644 src/reporter-plugins/docblock/parseJsdoc.ts delete mode 100644 src/reporter-plugins/index.ts delete mode 100644 src/reporter-plugins/manifest/ManifestResolver.test.ts delete mode 100644 src/reporter-plugins/manifest/ManifestResolver.ts delete mode 100644 src/reporter-plugins/manifest/index.ts delete mode 100644 src/reporter-plugins/remark.ts delete mode 100644 src/reporter-plugins/source-code/detectSourceLanguage.ts delete mode 100644 src/reporter-plugins/source-code/index.ts delete mode 100644 src/reporter-plugins/source-code/sourceCodePlugin.ts delete mode 100644 src/reporter-plugins/source-code/utils/FileNavigatorCache.ts delete mode 100644 src/reporter-plugins/source-code/utils/LineNavigator.test.ts delete mode 100644 src/reporter-plugins/source-code/utils/LineNavigator.ts delete mode 100644 src/reporter-plugins/source-code/utils/extractTypeScriptCode.ts delete mode 100644 src/reporter-plugins/source-code/utils/extractTypescriptAST.ts delete mode 100644 src/reporter-plugins/source-code/utils/importTypeScript.ts delete mode 100644 src/reporter-plugins/source-code/utils/index.ts rename src/runtime/{AllureRuntime.ts => AllureRuntimeImplementation.ts} (80%) rename src/runtime/__snapshots__/{AllureRuntime.test.ts.snap => runtime.test.ts.snap} (100%) rename src/runtime/{AllureRuntime.test.ts => runtime.test.ts} (87%) diff --git a/index.d.ts b/index.d.ts index 92029121..553df588 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,41 +1,12 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ -import type { AggregatedResult, Config, Test, TestCaseResult, TestContext, TestResult } from '@jest/reporters'; +import type {AggregatedResult, Config, TestCaseResult, TestResult} from '@jest/reporters'; import JestMetadataReporter from 'jest-metadata/reporter'; declare module 'jest-allure2-reporter' { // region Config - /** - * Configuration options for the `jest-allure2-reporter` package. - * These options are used in your Jest config. - * - * @example - * /** @type {import('@jest/types').Config.InitialOptions} *\/ - * module.exports = { - * // ... - * reporters: [ - * 'default', - * ['jest-allure2-reporter', { - * resultsDir: 'allure-results', - * testCase: {}, - * environment: () => process.env, - * executor: ({ value }) => value ?? ({ - * type: process.platform, - * name: require('os').hostname() - * }), - * categories: ({ value }) => [ - * ...value, - * { - * name: 'Custom defect category', - * messageRegex: '.*Custom defect message.*', - * }, - * ], - * }], - * ], - * }; - */ - export type ReporterOptions = { + export interface ReporterOptions extends ReporterOptionsAugmentation { /** * Overwrite the results directory if it already exists. * @default true @@ -77,6 +48,16 @@ declare module 'jest-allure2-reporter' { * Configures the executor information that will be reported. */ executor?: ExecutorInfo | ExecutorCustomizer; + /** + * Customize extractor helpers object to use later in the customizers. + */ + helpers?: Partial; + /** + * Customize how to report test runs (sessions) as pseudo-test cases. + * This is normally used to report broken global setup and teardown hooks, + * and to provide additional information about the test run. + */ + testRun?: Partial; /** * Customize how to report test files as pseudo-test cases. * This is normally used to report broken test files, so that you can be aware of them, @@ -91,14 +72,9 @@ declare module 'jest-allure2-reporter' { * Customize how individual test steps are reported. */ testStep?: Partial; - /** - * Plugins to extend the reporter functionality. - * Via plugins, you can extend the context used by customizers. - */ - plugins?: PluginDeclaration[]; - }; + } - export type ReporterConfig = { + export interface ReporterConfig extends ReporterConfigAugmentation { overwrite: boolean; resultsDir: string; injectGlobals: boolean; @@ -106,13 +82,23 @@ declare module 'jest-allure2-reporter' { categories: CategoriesCustomizer; environment: EnvironmentCustomizer; executor: ExecutorCustomizer; - testFile: ResolvedTestFileCustomizer; - testCase: ResolvedTestCaseCustomizer; - testStep: ResolvedTestStepCustomizer; - plugins: Promise; - }; + helpers: TestRunExtractor; + testRun: Required & { + labels: TestRunExtractor; + links: TestRunExtractor; + }; + testFile: Required & { + labels: TestFileExtractor; + links: TestFileExtractor; + }; + testCase: Required & { + labels: TestCaseExtractor; + links: TestCaseExtractor; + }; + testStep: Required; + } - export type AttachmentsOptions = { + export interface AttachmentsOptions { /** * Defines a subdirectory within the {@link ReporterOptions#resultsDir} where attachments will be stored. * Use absolute path if you want to store attachments outside the {@link ReporterOptions#resultsDir} directory. @@ -135,7 +121,7 @@ declare module 'jest-allure2-reporter' { * @see {@link AllureRuntime#createContentAttachment} */ contentHandler?: BuiltinContentAttachmentHandler | string; - }; + } /** @see {@link AttachmentsOptions#fileHandler} */ export type BuiltinFileAttachmentHandler = 'copy' | 'move' | 'ref'; @@ -189,6 +175,10 @@ declare module 'jest-allure2-reporter' { * Extractor to omit test file cases from the report. */ hidden: TestCaseExtractor; + /** + * Extractor to augment the test case context and the $ helper object. + */ + $: TestFileExtractor; /** * Test case ID extractor to fine-tune Allure's history feature. * @example ({ package, file, test }) => `${package.name}:${file.path}:${test.fullName}` @@ -278,6 +268,58 @@ declare module 'jest-allure2-reporter' { parameters: TestCaseExtractor; } + /** + * Global customizations for how test steps are reported, e.g. + * beforeAll, beforeEach, afterEach, afterAll hooks and custom steps. + */ + export interface TestStepCustomizer { + /** + * Extractor to omit test steps from the report. + */ + hidden: TestStepExtractor; + /** + * Extractor to augment the test step context and the $ helper object. + */ + $: TestFileExtractor; + /** + * Extractor for the step name. + * @example ({ value }) => value.replace(/(before|after)(Each|All)/, (_, p1, p2) => p1 + ' ' + p2.toLowerCase()) + */ + name: TestStepExtractor; + /** + * Extractor for the test step start timestamp. + */ + start: TestStepExtractor; + /** + * Extractor for the test step stop timestamp. + */ + stop: TestStepExtractor; + /** + * Extractor for the test step stage. + * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ + * TODO: add example + */ + stage: TestStepExtractor; + /** + * Extractor for the test step status. + * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ + * @example ({ value }) => value === 'broken' ? 'failed' : value + */ + status: TestStepExtractor; + /** + * Extractor for the test step status details. + */ + statusDetails: TestStepExtractor; + /** + * Customize step or test step attachments. + */ + attachments: TestStepExtractor; + /** + * Customize step or test step parameters. + */ + parameters: TestStepExtractor; + } + /** * Global customizations for how test files are reported (as pseudo-test cases). */ @@ -286,6 +328,10 @@ declare module 'jest-allure2-reporter' { * Extractor to omit test file cases from the report. */ hidden: TestFileExtractor; + /** + * Extractor to augment the test file context and the $ helper object. + */ + $: TestFileExtractor; /** * Test file ID extractor to fine-tune Allure's history feature. * @default ({ filePath }) => filePath.join('/') @@ -373,67 +419,104 @@ declare module 'jest-allure2-reporter' { parameters: TestFileExtractor; } - export type ResolvedTestFileCustomizer = Required & { - labels: TestFileExtractor; - links: TestFileExtractor; - }; - - export type ResolvedTestCaseCustomizer = Required & { - labels: TestCaseExtractor; - links: TestCaseExtractor; - }; - - export type ResolvedTestStepCustomizer = Required; - - export interface TestStepCustomizer { + /** + * Global customizations for how test runs (sessions) are reported (as pseudo-test cases). + */ + export interface TestRunCustomizer { /** - * Extractor to omit test steps from the report. + * Extractor to omit pseudo-test cases for test runs from the report. */ - hidden: TestStepExtractor; + hidden: TestRunExtractor; /** - * Extractor for the step name. - * @example ({ value }) => value.replace(/(before|after)(Each|All)/, (_, p1, p2) => p1 + ' ' + p2.toLowerCase()) + * Extractor to augment the test run context and the $ helper object. */ - name: TestStepExtractor; + $: TestRunExtractor; /** - * Extractor for the test step start timestamp. + * Test run ID extractor to fine-tune Allure's history feature. + * @default () => process.argv.slice(2).join(' ') */ - start: TestStepExtractor; + historyId: TestRunExtractor; /** - * Extractor for the test step stop timestamp. + * Extractor for test run name + * @default () => '(test run)' */ - stop: TestStepExtractor; + name: TestRunExtractor; /** - * Extractor for the test step stage. - * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ - * TODO: add example + * Extractor for the full test run name + * @default () => process.argv.slice(2).join(' ') */ - stage: TestStepExtractor; + fullName: TestRunExtractor; /** - * Extractor for the test step status. - * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ - * @example ({ value }) => value === 'broken' ? 'failed' : value + * Extractor for the test run start timestamp. */ - status: TestStepExtractor; + start: TestRunExtractor; /** - * Extractor for the test step status details. + * Extractor for the test run stop timestamp. */ - statusDetails: TestStepExtractor; + stop: TestRunExtractor; /** - * Customize step or test step attachments. + * Extractor for the test run description. + * Use this to provide additional information about the test run, + * which is not covered by the default Allure reporter capabilities. */ - attachments: TestStepExtractor; + description: TestRunExtractor; /** - * Customize step or test step parameters. + * Extractor for the test run description in HTML format. + * @see {@link TestRunCustomizer#description} */ - parameters: TestStepExtractor; + descriptionHtml: TestRunExtractor; + /** + * Extractor for the test run stage. + * 'interrupted' is used for failures with `--bail` enabled. + * Otherwise, 'finished' is used. + */ + stage: TestRunExtractor; + /** + * Extractor for the test run status. + * Either 'passed' or 'failed'. + */ + status: TestRunExtractor; + /** + * Extractor for the test file status details. + */ + statusDetails: TestRunExtractor; + /** + * Customize Allure labels for the pseudo-test case representing the test run. + */ + labels: + | TestRunExtractor + | Record< + LabelName | string, + string | string[] | TestRunExtractor + >; + /** + * Customize Allure links for the pseudo-test case representing the test run. + */ + links: + | TestRunExtractor + | Record>; + /** + * Customize test run attachments. + */ + attachments: TestRunExtractor; + /** + * Customize test run parameters. + */ + parameters: TestRunExtractor; } - export type EnvironmentCustomizer = GlobalExtractor>; + /** + * Override or add more helper functions to the default extractor helpers. + */ + export interface ExtractorHelpersCustomizer { + [key: keyof ExtractorHelpers]: ExtractorHelperExtractor; + } - export type ExecutorCustomizer = GlobalExtractor; + export type EnvironmentCustomizer = TestRunExtractor>; - export type CategoriesCustomizer = GlobalExtractor; + export type ExecutorCustomizer = TestRunExtractor; + + export type CategoriesCustomizer = TestRunExtractor; export type Extractor< T = unknown, @@ -441,9 +524,15 @@ declare module 'jest-allure2-reporter' { R = T, > = (context: Readonly) => R | undefined | Promise; - export type GlobalExtractor = Extractor< + export type ExtractorHelperExtractor = Extractor< + ExtractorHelpers[K], + ExtractorHelpers, + ExtractorHelpers[K] + >; + + export type TestRunExtractor = Extractor< T, - GlobalExtractorContext, + TestRunExtractorContext, R >; @@ -466,7 +555,7 @@ declare module 'jest-allure2-reporter' { >; export interface ExtractorContext { - value: T | undefined; + readonly value: T | undefined | Promise; } export interface GlobalExtractorContext @@ -477,6 +566,12 @@ declare module 'jest-allure2-reporter' { config: ReporterConfig; } + export interface TestRunExtractorContext + extends GlobalExtractorContext, + TestRunExtractorContextAugmentation { + aggregatedResult: AggregatedResult; + } + export interface TestFileExtractorContext extends GlobalExtractorContext, TestFileExtractorContextAugmentation { @@ -499,17 +594,74 @@ declare module 'jest-allure2-reporter' { } export interface ExtractorHelpers extends ExtractorHelpersAugmentation { - extractSourceCode(metadata: AllureTestItemMetadata): Promise; - extractSourceCodeWithSteps(metadata: AllureTestItemMetadata): Promise; - sourceCode2Markdown(sourceCode: Partial | undefined): string; + /** + * Extracts the source code of the current test case, test step or a test file. + * Pass `true` as the second argument to extract source code recursively from all steps. + * + * @example + * ({ $, testFileMetadata }) => $.extractSourceCode(testFileMetadata) + * @example + * ({ $, testCaseMetadata }) => $.extractSourceCode(testCaseMetadata, true) + */ + extractSourceCode: ExtractorSourceCodeHelper; + /** + * Extracts the executor information from the current environment. + * Pass `true` as the argument to include local executor information. + * By default, supports GitHub Actions and Buildkite. + * + * @example + * ({ $ }) => $.getExecutorInfo() + * @example + * ({ $ }) => $.getExecutorInfo(true) + */ + getExecutorInfo: ExtractorExecutorInfoHelper; + /** + * Extracts the manifest of the current project or a specific package. + * Pass a callback to extract specific data from the manifest – this way you can omit async/await. + * + * @example + * ({ $ }) => $.manifest(m => m.version) + * @example + * ({ $ }) => $.manifest('jest', jest => jest.version) + * @example + * ({ $ }) => (await $.manifest()).version + * @example + * ({ $ }) => (await $.manifest('jest')).version + */ + manifest: ExtractorManifestHelper; markdown2html(markdown: string): Promise; + sourceCode2Markdown(sourceCode: Partial | undefined): string; + stripAnsi: StripAnsiHelper; + } + + export interface ExtractorSourceCodeHelper { + (metadata: AllureTestItemMetadata): Promise; + (metadata: AllureTestItemMetadata, recursive: true): Promise; + } + + export interface ExtractorExecutorInfoHelper { + (): Promise; + (includeLocal: true): Promise; + } + + export interface ExtractorManifestHelper { + (): Promise | undefined>; + (packageName: string): Promise | undefined>; + (callback: ExtractorManifestHelperCallback): Promise; + (packageName: string, callback: ExtractorManifestHelperCallback): Promise; } - export type ExtractorHelperSourceCode = { + export type ExtractorManifestHelperCallback = (manifest: Record) => T; + + export interface ExtractorHelperSourceCode { fileName: string; code: string; language: string; - }; + } + + export interface StripAnsiHelper { + (text: R): R; + } // endregion @@ -625,69 +777,43 @@ declare module 'jest-allure2-reporter' { // endregion - // region Plugins + // region Extensibility + + export interface ReporterOptionsAugmentation { + // Use to extend ReporterOptions + } + + export interface ReporterConfigAugmentation { + // Use to extend ReporterConfig + } export interface ExtractorHelpersAugmentation { - // This may be extended by plugins + // Use to extend ExtractorHelpers } export interface GlobalExtractorContextAugmentation { - // This may be extended by plugins + // Use to extend GlobalExtractorContext + } + + export interface TestRunExtractorContextAugmentation { + // Use to extend TestRunExtractorContext } export interface TestFileExtractorContextAugmentation { - // This may be extended by plugins + // Use to extend TestFileExtractorContext } export interface TestCaseExtractorContextAugmentation { - // This may be extended by plugins + // Use to extend TestCaseExtractorContext } export interface TestStepExtractorContextAugmentation { - // This may be extended by plugins - } - - export type PluginDeclaration = - | PluginReference - | [PluginReference, Record]; - - export type PluginReference = string | PluginConstructor; - - export type PluginConstructor = ( - options: Record, - context: PluginContext, - ) => Plugin | Promise; - - export type PluginContext = Readonly<{ - globalConfig: Readonly; - }>; - - export interface Plugin { - /** Also used to deduplicate plugins if they are declared multiple times. */ - readonly name: string; - - /** Optional method for deduplicating plugins. Return the instance which you want to keep. */ - extend?(previous: Plugin): Plugin; - - helpers?(helpers: Partial): void | Promise; - - /** Allows to modify the raw metadata before it's processed by the reporter. [UNSTABLE!] */ - postProcessMetadata?(context: PluginHookContexts['postProcessMetadata']): void | Promise; + // Use to extend TestStepExtractorContext } - export type PluginHookContexts = { - helpers: Partial; - postProcessMetadata: { - $: Readonly; - metadata: AllureTestItemMetadata; - }; - }; - - export type PluginHookName = keyof PluginHookContexts; - // endregion - //region Allure types + //region Allure Vendor types export interface Attachment { name: string; diff --git a/src/api/index.ts b/src/api/index.ts index ec5bf0dd..2ee55973 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,10 +1,10 @@ import realm from '../realms'; -import type { IAllureRuntime } from '../runtime'; +import type { AllureRuntime } from '../runtime'; export * from './annotations'; export * from './decorators'; -export const allure = realm.runtime as IAllureRuntime; +export const allure = realm.runtime as AllureRuntime; export type { AllureRuntimePluginCallback, @@ -18,7 +18,7 @@ export type { FileAttachmentContext, FileAttachmentHandler, FileAttachmentOptions, - IAllureRuntime, + AllureRuntime, MIMEInferer, MIMEInfererContext, ParameterOrString, diff --git a/src/helpers/index.ts b/src/helpers/index.ts new file mode 100644 index 00000000..65eae862 --- /dev/null +++ b/src/helpers/index.ts @@ -0,0 +1 @@ +export * from './stripAnsi'; diff --git a/src/helpers/stripAnsi.test.ts b/src/helpers/stripAnsi.test.ts new file mode 100644 index 00000000..9976729c --- /dev/null +++ b/src/helpers/stripAnsi.test.ts @@ -0,0 +1,20 @@ +import { stripAnsi } from './stripAnsi'; + +describe('stripAnsi', () => { + test.each` + description | input | expected + ${'string with ANSI escape codes'} | ${'Hello \u001B[31mWorld\u001B[0m'} | ${'Hello World'} + ${'string without ANSI escape codes'} | ${'Hello World'} | ${'Hello World'} + ${'array of strings'} | ${['Hello \u001B[31mWorld\u001B[0m', 'Foo \u001B[32mBar\u001B[0m']} | ${['Hello World', 'Foo Bar']} + ${'array of mixed types'} | ${['Hello \u001B[31mWorld\u001B[0m', 42, true]} | ${['Hello World', 42, true]} + ${'object with string values'} | ${{ name: 'John \u001B[31mDoe\u001B[0m', age: 30 }} | ${{ name: 'John Doe', age: 30 }} + ${'object with mixed value types'} | ${{ name: 'John \u001B[31mDoe\u001B[0m', age: 30, active: true }} | ${{ name: 'John Doe', age: 30, active: true }} + ${'nested object'} | ${{ person: { name: 'John \u001B[31mDoe\u001B[0m', age: 30 }, active: true }} | ${{ person: { name: 'John Doe', age: 30 }, active: true }} + ${'non-string primitive'} | ${42} | ${42} + ${'boolean'} | ${true} | ${true} + ${'null'} | ${null} | ${null} + ${'undefined'} | ${undefined} | ${undefined} + `('should handle $description', ({ input, expected }) => { + expect(stripAnsi(input)).toEqual(expected); + }); +}); diff --git a/src/helpers/stripAnsi.ts b/src/helpers/stripAnsi.ts new file mode 100644 index 00000000..cc498f8d --- /dev/null +++ b/src/helpers/stripAnsi.ts @@ -0,0 +1,23 @@ +import stripAnsiString from 'strip-ansi'; + +export function stripAnsi(value: T): T { + if (typeof value === 'string') { + return stripAnsiString(value) as T; + } + + if (Array.isArray(value)) { + return value.map(stripAnsi) as T; + } + + if (typeof value === 'object' && value !== null) { + return Object.entries(value).reduce( + (object, [key, value_]) => { + object[key] = stripAnsi(value_); + return object; + }, + {} as Record, + ) as T; + } + + return value; +} diff --git a/src/options/compose-options/aggregateHelpersCustomizers.ts b/src/options/compose-options/aggregateHelpersCustomizers.ts new file mode 100644 index 00000000..7dfb45ca --- /dev/null +++ b/src/options/compose-options/aggregateHelpersCustomizers.ts @@ -0,0 +1,8 @@ +import type { ExtractorHelpers, TestRunExtractor } from 'jest-allure2-reporter'; + +export function aggregateHelpersCustomizers( + a: any, +): TestRunExtractor { + // TODO: Implement + return a; +} diff --git a/src/options/utils/aggregateLabelCustomizers.ts b/src/options/compose-options/aggregateLabelCustomizers.ts similarity index 85% rename from src/options/utils/aggregateLabelCustomizers.ts rename to src/options/compose-options/aggregateLabelCustomizers.ts index fb169f20..a81b6f45 100644 --- a/src/options/utils/aggregateLabelCustomizers.ts +++ b/src/options/compose-options/aggregateLabelCustomizers.ts @@ -9,10 +9,12 @@ import type { Label } from 'jest-allure2-reporter'; import { flatMapAsync } from '../../utils/flatMapAsync'; import { asArray } from '../../utils'; - -import { asExtractor } from './asExtractor'; +import { asExtractor } from '../extractors'; type Customizer = TestFileCustomizer | TestCaseCustomizer; + +const constant = (value: T) => () => value; + export function aggregateLabelCustomizers( labels: C['labels'] | undefined, ): Extractor | undefined { @@ -22,7 +24,7 @@ export function aggregateLabelCustomizers( const extractors = Object.keys(labels).reduce( (accumulator, key) => { - const extractor = asExtractor(labels[key]) as Extractor; + const extractor = asExtractor(labels[key]); if (extractor) { accumulator[key] = extractor; } @@ -43,8 +45,9 @@ export function aggregateLabelCustomizers( {} as Record, ); - if (context.value) { - for (const label of context.value) { + const baseValue = await context.value; + if (baseValue) { + for (const label of baseValue) { if (found[label.name]) { found[label.name].push(label.value); } else { @@ -59,7 +62,7 @@ export function aggregateLabelCustomizers( const extractor = extractors[name]; const aContext: ExtractorContext = { ...context, - value: asArray(found[name]), + base: constant(asArray(found[name])), }; const extracted = await extractor(aContext); const value = asArray(extracted); diff --git a/src/options/compose-options/aggregateLinkCustomizers.ts b/src/options/compose-options/aggregateLinkCustomizers.ts new file mode 100644 index 00000000..a6c4d465 --- /dev/null +++ b/src/options/compose-options/aggregateLinkCustomizers.ts @@ -0,0 +1,37 @@ +import type { + Extractor, + ExtractorContext, + TestRunCustomizer, + TestFileCustomizer, + TestCaseCustomizer, +} from 'jest-allure2-reporter'; +import type { Link } from 'jest-allure2-reporter'; + +import { isPromiseLike } from '../../utils'; + +type Customizer = TestFileCustomizer | TestCaseCustomizer | TestRunCustomizer; + +export function aggregateLinkCustomizers( + links: C['links'] | undefined, +): Extractor | undefined { + if (!links || typeof links === 'function') { + return links as Extractor | undefined; + } + + return async (context: ExtractorContext) => { + const value = isPromiseLike(context.value) + ? await context.value + : context.value; + + const promisedLinks = + value?.map(async (link) => { + const extractor = links[link.type ?? '']; + return extractor + ? await extractor({ ...context, value: link } as any) + : link; + }) ?? []; + + const filteredLinks = await Promise.all(promisedLinks); + return filteredLinks.filter(Boolean); + }; +} diff --git a/src/options/compose-options/index.ts b/src/options/compose-options/index.ts index 0982b878..cd8bedb2 100644 --- a/src/options/compose-options/index.ts +++ b/src/options/compose-options/index.ts @@ -1,49 +1,5 @@ -import type { PluginContext } from 'jest-allure2-reporter'; -import type { - ReporterOptions, - ReporterConfig, - TestStepCustomizer, -} from 'jest-allure2-reporter'; +export { aggregateLabelCustomizers } from './aggregateLabelCustomizers'; -import { asExtractor, composeExtractors } from '../utils'; +export { aggregateLinkCustomizers } from './aggregateLinkCustomizers'; -import { composeAttachments } from './attachments'; -import { composePlugins } from './plugins'; -import { composeTestCaseCustomizers } from './testCase'; -import { composeTestFileCustomizers } from './testFile'; -import { composeTestStepCustomizers } from './testStep'; - -export function composeOptions( - context: PluginContext, - base: ReporterConfig, - custom: ReporterOptions | undefined, -): ReporterConfig { - if (!custom) { - return base; - } - - return { - ...custom, - - overwrite: custom.overwrite ?? base.overwrite, - resultsDir: custom.resultsDir ?? base.resultsDir, - injectGlobals: custom.injectGlobals ?? base.injectGlobals, - attachments: composeAttachments(base.attachments, custom.attachments), - testCase: composeTestCaseCustomizers(base.testCase, custom.testCase), - testFile: composeTestFileCustomizers(base.testFile, custom.testFile), - testStep: composeTestStepCustomizers( - base.testStep as TestStepCustomizer, - custom.testStep, - ), - environment: composeExtractors( - asExtractor(custom.environment), - base.environment, - ), - executor: composeExtractors(asExtractor(custom.executor), base.executor), - categories: composeExtractors( - asExtractor(custom.categories), - base.categories, - ), - plugins: composePlugins(context, base.plugins, custom.plugins), - }; -} +export { reporterOptions } from './reporterOptions'; diff --git a/src/options/compose-options/plugins.ts b/src/options/compose-options/plugins.ts deleted file mode 100644 index b8150c0d..00000000 --- a/src/options/compose-options/plugins.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { - Plugin, - PluginContext, - PluginDeclaration, -} from 'jest-allure2-reporter'; - -import { resolvePlugins } from '../utils'; - -export async function composePlugins( - context: PluginContext, - basePlugins: Promise, - customPlugins: PluginDeclaration[] | undefined, -): Promise { - if (!customPlugins) { - return basePlugins; - } - - const [base, custom] = await Promise.all([ - basePlugins, - resolvePlugins(context, customPlugins), - ]); - - const result: Plugin[] = []; - const indices: Record = {}; - - // eslint-disable-next-line unicorn/prefer-spread - for (const plugin of base.concat(custom)) { - const index = indices[plugin.name]; - if (index === undefined) { - indices[plugin.name] = result.push(plugin) - 1; - } else { - const previous = result[index]; - const extended = plugin?.extend?.(previous) ?? plugin; - result[index] = extended; - } - } - - return result; -} diff --git a/src/options/compose-options/reporterOptions.ts b/src/options/compose-options/reporterOptions.ts new file mode 100644 index 00000000..07d89cb1 --- /dev/null +++ b/src/options/compose-options/reporterOptions.ts @@ -0,0 +1,50 @@ +import type { + ReporterOptions, + ReporterConfig, + TestStepCustomizer, +} from 'jest-allure2-reporter'; + +import { asExtractor, composeExtractors } from '../extractors'; + +import { composeAttachments } from './attachments'; +import { composeTestCaseCustomizers } from './testCase'; +import { composeTestStepCustomizers } from './testStep'; +import { aggregateHelpersCustomizers } from './aggregateHelpersCustomizers'; + +export function reporterOptions( + base: ReporterConfig, + custom: ReporterOptions | undefined, +): ReporterConfig { + if (!custom) { + return base; + } + + return { + ...custom, + + overwrite: custom.overwrite ?? base.overwrite, + resultsDir: custom.resultsDir ?? base.resultsDir, + injectGlobals: custom.injectGlobals ?? base.injectGlobals, + attachments: composeAttachments(base.attachments, custom.attachments), + categories: composeExtractors( + asExtractor(custom.categories), + base.categories, + ), + environment: composeExtractors( + asExtractor(custom.environment), + base.environment, + ), + executor: composeExtractors(asExtractor(custom.executor), base.executor), + helpers: composeExtractors( + aggregateHelpersCustomizers(custom.helpers), + base.helpers, + ), + testRun: composeTestCaseCustomizers(base.testRun, custom.testRun), + testCase: composeTestCaseCustomizers(base.testCase, custom.testCase), + testFile: composeTestCaseCustomizers(base.testFile, custom.testFile), + testStep: composeTestStepCustomizers( + base.testStep as TestStepCustomizer, + custom.testStep, + ), + }; +} diff --git a/src/options/compose-options/testCase.ts b/src/options/compose-options/testCase.ts index 8dabc0dc..aa39f494 100644 --- a/src/options/compose-options/testCase.ts +++ b/src/options/compose-options/testCase.ts @@ -1,24 +1,33 @@ -import type { - ResolvedTestCaseCustomizer, - TestCaseCustomizer, -} from 'jest-allure2-reporter'; +import type { ReporterConfig, TestCaseCustomizer } from 'jest-allure2-reporter'; -import { - aggregateLabelCustomizers, - aggregateLinkCustomizers, - composeExtractors, -} from '../utils'; +import { composeExtractors } from '../extractors'; +import { aggregateLabelCustomizers } from './aggregateLabelCustomizers'; +import { aggregateLinkCustomizers } from './aggregateLinkCustomizers'; + +export function composeTestCaseCustomizers( + base: ReporterConfig['testRun'], + custom: Partial | undefined, +): typeof base; +export function composeTestCaseCustomizers( + base: ReporterConfig['testFile'], + custom: Partial | undefined, +): typeof base; +export function composeTestCaseCustomizers( + base: ReporterConfig['testCase'], + custom: Partial | undefined, +): typeof base; export function composeTestCaseCustomizers( - base: ResolvedTestCaseCustomizer, + base: ReporterConfig['testRun' | 'testFile' | 'testCase'], custom: Partial | undefined, -): ResolvedTestCaseCustomizer { +): typeof base { if (!custom) { return base; } return { hidden: composeExtractors(custom.hidden, base.hidden), + $: composeExtractors(custom.$, base.$), historyId: composeExtractors(custom.historyId, base.historyId), fullName: composeExtractors(custom.fullName, base.fullName), name: composeExtractors(custom.name, base.name), diff --git a/src/options/compose-options/testFile.ts b/src/options/compose-options/testFile.ts deleted file mode 100644 index 4c70c07d..00000000 --- a/src/options/compose-options/testFile.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { - ResolvedTestFileCustomizer, - TestFileCustomizer, -} from 'jest-allure2-reporter'; - -import { composeExtractors } from '../utils'; - -import { composeTestCaseCustomizers } from './testCase'; - -export function composeTestFileCustomizers( - base: ResolvedTestFileCustomizer, - custom: Partial | undefined, -): ResolvedTestFileCustomizer { - if (!custom) { - return base; - } - - return { - ...(composeTestCaseCustomizers(base, custom) as any), - hidden: composeExtractors(custom.hidden, base.hidden), - }; -} diff --git a/src/options/compose-options/testStep.ts b/src/options/compose-options/testStep.ts index 338b7601..e457dea6 100644 --- a/src/options/compose-options/testStep.ts +++ b/src/options/compose-options/testStep.ts @@ -1,17 +1,18 @@ -import type { TestStepCustomizer } from 'jest-allure2-reporter'; +import type { ReporterConfig, TestStepCustomizer } from 'jest-allure2-reporter'; -import { composeExtractors } from '../utils'; +import { composeExtractors } from '../extractors'; export function composeTestStepCustomizers( - base: TestStepCustomizer, + base: ReporterConfig['testStep'], custom: Partial | undefined, -): TestStepCustomizer { +): typeof base { if (!custom) { return base; } return { hidden: composeExtractors(custom.hidden, base.hidden), + $: composeExtractors(custom.$, base.$), name: composeExtractors(custom.name, base.name), stage: composeExtractors(custom.stage, base.stage), start: composeExtractors(custom.start, base.start), diff --git a/src/options/default-options/executor.ts b/src/options/default-options/executor.ts deleted file mode 100644 index fc633f22..00000000 --- a/src/options/default-options/executor.ts +++ /dev/null @@ -1,57 +0,0 @@ -import os from 'node:os'; - -import type { ExecutorCustomizer } from 'jest-allure2-reporter'; -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -export function executor(): ExecutorCustomizer { - if (process.env.GITHUB_ACTIONS) return githubActions; - if (process.env.BUILDKITE) return buildkite; - - return local; -} - -function githubActions(): ExecutorInfo { - const { - ALLURE_GITHUB_URL, - GITHUB_RUN_ATTEMPT, - GITHUB_RUN_ID, - GITHUB_RUN_NUMBER, - GITHUB_JOB, - RUNNER_NAME, - } = process.env; - - const runnerName = RUNNER_NAME || 'GitHub Actions'; - - return { - name: `${runnerName} (${getOsDetails()})`, - type: 'github', - buildUrl: ALLURE_GITHUB_URL, - buildOrder: Number(GITHUB_RUN_NUMBER) * 10 + Number(GITHUB_RUN_ATTEMPT), - buildName: `${GITHUB_RUN_ID}#${GITHUB_JOB}`, - }; -} - -function buildkite(): ExecutorInfo { - const { BUILDKITE_AGENT_NAME, BUILDKITE_BUILD_URL, BUILDKITE_BUILD_NUMBER } = - process.env; - const agentName = BUILDKITE_AGENT_NAME || 'Buildkite'; - - return { - name: `${agentName} (${getOsDetails()})`, - type: 'buildkite', - buildUrl: BUILDKITE_BUILD_URL, - buildOrder: Number(BUILDKITE_BUILD_NUMBER), - buildName: `#${BUILDKITE_BUILD_NUMBER}`, - }; -} - -function local(): ExecutorInfo { - return { - name: `${os.hostname()} (${getOsDetails()})`, - type: `${os.platform()}-${os.arch()}`, - }; -} - -function getOsDetails(): string { - return `${os.type()} ${os.release()}/${os.arch()}`; -} diff --git a/src/options/default-options/helpers.ts b/src/options/default-options/helpers.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/options/default-options/index.ts b/src/options/default-options/index.ts index eeada8f1..aa8ccd9e 100644 --- a/src/options/default-options/index.ts +++ b/src/options/default-options/index.ts @@ -5,15 +5,15 @@ import type { } from 'jest-allure2-reporter'; import { categories } from './categories'; -import { defaultPlugins } from './plugins'; -import { testCase } from './testCase'; +import { helpers } from './helpers'; +import { testRun } from './testRun'; import { testFile } from './testFile'; +import { testCase } from './testCase'; import { testStep } from './testStep'; -import { executor } from './executor'; const identity = (context: ExtractorContext) => context.value; -export function defaultOptions(context: PluginContext): ReporterConfig { +export function defaultOptions(): ReporterConfig { return { overwrite: true, resultsDir: 'allure-results', @@ -23,12 +23,13 @@ export function defaultOptions(context: PluginContext): ReporterConfig { contentHandler: 'write', fileHandler: 'ref', }, + helpers, + testRun, testFile, testCase, testStep, categories, environment: identity, - executor: executor(), - plugins: defaultPlugins(context), + executor: ({ $ }) => $.getExecutorInfo(true), }; } diff --git a/src/options/default-options/testCase.ts b/src/options/default-options/testCase.ts index 4efc175a..40d77441 100644 --- a/src/options/default-options/testCase.ts +++ b/src/options/default-options/testCase.ts @@ -4,26 +4,27 @@ import type { TestCaseResult } from '@jest/reporters'; import type { ExtractorContext, Label, - ResolvedTestCaseCustomizer, + ReporterConfig, Stage, Status, TestCaseCustomizer, TestCaseExtractorContext, } from 'jest-allure2-reporter'; -import { - aggregateLabelCustomizers, - composeExtractors, - stripStatusDetails, -} from '../utils'; +import { composeExtractors } from '../extractors'; import { getStatusDetails } from '../../utils'; +import { aggregateLabelCustomizers } from '../compose-options'; const identity = (context: ExtractorContext) => context.value; -const last = (context: ExtractorContext) => context.value?.at(-1); +const last = async (context: ExtractorContext) => { + const value = await context.value; + return value?.at(-1); +}; const all = identity; -export const testCase: ResolvedTestCaseCustomizer = { +export const testCase: ReporterConfig['testCase'] = { hidden: () => false, + $: ({ $ }) => $, historyId: ({ testCase, testCaseMetadata }) => testCaseMetadata.historyId ?? testCase.fullName, name: ({ testCase, testCaseMetadata }) => @@ -32,7 +33,7 @@ export const testCase: ResolvedTestCaseCustomizer = { testCaseMetadata.fullName ?? testCase.fullName, description: async ({ $, testCaseMetadata }) => { const text = testCaseMetadata.description?.join('\n\n') ?? ''; - const codes = await $.extractSourceCodeWithSteps(testCaseMetadata); + const codes = await $.extractSourceCode(testCaseMetadata, true); const snippets = codes.map($.sourceCode2Markdown); return [text, ...snippets].filter(Boolean).join('\n\n'); }, @@ -46,8 +47,8 @@ export const testCase: ResolvedTestCaseCustomizer = { testCaseMetadata.stage ?? getTestCaseStage(testCase), status: ({ testCase, testCaseMetadata }) => testCaseMetadata.status ?? getTestCaseStatus(testCase), - statusDetails: ({ testCase, testCaseMetadata }) => - stripStatusDetails( + statusDetails: ({ $, testCase, testCaseMetadata }) => + $.stripAnsi( testCaseMetadata.statusDetails ?? getStatusDetails((testCase.failureMessages ?? []).join('\n')), ), diff --git a/src/options/default-options/testFile.ts b/src/options/default-options/testFile.ts index cf1b2157..6fa34ed7 100644 --- a/src/options/default-options/testFile.ts +++ b/src/options/default-options/testFile.ts @@ -4,24 +4,22 @@ import type { ExtractorContext, Label, Link, - ResolvedTestFileCustomizer, + ReporterConfig, TestCaseCustomizer, TestFileExtractorContext, } from 'jest-allure2-reporter'; import { getStatusDetails } from '../../utils'; -import { - aggregateLabelCustomizers, - composeExtractors, - stripStatusDetails, -} from '../utils'; +import { aggregateLabelCustomizers } from '../compose-options'; +import { composeExtractors } from '../extractors'; const identity = (context: ExtractorContext) => context.value; const last = (context: ExtractorContext) => context.value?.at(-1); const all = identity; -export const testFile: ResolvedTestFileCustomizer = { +export const testFile: ReporterConfig['testFile'] = { hidden: ({ testFile }) => !testFile.testExecError, + $: ({ $ }) => $, historyId: ({ filePath }) => filePath.join('/'), name: ({ filePath }) => filePath.join(path.sep), fullName: ({ globalConfig, testFile }) => @@ -39,8 +37,8 @@ export const testFile: ResolvedTestFileCustomizer = { testFile.testExecError == null ? 'finished' : 'interrupted', status: ({ testFile }) => testFile.testExecError == null ? 'passed' : 'broken', - statusDetails: ({ testFile }) => - stripStatusDetails(getStatusDetails(testFile.testExecError)), + statusDetails: ({ $, testFile }) => + $.stripAnsi(getStatusDetails(testFile.testExecError)), attachments: ({ testFileMetadata }) => testFileMetadata.attachments ?? [], parameters: ({ testFileMetadata }) => testFileMetadata.parameters ?? [], labels: composeExtractors>( diff --git a/src/options/default-options/testRun.ts b/src/options/default-options/testRun.ts new file mode 100644 index 00000000..b64701db --- /dev/null +++ b/src/options/default-options/testRun.ts @@ -0,0 +1,39 @@ +import type { + ReporterConfig, +} from 'jest-allure2-reporter'; + +export const testRun: ReporterConfig['testRun'] = { + hidden: ({ aggregatedResult }) => aggregatedResult.numFailedTestSuites > 0, + $: ({ $ }) => $, + historyId: async ({ $ }) => (await $.manifest((x) => x.name)) ?? '', + name: () => '(test run)', + fullName: async ({ $ }) => (await $.manifest((x) => x.name)) ?? '', + description: () => '', + descriptionHtml: () => '', + start: ({ aggregatedResult }) => aggregatedResult.startTime, + stop: () => Date.now(), + stage: ({ aggregatedResult }) => aggregatedResult.wasInterrupted ? 'interrupted' : 'finished', + status: ({ aggregatedResult }) => aggregatedResult.numFailedTestSuites > 0 ? 'failed' : 'passed', + statusDetails: () => void 0, + attachments: () => void 0, + parameters: ({ aggregatedResult }) => [ + { + name: 'Suites passed', + value: `${aggregatedResult.numPassedTestSuites}`, + }, + { + name: 'Suites failed', + value: `${aggregatedResult.numFailedTestSuites}`, + }, + { + name: 'Suites broken', + value: `${aggregatedResult.numRuntimeErrorTestSuites}`, + }, + { + name: 'Suites pending', + value: `${aggregatedResult.numPendingTestSuites}`, + }, + ], + labels: () => [], + links: () => [], +}; diff --git a/src/options/default-options/testStep.ts b/src/options/default-options/testStep.ts index 06782829..b4efee6e 100644 --- a/src/options/default-options/testStep.ts +++ b/src/options/default-options/testStep.ts @@ -1,14 +1,13 @@ import type { AllureTestStepMetadata, + ReporterConfig, Stage, Status, - ResolvedTestStepCustomizer, } from 'jest-allure2-reporter'; -import { stripStatusDetails } from '../utils'; - -export const testStep: ResolvedTestStepCustomizer = { +export const testStep: ReporterConfig['testStep'] = { hidden: () => false, + $: ({ $ }) => $, name: ({ testStepMetadata }) => testStepMetadata.displayName || testStepMetadata.hookType || @@ -18,8 +17,8 @@ export const testStep: ResolvedTestStepCustomizer = { stage: ({ testStepMetadata }) => testStepMetadata.stage, status: ({ testStepMetadata }) => testStepMetadata.status ?? inferStatus(testStepMetadata), - statusDetails: ({ testStepMetadata }) => - stripStatusDetails(testStepMetadata.statusDetails), + statusDetails: ({ $, testStepMetadata }) => + $.stripAnsi(testStepMetadata.statusDetails), attachments: ({ testStepMetadata }) => testStepMetadata.attachments ?? [], parameters: ({ testStepMetadata }) => testStepMetadata.parameters ?? [], }; diff --git a/src/options/extractors/asExtractor.ts b/src/options/extractors/asExtractor.ts new file mode 100644 index 00000000..10d08862 --- /dev/null +++ b/src/options/extractors/asExtractor.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Extractor, ExtractorContext } from 'jest-allure2-reporter'; + +import { isPromiseLike } from '../../utils'; + +/** + * Resolves the unknown value either as an extractor or it + * builds a fallback extractor that returns the given value. + * + * Since Allure 2 has a quirky convention that the first value + * in an array takes precedence, we on purpose put the custom + * value first and the default value second. + * + * The fallback extractor is capable both of merging arrays and + * defaulting the values. The former is useful for tags, the latter + * for the rest of the labels which don't support multiple occurrences. + */ +export function asExtractor< + R, + E extends Extractor, R> = Extractor< + any, + ExtractorContext, + R + >, +>(maybeExtractor: R | E | undefined): E | undefined { + if (maybeExtractor == null) { + return undefined; + } + + if (isExtractor(maybeExtractor)) { + return maybeExtractor; + } + + const value = maybeExtractor; + const extractor = (async ({ value: maybePromise }) => { + const baseValue = isPromiseLike(maybePromise) + ? await maybePromise + : maybePromise; + + if (Array.isArray(baseValue)) { + return Array.isArray(value) + ? [...baseValue, ...value] + : [...baseValue, value]; + } + + return value ?? baseValue; + }) as E; + + return extractor; +} + +function isExtractor(value: unknown): value is E { + return typeof value === 'function'; +} diff --git a/src/options/extractors/composeExtractors.ts b/src/options/extractors/composeExtractors.ts new file mode 100644 index 00000000..df984c67 --- /dev/null +++ b/src/options/extractors/composeExtractors.ts @@ -0,0 +1,23 @@ +import type { Extractor, ExtractorContext } from 'jest-allure2-reporter'; + +import { once } from '../../utils'; + +export function composeExtractors>( + a: Extractor | undefined, + b: Extractor, +): Extractor { + if (!a) { + return b; + } + + return (context: any) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { value, ...newContext } = context; + Object.defineProperty(newContext, 'value', { + get: once(() => b(context)), + enumerable: true, + }); + + return a(newContext as typeof context); + }; +} diff --git a/src/options/extractors/extractors.test.ts b/src/options/extractors/extractors.test.ts new file mode 100644 index 00000000..0af68cb8 --- /dev/null +++ b/src/options/extractors/extractors.test.ts @@ -0,0 +1,30 @@ +import { composeExtractors } from './composeExtractors'; + +describe('extractors', () => { + describe('composeExtractors', () => { + it('should compose extractors correctly in a complex scenario', async () => { + const one = () => 1; + const two = composeExtractors(({ value }) => { + assertEq(value, 1); + return value * 2; + }, one); + const twoToo = composeExtractors(undefined, two); + const twoAlso = composeExtractors(({ value }) => value, twoToo); + const three = composeExtractors(async ({ value }) => { + assertEq(value, 2); + return value * 3; + }, twoAlso); + const threeAlso = composeExtractors(async ({ value }) => { + expect(value).toBeInstanceOf(Promise); + await expect(value).resolves.toBe(6); + return value; + }, three); + const result = await threeAlso({ value: void 0 }); + expect(result).toBe(6); + }); + }); +}); + +function assertEq(actual: unknown, expected: T): asserts actual is T { + expect(actual).toBe(expected); +} diff --git a/src/options/extractors/index.ts b/src/options/extractors/index.ts new file mode 100644 index 00000000..521a0d57 --- /dev/null +++ b/src/options/extractors/index.ts @@ -0,0 +1,2 @@ +export * from './asExtractor'; +export * from './composeExtractors'; diff --git a/src/options/utils/aggregateLinkCustomizers.ts b/src/options/utils/aggregateLinkCustomizers.ts deleted file mode 100644 index 20f75ff5..00000000 --- a/src/options/utils/aggregateLinkCustomizers.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { - Extractor, - ExtractorContext, - TestFileCustomizer, - TestCaseCustomizer, -} from 'jest-allure2-reporter'; -import type { Link } from 'jest-allure2-reporter'; - -type Customizer = TestFileCustomizer | TestCaseCustomizer; - -export function aggregateLinkCustomizers( - links: C['links'] | undefined, -): Extractor | undefined { - if (!links || typeof links === 'function') { - return links as Extractor | undefined; - } - - return (context: ExtractorContext) => { - return context.value - ?.map((link) => { - const extractor = links[link.type ?? '']; - return extractor ? extractor({ ...context, value: link } as any) : link; - }) - ?.filter(Boolean) as Link[] | undefined; - }; -} diff --git a/src/options/utils/asExtractor.ts b/src/options/utils/asExtractor.ts deleted file mode 100644 index bc025c6b..00000000 --- a/src/options/utils/asExtractor.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Extractor } from 'jest-allure2-reporter'; - -/** - * Resolves the unknown value either as an extractor or it - * builds a fallback extractor that returns the given value. - * - * Since Allure 2 has a quirky convention that the first value - * in an array takes precedence, we on purpose put the custom - * value first and the default value second. - * - * The fallback extractor is capable both of merging arrays and - * defaulting the values. The former is useful for tags, the latter - * for the rest of the labels which don't support multiple occurrences. - */ -export function asExtractor>( - value: R | E | undefined, -): E | undefined { - if (value === undefined) { - return undefined; - } - - return ( - typeof value === 'function' - ? value - : ({ value: customValue }) => { - if (Array.isArray(customValue)) { - return Array.isArray(value) - ? [...customValue, ...value] - : [...customValue, value]; - } else { - return customValue ?? value; - } - } - ) as E; -} diff --git a/src/options/utils/composeExtractors.ts b/src/options/utils/composeExtractors.ts deleted file mode 100644 index 044812c8..00000000 --- a/src/options/utils/composeExtractors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Extractor, ExtractorContext } from 'jest-allure2-reporter'; - -export function composeExtractors>( - a: Extractor | undefined, - b: Extractor, -): Extractor { - return a ? async (context) => a({ ...context, value: await b(context) }) : b; -} diff --git a/src/options/utils/index.ts b/src/options/utils/index.ts deleted file mode 100644 index 6b0f812a..00000000 --- a/src/options/utils/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './aggregateLabelCustomizers'; -export * from './aggregateLinkCustomizers'; -export * from './asExtractor'; -export * from './composeExtractors'; -export * from './resolvePlugins'; -export * from './stripStatusDetails'; diff --git a/src/options/utils/resolvePlugins.ts b/src/options/utils/resolvePlugins.ts deleted file mode 100644 index c05c65ca..00000000 --- a/src/options/utils/resolvePlugins.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { - Plugin, - PluginConstructor, - PluginReference, - PluginDeclaration, - PluginContext, -} from 'jest-allure2-reporter'; - -export function resolvePlugins( - context: PluginContext, - plugins: PluginDeclaration[] | undefined, -): Promise { - if (!plugins) { - return Promise.resolve([]); - } - - const promises = plugins.map((plugin) => { - return Array.isArray(plugin) - ? resolvePlugin(context, plugin[0], plugin[1]) - : resolvePlugin(context, plugin, {}); - }); - - return Promise.all(promises); -} - -async function resolvePlugin( - context: PluginContext, - reference: PluginReference, - options: Record, -): Promise { - let createPlugin: PluginConstructor; - - if (typeof reference === 'string') { - const rootDirectory = context.globalConfig.rootDir; - const resolved = require.resolve(reference, { paths: [rootDirectory] }); - createPlugin = await import(resolved); - } else { - createPlugin = reference; - } - - return createPlugin(options, context); -} diff --git a/src/options/utils/stripStatusDetails.ts b/src/options/utils/stripStatusDetails.ts deleted file mode 100644 index 2f58719e..00000000 --- a/src/options/utils/stripStatusDetails.ts +++ /dev/null @@ -1,16 +0,0 @@ -import stripAnsi from 'strip-ansi'; -import type { StatusDetails } from 'jest-allure2-reporter'; - -export function stripStatusDetails( - statusDetails?: StatusDetails, -): StatusDetails | undefined { - if (!statusDetails) { - return undefined; - } - - const { message, trace } = statusDetails; - return { - message: message ? stripAnsi(message) : undefined, - trace: trace ? stripAnsi(trace) : undefined, - }; -} diff --git a/src/realms/AllureRealm.ts b/src/realms/AllureRealm.ts index 6923da0a..28d77c5e 100644 --- a/src/realms/AllureRealm.ts +++ b/src/realms/AllureRealm.ts @@ -5,7 +5,7 @@ import { state } from 'jest-metadata'; import type { AllureGlobalMetadata } from 'jest-allure2-reporter'; import type { SharedReporterConfig } from '../runtime'; -import { AllureRuntime, AllureRuntimeContext } from '../runtime'; +import { AllureRuntimeImplementation, AllureRuntimeContext } from '../runtime'; import { AllureMetadataProxy } from '../metadata'; export class AllureRealm { @@ -39,5 +39,5 @@ export class AllureRealm { }, }); - runtime = new AllureRuntime(this.runtimeContext); + runtime = new AllureRuntimeImplementation(this.runtimeContext); } diff --git a/src/reporter-plugins/augs.d.ts b/src/reporter-plugins/augs.d.ts deleted file mode 100644 index 5227d4df..00000000 --- a/src/reporter-plugins/augs.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare module 'jest-allure2-reporter' { - import type { ExecutorInfo } from '@noomorph/allure-js-commons'; - - import type { ManifestHelper } from './manifest'; - - interface ExtractorHelpersAugmentation { - /** - * The contents of the `package.json` file if it exists. - */ - manifest: ManifestHelper; - - /** - * Information about the current executor - */ - getExecutorInfo(): Promise; - getExecutorInfo(includeLocal: true): Promise; - } -} diff --git a/src/reporter-plugins/ci/BuildkiteInfoProvider.test.ts b/src/reporter-plugins/ci/BuildkiteInfoProvider.test.ts deleted file mode 100644 index 27b27918..00000000 --- a/src/reporter-plugins/ci/BuildkiteInfoProvider.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { BuildkiteInfoProvider } from './BuildkiteInfoProvider'; - -describe('BuildkiteInfoProvider', () => { - const environment = { - BUILDKITE: 'true', - BUILDKITE_AGENT_NAME: 'Buildkite Agent', - BUILDKITE_BUILD_URL: 'https://buildkite.com/owner/repo/builds/123', - BUILDKITE_BUILD_NUMBER: '123', - }; - - it('should return Buildkite info when enabled', async () => { - const provider = new BuildkiteInfoProvider(environment); - expect(provider.enabled).toBe(true); - - const info = await provider.getExecutorInfo(); - expect(info).toEqual({ - name: expect.stringContaining('Buildkite Agent'), - type: 'buildkite', - buildUrl: 'https://buildkite.com/owner/repo/builds/123', - buildOrder: 123, - buildName: '#123', - }); - }); - - it('should return default info when not enabled', async () => { - const provider = new BuildkiteInfoProvider({}); - expect(provider.enabled).toBe(false); - - const info = await provider.getExecutorInfo(); - expect(info).toEqual({ - name: expect.stringContaining('Buildkite'), - type: 'buildkite', - buildUrl: undefined, - buildOrder: Number.NaN, - buildName: '#undefined', - }); - }); -}); diff --git a/src/reporter-plugins/ci/BuildkiteInfoProvider.ts b/src/reporter-plugins/ci/BuildkiteInfoProvider.ts deleted file mode 100644 index 3bca9b41..00000000 --- a/src/reporter-plugins/ci/BuildkiteInfoProvider.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -import type { ExecutorInfoProvider } from './ExecutorInfoProvider'; -import { getOSDetails } from './utils'; - -export interface BuildkiteEnvironment { - BUILDKITE?: string; - BUILDKITE_AGENT_NAME?: string; - BUILDKITE_BUILD_URL?: string; - BUILDKITE_BUILD_NUMBER?: string; -} - -export class BuildkiteInfoProvider implements ExecutorInfoProvider { - constructor(private readonly environment: Partial) {} - - get enabled() { - return !!this.environment.BUILDKITE; - } - - async getExecutorInfo(): Promise { - const { - BUILDKITE_AGENT_NAME, - BUILDKITE_BUILD_URL, - BUILDKITE_BUILD_NUMBER, - } = this.environment; - const agentName = BUILDKITE_AGENT_NAME || 'Buildkite'; - - return { - name: `${agentName} (${getOSDetails()})`, - type: 'buildkite', - buildUrl: BUILDKITE_BUILD_URL, - buildOrder: Number(BUILDKITE_BUILD_NUMBER), - buildName: `#${BUILDKITE_BUILD_NUMBER}`, - }; - } -} diff --git a/src/reporter-plugins/ci/ExecutorInfoProvider.ts b/src/reporter-plugins/ci/ExecutorInfoProvider.ts deleted file mode 100644 index 8abfe6ae..00000000 --- a/src/reporter-plugins/ci/ExecutorInfoProvider.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -export interface ExecutorInfoProvider { - readonly enabled: boolean; - getExecutorInfo(): Promise; -} diff --git a/src/reporter-plugins/ci/GitHubInfoProvider.test.ts b/src/reporter-plugins/ci/GitHubInfoProvider.test.ts deleted file mode 100644 index 5e44b1d5..00000000 --- a/src/reporter-plugins/ci/GitHubInfoProvider.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-imports */ -jest.mock('node-fetch'); - -import { - type GitHubEnvironment, - GitHubInfoProvider, -} from './GitHubInfoProvider'; - -describe('GitHubInfoProvider', () => { - const apiUrl = - 'https://api.github.com/repos/owner/repo/actions/runs/123/attempts/1/jobs'; - - let fetch: jest.MockedFunction; - let environment: Partial; - - beforeEach(() => { - environment = { - GITHUB_ACTIONS: 'true', - GITHUB_JOB: 'test_job', - GITHUB_REPOSITORY: 'owner/repo', - GITHUB_RUN_ATTEMPT: '1', - GITHUB_RUN_ID: '123', - GITHUB_RUN_NUMBER: '10', - GITHUB_SERVER_URL: 'https://github.com', - GITHUB_TOKEN: 'my_token', - RUNNER_NAME: 'GitHub Runner', - }; - }); - - beforeEach(() => { - fetch = jest.requireMock('node-fetch'); - fetch.mockClear(); - }); - - const mockSuccessResponse = (jobs: any[]) => ({ - ok: true, - json: jest.fn().mockResolvedValue({ jobs }), - }); - - it('should be disabled if GITHUB_ACTIONS env var is undefined', () => { - const provider = new GitHubInfoProvider({ - ...environment, - GITHUB_ACTIONS: '', - }); - - expect(provider.enabled).toBe(false); - }); - - it('should be enabled if GITHUB_ACTIONS env var is defined', () => { - const provider = new GitHubInfoProvider({ - ...environment, - GITHUB_ACTIONS: '', - }); - - expect(provider.enabled).toBe(false); - }); - - it('should return executor info when API request is successful', async () => { - const jobInfo = { - id: 'job_id', - name: 'Test Job', - html_url: 'https://github.com/owner/repo/actions/runs/123#test_job', - }; - - fetch.mockResolvedValueOnce(mockSuccessResponse([jobInfo]) as any); - - const provider = new GitHubInfoProvider(environment); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringContaining('GitHub Runner'), - type: 'github', - buildUrl: jobInfo.html_url, - buildOrder: 101, - buildName: '123#test_job', - }); - }); - - it('should return executor info with fallback URL when job is not found', async () => { - fetch.mockResolvedValueOnce(mockSuccessResponse([]) as any); - - const provider = new GitHubInfoProvider(environment); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringContaining('GitHub Runner'), - type: 'github', - buildUrl: 'https://github.com/owner/repo/actions/runs/123', - buildOrder: 101, - buildName: '123#test_job', - }); - }); - - it('should return executor info with fallback URL when API request fails', async () => { - const mockError = new Error('API error'); - fetch.mockRejectedValueOnce(mockError); - jest.spyOn(console, 'error').mockReturnValueOnce(); - - const provider = new GitHubInfoProvider(environment); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringContaining('GitHub Runner'), - type: 'github', - buildUrl: 'https://github.com/owner/repo/actions/runs/123', - buildOrder: 101, - buildName: '123#test_job', - }); - }); - - it('should return executor info with fallback URL when token is not provided', async () => { - delete environment.GITHUB_TOKEN; - - const provider = new GitHubInfoProvider(environment); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringContaining('GitHub Runner'), - type: 'github', - buildUrl: 'https://github.com/owner/repo/actions/runs/123', - buildOrder: 101, - buildName: '123#test_job', - }); - expect(fetch).not.toHaveBeenCalled(); - }); - - it('should include authorization header when githubToken is provided', async () => { - fetch.mockResolvedValueOnce(mockSuccessResponse([]) as any); - - const provider = new GitHubInfoProvider(environment); - await provider.getExecutorInfo(); - - expect(fetch).toHaveBeenCalledWith(apiUrl, { - headers: { - Accept: 'application/vnd.github.v3+json', - Authorization: 'token my_token', - }, - }); - }); -}); diff --git a/src/reporter-plugins/ci/GitHubInfoProvider.ts b/src/reporter-plugins/ci/GitHubInfoProvider.ts deleted file mode 100644 index 9849f827..00000000 --- a/src/reporter-plugins/ci/GitHubInfoProvider.ts +++ /dev/null @@ -1,103 +0,0 @@ -import fetch from 'node-fetch'; -import snakeCase from 'lodash.snakecase'; -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -import type { ExecutorInfoProvider } from './ExecutorInfoProvider'; -import { getOSDetails } from './utils'; - -export interface GitHubEnvironment { - GITHUB_ACTIONS: string; - GITHUB_JOB: string; - GITHUB_REPOSITORY: string; - GITHUB_RUN_ATTEMPT: string; - GITHUB_RUN_ID: string; - GITHUB_RUN_NUMBER: string; - GITHUB_SERVER_URL: string; - GITHUB_TOKEN: string; - RUNNER_NAME?: string; -} - -type Job = { - id: string; - name: string; - html_url: string; -}; - -export class GitHubInfoProvider implements ExecutorInfoProvider { - constructor(private readonly environment: Partial) {} - - get enabled() { - return !!this.environment.GITHUB_ACTIONS; - } - - async getExecutorInfo(): Promise { - const job = await this._fetchJob(); - - const { - GITHUB_RUN_ATTEMPT, - GITHUB_RUN_ID, - GITHUB_RUN_NUMBER, - GITHUB_JOB, - RUNNER_NAME, - GITHUB_SERVER_URL, - GITHUB_REPOSITORY, - } = this.environment; - - const runnerName = RUNNER_NAME || 'GitHub Actions'; - const buildUrl = job - ? job.html_url - : `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; - - return { - name: `${runnerName} (${getOSDetails()})`, - type: 'github', - buildUrl, - buildOrder: Number(GITHUB_RUN_NUMBER) * 10 + Number(GITHUB_RUN_ATTEMPT), - buildName: `${GITHUB_RUN_ID}#${GITHUB_JOB}`, - }; - } - - private async _fetchJob(): Promise { - const url = this._buildApiUrl(); - - try { - const data = await this._fetchJobs(url); - return this._findJob(data.jobs); - } catch (error: unknown) { - console.error(`Failed to fetch job ID from: ${url}\nReason:`, error); - return; - } - } - - private async _fetchJobs(url: string) { - if (!this.environment.GITHUB_TOKEN) { - return { jobs: [] }; - } - - const headers = { - Accept: 'application/vnd.github.v3+json', - Authorization: `token ${this.environment.GITHUB_TOKEN}`, - }; - - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`HTTP ${response.status} ${response.statusText}`); - } - - return await response.json(); - } - - private _buildApiUrl(): string { - const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT } = - this.environment; - return `https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}/jobs`; - } - - private _findJob(jobs: Job[]): Job | undefined { - const { GITHUB_JOB } = this.environment; - - return jobs.length === 1 - ? jobs[0] - : jobs.find((job) => snakeCase(job.name) === GITHUB_JOB); - } -} diff --git a/src/reporter-plugins/ci/LocalInfoProvider.test.ts b/src/reporter-plugins/ci/LocalInfoProvider.test.ts deleted file mode 100644 index a79aa950..00000000 --- a/src/reporter-plugins/ci/LocalInfoProvider.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { LocalInfoProvider } from './LocalInfoProvider'; - -describe('LocalInfoProvider', () => { - it('should return local info', async () => { - const provider = new LocalInfoProvider(true); - const info = await provider.getExecutorInfo(); - - expect(info).toEqual({ - name: expect.stringMatching(/.* \(.*\/.*\)/), - type: expect.any(String), - }); - }); -}); diff --git a/src/reporter-plugins/ci/LocalInfoProvider.ts b/src/reporter-plugins/ci/LocalInfoProvider.ts deleted file mode 100644 index a5205e2a..00000000 --- a/src/reporter-plugins/ci/LocalInfoProvider.ts +++ /dev/null @@ -1,17 +0,0 @@ -import os from 'node:os'; - -import type { ExecutorInfo } from 'jest-allure2-reporter'; - -import type { ExecutorInfoProvider } from './ExecutorInfoProvider'; -import { getOSDetails } from './utils'; - -export class LocalInfoProvider implements ExecutorInfoProvider { - constructor(public readonly enabled: boolean) {} - - async getExecutorInfo(): Promise { - return { - name: `${os.hostname()} (${getOSDetails()})`, - type: `${os.platform()}-${os.arch()}`, - }; - } -} diff --git a/src/reporter-plugins/ci/index.ts b/src/reporter-plugins/ci/index.ts deleted file mode 100644 index e87890dd..00000000 --- a/src/reporter-plugins/ci/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -/// - -import type { - ExecutorInfo, - Plugin, - PluginConstructor, -} from 'jest-allure2-reporter'; - -import { GitHubInfoProvider } from './GitHubInfoProvider'; -import { BuildkiteInfoProvider } from './BuildkiteInfoProvider'; -import { LocalInfoProvider } from './LocalInfoProvider'; -import type { ExecutorInfoProvider } from './ExecutorInfoProvider'; - -export const ciPlugin: PluginConstructor = () => { - const environment = process.env as Record; - const providers: ExecutorInfoProvider[] = [ - new BuildkiteInfoProvider(environment), - new GitHubInfoProvider(environment), - ]; - - const isEnabled = (provider: ExecutorInfoProvider) => provider.enabled; - - async function getExecutorInfo( - includeLocal: boolean, - ): Promise { - const local: ExecutorInfoProvider = new LocalInfoProvider(includeLocal); - return [...providers, local].find(isEnabled)?.getExecutorInfo(); - } - - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/ci', - async helpers($) { - Object.assign($, { getExecutorInfo }); - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/ci/utils/getOSDetails.ts b/src/reporter-plugins/ci/utils/getOSDetails.ts deleted file mode 100644 index 7a7397a4..00000000 --- a/src/reporter-plugins/ci/utils/getOSDetails.ts +++ /dev/null @@ -1,5 +0,0 @@ -import os from 'node:os'; - -export function getOSDetails() { - return `${os.type()} ${os.release()}/${os.arch()}`; -} diff --git a/src/reporter-plugins/ci/utils/index.ts b/src/reporter-plugins/ci/utils/index.ts deleted file mode 100644 index a6ba1076..00000000 --- a/src/reporter-plugins/ci/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './getOSDetails'; diff --git a/src/reporter-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap b/src/reporter-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap deleted file mode 100644 index 3ecc6c89..00000000 --- a/src/reporter-plugins/docblock/__snapshots__/extractJsdocAbove.test.ts.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractJsDoc should extract a broken docblock 1`] = ` -"/** - * This is a broken docblock - * but it's still a docblock -*/" -`; - -exports[`extractJsDoc should extract a multiline docblock 1`] = ` -"/** - * This is a multiline docblock - */" -`; - -exports[`extractJsDoc should extract a single line docblock 1`] = `" /** This is a single line docblock */"`; - -exports[`extractJsDoc should extract a weird two-line docblock 1`] = ` -"/** - * This is a weird two-line docblock */" -`; - -exports[`extractJsDoc should not extract a non-docblock 1`] = `""`; diff --git a/src/reporter-plugins/docblock/docblockPlugin.ts b/src/reporter-plugins/docblock/docblockPlugin.ts deleted file mode 100644 index 51ea7b53..00000000 --- a/src/reporter-plugins/docblock/docblockPlugin.ts +++ /dev/null @@ -1,58 +0,0 @@ -// eslint-disable-next-line node/no-unpublished-import -import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; -import { extract, parseWithComments } from 'jest-docblock'; - -import { - extractTypescriptAST, - extractTypeScriptCode, - FileNavigatorCache, - importTypeScript, -} from '../source-code'; - -import { extractJsdocAbove } from './extractJsdocAbove'; - -export const docblockPlugin: PluginConstructor = () => { - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/docblock', - - async postProcessMetadata({ metadata }) { - const { fileName, lineNumber, columnNumber } = - metadata.sourceLocation || {}; - let extracted: string | undefined; - const ts = await importTypeScript(); - if ( - ts && - fileName != null && - lineNumber != null && - columnNumber != null - ) { - const ast = await extractTypescriptAST(ts, fileName); - const fullCode = await extractTypeScriptCode( - ts, - ast, - [lineNumber, columnNumber], - true, - ); - if (fullCode) { - extracted = extract(fullCode); - } - } - - if (!extracted && fileName) { - const navigator = await FileNavigatorCache.instance.resolve(fileName); - - extracted = - lineNumber == null - ? extract(navigator.sourceCode) - : extractJsdocAbove(navigator, lineNumber); - } - - if (extracted) { - const { comments, pragmas } = parseWithComments(extracted); - metadata.docblock = { comments, pragmas }; - } - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/docblock/extractJsdocAbove.test.ts b/src/reporter-plugins/docblock/extractJsdocAbove.test.ts deleted file mode 100644 index 0f101c86..00000000 --- a/src/reporter-plugins/docblock/extractJsdocAbove.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { LineNavigator } from '../source-code'; - -import { extractJsdocAbove as extractJsDocument_ } from './extractJsdocAbove'; - -const FIXTURES = [ - `\ -/** - * This is a multiline docblock - */`, - ' /** This is a single line docblock */', - `/** - * This is a broken docblock - * but it's still a docblock -*/`, - `/** - * This is a weird two-line docblock */`, - `/* - * This is not a docblock - */`, -].map((sourceCode) => sourceCode + '\n' + 'function test() {}\n'); - -describe('extractJsDoc', () => { - const extract = (index: number, line: number) => - extractJsDocument_(new LineNavigator(FIXTURES[index]), line); - - it('should extract a multiline docblock', () => - expect(extract(0, 4)).toMatchSnapshot()); - - it('should extract a single line docblock', () => - expect(extract(1, 2)).toMatchSnapshot()); - - it('should extract a broken docblock', () => - expect(extract(2, 5)).toMatchSnapshot()); - - it('should extract a weird two-line docblock', () => - expect(extract(3, 3)).toMatchSnapshot()); - - it('should not extract a non-docblock', () => - expect(extract(4, 4)).toMatchSnapshot()); - - it('should ignore out of range line index', () => - expect(extract(0, 5)).toBe('')); - - it('should ignore zero line index', () => expect(extract(0, 0)).toBe('')); - - it('should ignore the middle of a docblock', () => - expect(extract(0, 2)).toBe('')); -}); diff --git a/src/reporter-plugins/docblock/extractJsdocAbove.ts b/src/reporter-plugins/docblock/extractJsdocAbove.ts deleted file mode 100644 index 08c41ac6..00000000 --- a/src/reporter-plugins/docblock/extractJsdocAbove.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { LineNavigator } from '../source-code'; - -export function extractJsdocAbove( - navigator: LineNavigator, - testLineIndex: number, -): string { - if (!navigator.jump(testLineIndex)) return ''; - if (!navigator.prev()) return ''; - - let currentLine = navigator.read(); - const docblockEndIndex = getCommentEnd(currentLine); - if (docblockEndIndex === -1) return ''; - - if (isSingleLineDocblock(currentLine, docblockEndIndex)) { - return currentLine; - } - - const buffer: string[] = []; - buffer.unshift(currentLine.slice(0, Math.max(0, docblockEndIndex + 2))); - - while (navigator.prev()) { - currentLine = navigator.read(); - buffer.unshift(currentLine); - - const start = getCommentStart(currentLine); - if (isDocblockStart(currentLine, start)) { - return buffer.join('\n'); - } - - if (start >= 0) { - break; - } - } - - return ''; -} - -function isSingleLineDocblock(line: string, end: number): boolean { - const start = getCommentStart(line); - if (start < 0) return false; - - return start < end; -} - -function getCommentStart(line: string): number { - const start = line.indexOf('/*'); - if (start <= 0) return start; - - const whitespace = line.slice(0, start); - return whitespace.trim() ? -1 : start; -} - -function getCommentEnd(line: string): number { - return line.lastIndexOf('*/'); -} - -function isDocblockStart(line: string, commentIndex: number): boolean { - return commentIndex >= 0 && line[commentIndex + 2] === '*'; -} diff --git a/src/reporter-plugins/docblock/index.ts b/src/reporter-plugins/docblock/index.ts deleted file mode 100644 index 00a9b5f6..00000000 --- a/src/reporter-plugins/docblock/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './docblockPlugin'; diff --git a/src/reporter-plugins/docblock/parseJsdoc.ts b/src/reporter-plugins/docblock/parseJsdoc.ts deleted file mode 100644 index 0b0f0a22..00000000 --- a/src/reporter-plugins/docblock/parseJsdoc.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { AllureTestItemDocblock } from 'jest-allure2-reporter'; -import { extract, parseWithComments } from 'jest-docblock'; - -import type { LineNavigator } from '../source-code'; - -import { extractJsdocAbove } from './extractJsdocAbove'; - -export function parseJsdoc( - navigator: LineNavigator, - lineNumber: number, -): AllureTestItemDocblock { - const contents = extractJsdocAbove(navigator, lineNumber); - const extracted = extract(contents); - - const { comments, pragmas } = parseWithComments(extracted); - return { comments, pragmas }; -} diff --git a/src/reporter-plugins/index.ts b/src/reporter-plugins/index.ts deleted file mode 100644 index 3d1f19e2..00000000 --- a/src/reporter-plugins/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { ciPlugin as ci } from './ci'; -export { docblockPlugin as docblock } from './docblock'; -export { manifestPlugin as manifest } from './manifest'; -export { remarkPlugin as remark } from './remark'; -export { sourceCodePlugin as sourceCode } from './source-code'; diff --git a/src/reporter-plugins/manifest/ManifestResolver.test.ts b/src/reporter-plugins/manifest/ManifestResolver.test.ts deleted file mode 100644 index ef3a835b..00000000 --- a/src/reporter-plugins/manifest/ManifestResolver.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ManifestResolver } from './ManifestResolver'; - -describe('manifest', () => { - const manifestResolver = new ManifestResolver( - process.cwd(), - jest.requireActual, - ); - - const manifest = manifestResolver.extract; - - it('should return the entire package.json content of the current package when called without arguments', async () => { - const result = await manifest(); - expect(result).toHaveProperty('version'); - expect(result).toHaveProperty('name', 'jest-allure2-reporter'); - }); - - it('should return the entire package.json content of the specified package when called with a string argument', async () => { - const result = await manifest('lodash'); - expect(result).toHaveProperty('version'); - expect(result).toHaveProperty('name', 'lodash'); - }); - - it('should return a specific property of the package.json content of the current package when called with a callback', async () => { - const version = await manifest((m) => m.version); - expect(typeof version).toBe('string'); - }); - - it('should return a specific property of the package.json content of the specified package when called with a string argument and a callback', async () => { - const version = await manifest('lodash', (m) => m.version); - expect(typeof version).toBe('string'); - }); -}); diff --git a/src/reporter-plugins/manifest/ManifestResolver.ts b/src/reporter-plugins/manifest/ManifestResolver.ts deleted file mode 100644 index 739395f8..00000000 --- a/src/reporter-plugins/manifest/ManifestResolver.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import pkgUp from 'pkg-up'; - -export type ManifestExtractorCallback = (manifest: Record) => T; - -export interface ManifestHelper { - (): Promise | undefined>; - (packageName: string): Promise | undefined>; - (callback: ManifestExtractorCallback): Promise; - (packageName: string, callback: ManifestExtractorCallback): Promise; -} - -export type ImportModuleFunction = ( - path: string, -) => Record | Promise>; - -export class ManifestResolver { - private readonly cwd: string; - private readonly importFn: ImportModuleFunction; - - constructor(cwd: string, importFunction: ImportModuleFunction) { - this.cwd = cwd; - this.importFn = importFunction; - } - - public extract: ManifestHelper = ( - packageNameOrCallback?: string | ManifestExtractorCallback, - callback?: ManifestExtractorCallback, - ) => { - if (!packageNameOrCallback) { - return this.manifestImpl(); - } else if (this.isManifestExtractorCallback(packageNameOrCallback)) { - return this.manifestImpl(undefined, packageNameOrCallback); - } else if (callback) { - return this.manifestImpl(packageNameOrCallback, callback); - } else { - return this.manifestImpl(packageNameOrCallback); - } - }; - - private isManifestExtractorCallback( - value: unknown, - ): value is ManifestExtractorCallback { - return typeof value === 'function'; - } - - private async manifestImpl( - packageName?: string, - callback?: ManifestExtractorCallback, - ): Promise { - const manifestPath = await this.resolveManifestPath(packageName); - if (!manifestPath) { - // TODO: log warning - return; - } - - try { - const manifest = await this.importFn(manifestPath); - return callback ? callback(manifest as any) : manifest; - } catch { - // TODO: log error - return; - } - } - - private async resolveManifestPath(packageName?: string) { - return packageName - ? require.resolve(packageName + '/package.json') - : await pkgUp({ cwd: this.cwd }); - } -} diff --git a/src/reporter-plugins/manifest/index.ts b/src/reporter-plugins/manifest/index.ts deleted file mode 100644 index 834fe03e..00000000 --- a/src/reporter-plugins/manifest/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/// - -import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; -import importFrom from 'import-from'; - -import { ManifestResolver } from './ManifestResolver'; - -export type { - ManifestExtractorCallback, - ManifestHelper, -} from './ManifestResolver'; - -export const manifestPlugin: PluginConstructor = (_1, { globalConfig }) => { - const cwd = globalConfig.rootDir; - const resolver = new ManifestResolver( - cwd, - (modulePath) => importFrom(cwd, modulePath) as Record, - ); - - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/manifest', - async helpers($) { - $.manifest = resolver.extract; - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/remark.ts b/src/reporter-plugins/remark.ts deleted file mode 100644 index dbde8a35..00000000 --- a/src/reporter-plugins/remark.ts +++ /dev/null @@ -1,43 +0,0 @@ -/// - -import type { Plugin, PluginConstructor } from 'jest-allure2-reporter'; - -export const remarkPlugin: PluginConstructor = async () => { - const remark = await import('remark'); - const [ - remarkGfm, - remarkRehype, - rehypeSanitize, - rehypeStringify, - rehypeHighlight, - ] = await Promise.all([ - import('remark-gfm'), - import('remark-rehype'), - import('rehype-sanitize'), - import('rehype-stringify'), - import('rehype-highlight'), - ]); - - const processor = remark - .remark() - .use(remarkGfm.default) - .use(remarkRehype.default) - .use(rehypeSanitize.default) - .use(rehypeHighlight.default) - .use(rehypeStringify.default); - - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/remark', - helpers($) { - $.markdown2html = async (markdown: string) => { - return processor.process(markdown).then((result) => result.toString()); - }; - - $.sourceCode2Markdown = ({ code, language = '' } = {}) => { - return code ? '```' + language + '\n' + code + '\n```' : ''; - }; - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/source-code/detectSourceLanguage.ts b/src/reporter-plugins/source-code/detectSourceLanguage.ts deleted file mode 100644 index 32174448..00000000 --- a/src/reporter-plugins/source-code/detectSourceLanguage.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from 'node:path'; - -export function detectSourceLanguage(fileName: string): string { - switch (path.extname(fileName)) { - case '.js': - case '.jsx': - case '.cjs': - case '.mjs': { - return 'javascript'; - } - case '.ts': - case '.tsx': - case '.cts': - case '.mts': { - return 'typescript'; - } - default: { - return ''; - } - } -} diff --git a/src/reporter-plugins/source-code/index.ts b/src/reporter-plugins/source-code/index.ts deleted file mode 100644 index c17b8775..00000000 --- a/src/reporter-plugins/source-code/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './sourceCodePlugin'; -export { - LineNavigator, - FileNavigatorCache, - importTypeScript, - extractTypeScriptCode, - extractTypescriptAST, -} from './utils'; diff --git a/src/reporter-plugins/source-code/sourceCodePlugin.ts b/src/reporter-plugins/source-code/sourceCodePlugin.ts deleted file mode 100644 index 393b1d53..00000000 --- a/src/reporter-plugins/source-code/sourceCodePlugin.ts +++ /dev/null @@ -1,104 +0,0 @@ -/// - -import type { ExtractorHelperSourceCode } from 'jest-allure2-reporter'; -import type { - AllureNestedTestStepMetadata, - AllureTestItemSourceLocation, - Plugin, - PluginConstructor, -} from 'jest-allure2-reporter'; - -import { weakMemoize } from '../../utils'; - -import { - extractTypescriptAST, - extractTypeScriptCode, - FileNavigatorCache, - importTypeScript, -} from './utils'; -import { detectSourceLanguage } from './detectSourceLanguage'; - -function isBeforeHook({ hookType }: AllureNestedTestStepMetadata) { - return hookType === 'beforeAll' || hookType === 'beforeEach'; -} - -function isAfterHook({ hookType }: AllureNestedTestStepMetadata) { - return hookType === 'afterAll' || hookType === 'afterEach'; -} - -function isDefined(value: T | undefined): value is T { - return value !== undefined; -} - -export const sourceCodePlugin: PluginConstructor = async () => { - const ts = await importTypeScript(); - - async function doExtract( - sourceLocation: AllureTestItemSourceLocation | undefined, - ): Promise { - if (!sourceLocation?.fileName) { - return; - } - - await FileNavigatorCache.instance.resolve(sourceLocation.fileName); - const language = detectSourceLanguage(sourceLocation.fileName); - if ((language === 'typescript' || language === 'javascript') && ts) { - const ast = await extractTypescriptAST(ts, sourceLocation.fileName); - const location = [ - sourceLocation.lineNumber, - sourceLocation.columnNumber, - ] as const; - const code = await extractTypeScriptCode(ts, ast, location); - if (code) { - return { - code, - language, - fileName: sourceLocation.fileName, - }; - } - } - - return; - } - - const plugin: Plugin = { - name: 'jest-allure2-reporter/plugins/source-code', - - async helpers($) { - const extractSourceCode = weakMemoize( - async ( - metadata: AllureNestedTestStepMetadata, - ): Promise => { - return doExtract(metadata.sourceLocation); - }, - ); - - const extractSourceCodeWithSteps = weakMemoize( - async ( - metadata: AllureNestedTestStepMetadata, - ): Promise => { - const test = await extractSourceCode(metadata); - const before = await Promise.all( - metadata.steps?.filter(isBeforeHook)?.map(extractSourceCode) ?? [], - ); - const after = await Promise.all( - metadata.steps?.filter(isAfterHook)?.map(extractSourceCode) ?? [], - ); - - return [...before, test, ...after].filter(isDefined); - }, - ); - - Object.assign($, { - extractSourceCode, - extractSourceCodeWithSteps, - }); - }, - - async postProcessMetadata(context) { - await context.$.extractSourceCode(context.metadata); - }, - }; - - return plugin; -}; diff --git a/src/reporter-plugins/source-code/utils/FileNavigatorCache.ts b/src/reporter-plugins/source-code/utils/FileNavigatorCache.ts deleted file mode 100644 index 548af87d..00000000 --- a/src/reporter-plugins/source-code/utils/FileNavigatorCache.ts +++ /dev/null @@ -1,28 +0,0 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; - -import { LineNavigator } from './LineNavigator'; - -export class FileNavigatorCache { - #cache = new Map>(); - - async resolve(filePath: string): Promise { - const absolutePath = path.resolve(filePath); - if (!this.#cache.has(absolutePath)) { - this.#cache.set(absolutePath, this.#createNavigator(absolutePath)); - } - - return this.#cache.get(absolutePath)!; - } - - #createNavigator = async (filePath: string) => { - const sourceCode = await fs.readFile(filePath, 'utf8').catch(() => ''); - return new LineNavigator(sourceCode); - }; - - clear() { - this.#cache.clear(); - } - - static readonly instance = new FileNavigatorCache(); -} diff --git a/src/reporter-plugins/source-code/utils/LineNavigator.test.ts b/src/reporter-plugins/source-code/utils/LineNavigator.test.ts deleted file mode 100644 index 67ef42ea..00000000 --- a/src/reporter-plugins/source-code/utils/LineNavigator.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { LineNavigator } from './LineNavigator'; - -describe('LineNavigator', () => { - let navigator: LineNavigator; - - beforeAll(() => { - navigator = new LineNavigator('foo\nbar\nbaz'); - }); - - describe('jump', () => { - it('should jump to the first line', () => { - expect(navigator.jump(1)).toBe(true); - expect(navigator.read()).toBe('foo'); - }); - - it('should jump to the second line', () => { - expect(navigator.jump(2)).toBe(true); - expect(navigator.read()).toBe('bar'); - }); - - it('should jump to the third line', () => { - expect(navigator.jump(3)).toBe(true); - expect(navigator.read()).toBe('baz'); - }); - - it('should not jump out of bounds', () => { - expect(navigator.jump(4)).toBe(false); - expect(navigator.read()).toBe('baz'); - }); - }); - - describe('prev/next', () => { - beforeEach(() => navigator.jump(1)); - - it('should go down and up', () => { - expect(navigator.next()).toBe(true); - expect(navigator.read()).toBe('bar'); - expect(navigator.prev()).toBe(true); - expect(navigator.read()).toBe('foo'); - }); - - it('should not go up out of bounds', () => { - expect(navigator.prev()).toBe(false); - expect(navigator.read()).toBe('foo'); - }); - - it('should not go down out of bounds', () => { - expect(navigator.next()).toBe(true); - expect(navigator.next()).toBe(true); - expect(navigator.next()).toBe(false); - expect(navigator.read()).toBe('baz'); - }); - }); - - test('stress test', () => { - // eslint-disable-next-line unicorn/new-for-builtins - const bigString = 'abc\ndef\n'.repeat(500_000); - const navigator = new LineNavigator(bigString); - expect(navigator.jump(1e6)).toBe(true); - expect(navigator.read()).toBe('def'); - expect(navigator.jump(3)).toBe(true); - expect(navigator.read()).toBe('abc'); - }, 200); -}); diff --git a/src/reporter-plugins/source-code/utils/LineNavigator.ts b/src/reporter-plugins/source-code/utils/LineNavigator.ts deleted file mode 100644 index 81a06fe4..00000000 --- a/src/reporter-plugins/source-code/utils/LineNavigator.ts +++ /dev/null @@ -1,62 +0,0 @@ -export class LineNavigator { - readonly #sourceCode: string; - #cursor = 0; - #line = 1; - #lines: string[] | null = null; - - constructor(sourceCode: string) { - this.#sourceCode = sourceCode; - } - - get sourceCode() { - return this.#sourceCode; - } - - get lines() { - if (this.#lines === null) { - this.#lines = this.#sourceCode.split('\n'); - } - - return this.#lines; - } - - jump(lineIndex: number): boolean { - while (this.#line > lineIndex) { - if (!this.prev()) return false; - } - - while (this.#line < lineIndex) { - if (!this.next()) return false; - } - - return true; - } - - next(): boolean { - const next = this.#sourceCode.indexOf('\n', this.#cursor); - if (next === -1) { - return false; - } - - this.#cursor = next + 1; - this.#line++; - return true; - } - - prev(): boolean { - if (this.#cursor === 0) return false; - - this.#cursor = this.#sourceCode.lastIndexOf('\n', this.#cursor - 2) + 1; - this.#line--; - return true; - } - - read(): string { - const nextIndex = this.#sourceCode.indexOf('\n', this.#cursor); - if (nextIndex === -1) { - return this.#sourceCode.slice(this.#cursor); - } - - return this.#sourceCode.slice(this.#cursor, nextIndex); - } -} diff --git a/src/reporter-plugins/source-code/utils/extractTypeScriptCode.ts b/src/reporter-plugins/source-code/utils/extractTypeScriptCode.ts deleted file mode 100644 index 9fd39f73..00000000 --- a/src/reporter-plugins/source-code/utils/extractTypeScriptCode.ts +++ /dev/null @@ -1,39 +0,0 @@ -// eslint-disable-next-line node/no-unpublished-import -import type TypeScript from 'typescript'; - -import { autoIndent } from '../../../utils'; - -export async function extractTypeScriptCode( - ts: typeof TypeScript, - ast: TypeScript.SourceFile | undefined, - [lineNumber, columnNumber]: readonly [number | undefined, number | undefined], - includeComments = false, -): Promise { - if (lineNumber == null || columnNumber == null || ast == null) { - return; - } - - const pos = ast.getPositionOfLineAndCharacter( - lineNumber - 1, - columnNumber - 1, - ); - - // TODO: find a non-private API for `getTouchingToken` - const token = (ts as any).getTouchingToken(ast, pos) as TypeScript.Node; - let node = token; - while ( - node.kind !== ts.SyntaxKind.ExpressionStatement && - node !== token.parent.parent - ) { - node = node.parent; - } - const expression = node; - if (includeComments) { - const start = expression.getFullStart(); - return ast.text.slice(start, start + expression.getFullWidth()); - } else { - return autoIndent( - ast.text.slice(expression.getStart(), expression.getEnd()), - ); - } -} diff --git a/src/reporter-plugins/source-code/utils/extractTypescriptAST.ts b/src/reporter-plugins/source-code/utils/extractTypescriptAST.ts deleted file mode 100644 index 431463ab..00000000 --- a/src/reporter-plugins/source-code/utils/extractTypescriptAST.ts +++ /dev/null @@ -1,30 +0,0 @@ -// eslint-disable-next-line node/no-unpublished-import -import type TypeScript from 'typescript'; - -import { FileNavigatorCache } from '../utils'; - -const sourceFileMap = new Map< - string, - Promise ->(); - -export function extractTypescriptAST( - ts: typeof TypeScript, - filePath: string, -): Promise { - if (!sourceFileMap.has(filePath)) { - sourceFileMap.set(filePath, parseFile(ts, filePath)); - } - - return sourceFileMap.get(filePath)!; -} - -async function parseFile( - ts: typeof TypeScript, - filePath: string, -): Promise { - const { sourceCode } = await FileNavigatorCache.instance.resolve(filePath); - return sourceCode - ? ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true) - : undefined; -} diff --git a/src/reporter-plugins/source-code/utils/importTypeScript.ts b/src/reporter-plugins/source-code/utils/importTypeScript.ts deleted file mode 100644 index 5c4a72f8..00000000 --- a/src/reporter-plugins/source-code/utils/importTypeScript.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies,node/no-unpublished-import */ -import type TypeScript from 'typescript'; - -export async function importTypeScript(): Promise { - const ts = await import('typescript').catch(() => null); - if (ts) { - return ts.default; - } - - return ts; -} diff --git a/src/reporter-plugins/source-code/utils/index.ts b/src/reporter-plugins/source-code/utils/index.ts deleted file mode 100644 index d98fa544..00000000 --- a/src/reporter-plugins/source-code/utils/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './extractTypescriptAST'; -export * from './extractTypeScriptCode'; -export * from './importTypeScript'; -export * from './FileNavigatorCache'; -export * from './LineNavigator'; diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index 3c7e1cf6..f940129f 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -30,15 +30,12 @@ import type { AllureTestStepMetadata, AllureTestStepResult, ExtractorHelpers, - GlobalExtractorContext, - Plugin, - PluginHookContexts, - PluginHookName, ReporterConfig, ReporterOptions, TestCaseExtractorContext, TestFileExtractorContext, TestStepExtractorContext, + TestRunExtractorContext, } from 'jest-allure2-reporter'; import { resolveOptions } from '../options'; @@ -48,7 +45,6 @@ import { md5 } from '../utils'; import * as fallbackHooks from './fallback'; export class JestAllure2Reporter extends JestMetadataReporter { - private _plugins: readonly Plugin[] = []; private readonly _$: Partial = {}; private readonly _allure: AllureRuntime; private readonly _config: ReporterConfig; @@ -77,16 +73,12 @@ export class JestAllure2Reporter extends JestMetadataReporter { aggregatedResult: AggregatedResult, options: ReporterOnStartOptions, ): Promise { - this._plugins = await this._config.plugins; - await super.onRunStart(aggregatedResult, options); if (this._config.overwrite) { await rimraf(this._config.resultsDir); await fs.mkdir(this._config.resultsDir, { recursive: true }); } - - await this._callPlugins('helpers', this._$); } async onTestFileStart(test: Test) { @@ -132,10 +124,10 @@ export class JestAllure2Reporter extends JestMetadataReporter { const config = this._config; - const globalContext: GlobalExtractorContext = { + const globalContext: TestRunExtractorContext = { globalConfig: this._globalConfig, config, - value: undefined, + default: () => void 0, $: this._$ as ExtractorHelpers, }; @@ -355,7 +347,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { } private async _renderHtmlDescription( - context: GlobalExtractorContext, + context: TestRunExtractorContext, test: AllurePayloadTest, ) { if (test.description) { @@ -440,21 +432,11 @@ export class JestAllure2Reporter extends JestMetadataReporter { await Promise.all( batch.map(async (metadata) => { const allureProxy = new AllureMetadataProxy(metadata); - await this._callPlugins('postProcessMetadata', { - $: this._$ as ExtractorHelpers, - metadata: allureProxy.assign({}).get(), - }); - }), - ); - } - - private async _callPlugins( - methodName: K, - context: PluginHookContexts[K], - ) { - await Promise.all( - this._plugins.map((p) => { - return p[methodName]?.(context as any); + allureProxy.get(); // TODO: remove this line + // await this._callPlugins('postProcessMetadata', { + // $: this._$ as ExtractorHelpers, + // metadata: allureProxy.assign({}).get(), + // }); }), ); } diff --git a/src/runtime/AllureRuntimeContext.ts b/src/runtime/AllureRuntimeContext.ts index 35c8c2a8..6552efd2 100644 --- a/src/runtime/AllureRuntimeContext.ts +++ b/src/runtime/AllureRuntimeContext.ts @@ -26,7 +26,7 @@ export class AllureRuntimeContext { readonly getNow: () => number; readonly flush: () => Promise; - readonly enqueueTask: (task: MaybeFunction>) => void; + readonly enqueueTask: (task: MaybeFunction>) => Promise; constructor(config: AllureRuntimeConfig) { this.contentAttachmentHandlers = config.contentAttachmentHandlers ?? { diff --git a/src/runtime/AllureRuntime.ts b/src/runtime/AllureRuntimeImplementation.ts similarity index 80% rename from src/runtime/AllureRuntime.ts rename to src/runtime/AllureRuntimeImplementation.ts index 3b5186d1..4fdb1cf5 100644 --- a/src/runtime/AllureRuntime.ts +++ b/src/runtime/AllureRuntimeImplementation.ts @@ -7,12 +7,12 @@ import { constant, isObject } from '../utils'; import type { AllureRuntimeBindOptions, AllureRuntimePluginCallback, - IAllureRuntime, + AllureRuntime, } from './types'; import * as runtimeModules from './modules'; import type { AllureRuntimeContext } from './AllureRuntimeContext'; -export class AllureRuntime implements IAllureRuntime { +export class AllureRuntimeImplementation implements AllureRuntime { readonly #context: AllureRuntimeContext; readonly #coreModule: runtimeModules.CoreModule; readonly #basicStepsModule: runtimeModules.StepsModule; @@ -31,10 +31,10 @@ export class AllureRuntime implements IAllureRuntime { this.#stepsDecorator = new runtimeModules.StepsDecorator({ runtime: this }); } - $bind = (options?: AllureRuntimeBindOptions): AllureRuntime => { + $bind = (options?: AllureRuntimeBindOptions): AllureRuntimeImplementation => { const { metadata = true, time = false } = options ?? {}; - return new AllureRuntime({ + return new AllureRuntimeImplementation({ ...this.#context, getCurrentMetadata: metadata ? constant(this.#context.getCurrentMetadata()) @@ -56,17 +56,17 @@ export class AllureRuntime implements IAllureRuntime { flush = () => this.#context.flush(); - description: IAllureRuntime['description'] = (value) => { + description: AllureRuntime['description'] = (value) => { // TODO: assert is a string this.#coreModule.description(value); }; - descriptionHtml: IAllureRuntime['descriptionHtml'] = (value) => { + descriptionHtml: AllureRuntime['descriptionHtml'] = (value) => { // TODO: assert is a string this.#coreModule.descriptionHtml(value); }; - fullName: IAllureRuntime['fullName'] = (value) => { + fullName: AllureRuntime['fullName'] = (value) => { // TODO: assert is a string this.#coreModule.fullName(value); }; @@ -76,25 +76,25 @@ export class AllureRuntime implements IAllureRuntime { this.#coreModule.historyId(value); } - label: IAllureRuntime['label'] = (name, value) => { + label: AllureRuntime['label'] = (name, value) => { // TODO: assert name is a string // TODO: assert value is a string this.#coreModule.label(name, value); }; - link: IAllureRuntime['link'] = (url, name, type) => { + link: AllureRuntime['link'] = (url, name, type) => { // TODO: url is a string // TODO: name is a string or nullish // TODO: type is a string or nullish this.#coreModule.link({ name, url, type }); }; - displayName: IAllureRuntime['displayName'] = (value) => { + displayName: AllureRuntime['displayName'] = (value) => { // TODO: assert is a string this.#coreModule.displayName(value); }; - parameter: IAllureRuntime['parameter'] = (name, value, options) => { + parameter: AllureRuntime['parameter'] = (name, value, options) => { // TODO: assert name is a string this.#coreModule.parameter({ name, @@ -103,7 +103,7 @@ export class AllureRuntime implements IAllureRuntime { }); }; - parameters: IAllureRuntime['parameters'] = (parameters) => { + parameters: AllureRuntime['parameters'] = (parameters) => { for (const [name, value] of Object.entries(parameters)) { if (value && typeof value === 'object') { const raw = value as Parameter; @@ -114,7 +114,7 @@ export class AllureRuntime implements IAllureRuntime { } }; - status: IAllureRuntime['status'] = (status, statusDetails) => { + status: AllureRuntime['status'] = (status, statusDetails) => { // TODO: assert string literal this.#coreModule.status(status); if (isObject(statusDetails)) { @@ -122,18 +122,18 @@ export class AllureRuntime implements IAllureRuntime { } }; - statusDetails: IAllureRuntime['statusDetails'] = (statusDetails) => { + statusDetails: AllureRuntime['statusDetails'] = (statusDetails) => { // TODO: assert is not nullish this.#coreModule.statusDetails(statusDetails); }; - step: IAllureRuntime['step'] = (name, function_) => + step: AllureRuntime['step'] = (name, function_) => // TODO: assert name is a string // TODO: assert function_ is a function this.#basicStepsModule.step(name, function_); // @ts-expect-error TS2322: too few arguments - createStep: IAllureRuntime['createStep'] = ( + createStep: AllureRuntime['createStep'] = ( nameFormat, maybeParameters, maybeFunction, @@ -157,15 +157,14 @@ export class AllureRuntime implements IAllureRuntime { ); }; - attachment: IAllureRuntime['attachment'] = (name, content, mimeType) => + attachment: AllureRuntime['attachment'] = (name, content, mimeType) => // TODO: assert name is a string this.#contentAttachmentsModule.attachment(content, { name, mimeType, }); - // @ts-expect-error TS2322: is not assignable to type 'string' - fileAttachment: IAllureRuntime['fileAttachment'] = ( + fileAttachment: AllureRuntime['fileAttachment'] = ( filePath, nameOrOptions, ) => { @@ -178,7 +177,7 @@ export class AllureRuntime implements IAllureRuntime { return this.#fileAttachmentsModule.attachment(filePath, options); }; - createAttachment: IAllureRuntime['createAttachment'] = ( + createAttachment: AllureRuntime['createAttachment'] = ( function_, nameOrOptions, ) => { @@ -192,7 +191,7 @@ export class AllureRuntime implements IAllureRuntime { return this.#contentAttachmentsModule.createAttachment(function_, options); }; - createFileAttachment: IAllureRuntime['createFileAttachment'] = ( + createFileAttachment: AllureRuntime['createFileAttachment'] = ( function_, nameOrOptions, ) => { diff --git a/src/runtime/__snapshots__/AllureRuntime.test.ts.snap b/src/runtime/__snapshots__/runtime.test.ts.snap similarity index 100% rename from src/runtime/__snapshots__/AllureRuntime.test.ts.snap rename to src/runtime/__snapshots__/runtime.test.ts.snap diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 0a691b73..a886c09f 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1,3 +1,3 @@ export * from './types'; -export * from './AllureRuntime'; +export * from './AllureRuntimeImplementation'; export * from './AllureRuntimeContext'; diff --git a/src/runtime/modules/AttachmentsModule.ts b/src/runtime/modules/AttachmentsModule.ts index 39eb3236..8cc3d1c2 100644 --- a/src/runtime/modules/AttachmentsModule.ts +++ b/src/runtime/modules/AttachmentsModule.ts @@ -1,7 +1,11 @@ import path from 'node:path'; -import { formatString, hijackFunction, processMaybePromise } from '../../utils'; -import type { Function_, MaybePromise } from '../../utils'; +import { + type Function_, + type MaybePromise, + processMaybePromise, +} from '../../utils'; +import { formatString, hijackFunction } from '../../utils'; import type { AttachmentContent, AttachmentContext, @@ -18,7 +22,7 @@ import type { import type { AllureTestItemMetadataProxy } from '../../metadata'; import type { AllureRuntimeContext } from '../AllureRuntimeContext'; -export type AttachmentsModuleContext< +type AttachmentsModuleContext< Context extends AttachmentContext, Handler extends AttachmentHandler, > = { @@ -26,7 +30,7 @@ export type AttachmentsModuleContext< readonly inferMimeType: (context: MIMEInfererContext) => string | undefined; readonly metadata: AllureTestItemMetadataProxy; readonly outDir: string; - readonly waitFor: (promise: Promise) => void; + readonly waitFor: (promise: Promise) => Promise; }; abstract class AttachmentsModule< @@ -42,11 +46,12 @@ abstract class AttachmentsModule< attachment( content: MaybePromise, options: Options, - ): typeof content { + ): Promise { if ( typeof options.handler === 'string' && !this.context.handlers[options.handler] ) { + // TODO: throw a more specific error throw new Error(`Unknown attachment handler: ${options.handler}`); } @@ -70,7 +75,10 @@ abstract class AttachmentsModule< ): Context; #handleAttachment(userOptions: Options) { - return (userContent: Content, arguments_?: unknown[]) => { + return ( + userContent: Content, + arguments_?: unknown[], + ): Promise => { const handler = this.#resolveHandler(userOptions); const name = this.#formatName(userOptions.name, arguments_); const mimeContext = this._createMimeContext(name, userContent); @@ -78,6 +86,7 @@ abstract class AttachmentsModule< userOptions.mimeType ?? this.context.inferMimeType(mimeContext) ?? 'application/octet-stream'; + const context = this._createAttachmentContext({ name, mimeType, @@ -85,8 +94,9 @@ abstract class AttachmentsModule< sourcePath: mimeContext.sourcePath, content: mimeContext.content, }); + const pushAttachment = this.#schedulePushAttachment(context); - this.context.waitFor( + return this.context.waitFor( Promise.resolve() .then(() => handler(context)) .then(pushAttachment), @@ -101,9 +111,11 @@ abstract class AttachmentsModule< : this.context.handlers[handler]; } - #schedulePushAttachment(context: Context) { + #schedulePushAttachment( + context: Context, + ): (destinationPath: string | undefined) => typeof destinationPath { const metadata = this.context.metadata.$bind(); - return (destinationPath: string | undefined) => { + return (destinationPath) => { if (destinationPath) { metadata.push('attachments', [ { @@ -113,6 +125,8 @@ abstract class AttachmentsModule< }, ]); } + + return destinationPath; }; } diff --git a/src/runtime/modules/StepsDecorator.ts b/src/runtime/modules/StepsDecorator.ts index 57dff5a5..70226304 100644 --- a/src/runtime/modules/StepsDecorator.ts +++ b/src/runtime/modules/StepsDecorator.ts @@ -1,9 +1,9 @@ import type { Function_ } from '../../utils'; import { formatString, wrapFunction } from '../../utils'; -import type { IAllureRuntime, ParameterOrString } from '../types'; +import type { AllureRuntime, ParameterOrString } from '../types'; export type FunctionalStepsModuleContext = { - runtime: Pick; + runtime: Pick; }; export class StepsDecorator { diff --git a/src/runtime/AllureRuntime.test.ts b/src/runtime/runtime.test.ts similarity index 87% rename from src/runtime/AllureRuntime.test.ts rename to src/runtime/runtime.test.ts index e5c71a79..bbf54c0d 100644 --- a/src/runtime/AllureRuntime.test.ts +++ b/src/runtime/runtime.test.ts @@ -4,7 +4,7 @@ import { state } from 'jest-metadata'; import { AllureMetadataProxy } from '../metadata'; -import { AllureRuntime } from './AllureRuntime'; +import { AllureRuntimeImplementation } from './AllureRuntimeImplementation'; import type { SharedReporterConfig } from './types'; import { AllureRuntimeContext } from './AllureRuntimeContext'; @@ -35,8 +35,14 @@ describe('AllureRuntime', () => { }, }); - const runtime = new AllureRuntime(context); - runtime.attachment('attachment1', Buffer.from('first'), 'text/plain'); + const runtime = new AllureRuntimeImplementation(context); + const resultingPath = await runtime.attachment( + 'attachment1', + Buffer.from('first'), + 'text/plain', + ); + + expect(resultingPath).toBe('/attachments/first'); const innerStep3 = runtime.createStep( 'inner step 3', diff --git a/src/runtime/types.ts b/src/runtime/types.ts index abb4bfe2..51e78bc0 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -11,12 +11,12 @@ import type { import type { Function_, MaybePromise } from '../utils'; -export interface IAllureRuntime { +export interface AllureRuntime { /** * Advanced API for attaching metadata to the same step or test. * Useful when your artifacts are delayed and are created asynchronously. */ - $bind(options?: AllureRuntimeBindOptions): IAllureRuntime; + $bind(options?: AllureRuntimeBindOptions): AllureRuntime; /** * Attach a runtime plugin using a callback. @@ -50,7 +50,7 @@ export interface IAllureRuntime { name: string, content: MaybePromise, mimeType?: string, - ): typeof content; + ): Promise; createAttachment< T extends AttachmentContent, @@ -67,16 +67,14 @@ export interface IAllureRuntime { options: ContentAttachmentOptions, ): typeof function_; - fileAttachment(filePath: string, name?: string): string; - fileAttachment(filePath: string, options?: FileAttachmentOptions): string; fileAttachment( - filePathPromise: Promise, + filePath: string | Promise, name?: string, - ): Promise; + ): Promise; fileAttachment( - filePathPromise: Promise, + filePath: string | Promise, options?: FileAttachmentOptions, - ): Promise; + ): Promise; createFileAttachment>>( function_: F, @@ -104,7 +102,7 @@ export type AllureRuntimePluginCallback = ( ) => void; export interface AllureRuntimePluginContext { - readonly runtime: IAllureRuntime; + readonly runtime: AllureRuntime; readonly contentAttachmentHandlers: Record< BuiltinContentAttachmentHandler | 'default' | string, ContentAttachmentHandler diff --git a/src/utils/TaskQueue.ts b/src/utils/TaskQueue.ts index 39fb720c..b0127bb9 100644 --- a/src/utils/TaskQueue.ts +++ b/src/utils/TaskQueue.ts @@ -14,13 +14,13 @@ export class TaskQueue { readonly flush = () => this.#idle; - readonly enqueueTask = (task: MaybeFunction>) => { - this.#idle = + readonly enqueueTask = (task: MaybeFunction>): Promise => { + const result = (this.#idle = typeof task === 'function' - ? this.#idle.then(task) - : this.#idle.then(() => task); + ? this.#idle.then(task) + : this.#idle.then(() => task)); this.#idle = this.#idle.catch(this.#logError); - return this.#idle; + return result; }; } diff --git a/src/utils/hijackFunction.ts b/src/utils/hijackFunction.ts index 9c2f50f9..3ad4e1e1 100644 --- a/src/utils/hijackFunction.ts +++ b/src/utils/hijackFunction.ts @@ -27,8 +27,9 @@ export const hijackFunction: FunctionHijacker = (function_, callback) => { arguments_, ) as MaybePromise; - return processMaybePromise(result, (value) => - callback(value, arguments_), + return processMaybePromise( + result, + (value) => (callback(value, arguments_), value), ); }, ); diff --git a/src/utils/processMaybePromise.test.ts b/src/utils/processMaybePromise.test.ts index 58eefc8c..a70675d6 100644 --- a/src/utils/processMaybePromise.test.ts +++ b/src/utils/processMaybePromise.test.ts @@ -2,21 +2,22 @@ import { processMaybePromise } from './processMaybePromise'; describe('processMaybePromise', () => { // Test: Direct Value - it('should call the callback with a direct value', () => { - const callback = jest.fn(); + it('should call the callback with a direct value', async () => { + const callback = jest.fn(async (x: number) => ++x); const result = processMaybePromise(5, callback); expect(callback).toHaveBeenCalledWith(5); - expect(result).toBe(5); + await expect(result).resolves.toBe(6); }); // Test: Promise it('should call the callback with a resolved promise', async () => { - const callback = jest.fn(); + const callback = jest.fn(async (x: number) => ++x); const promise = Promise.resolve(10); const result = processMaybePromise(promise, callback); - await expect(result).resolves.toBe(10); + expect(callback).not.toHaveBeenCalledWith(10); + await expect(result).resolves.toBe(11); expect(callback).toHaveBeenCalledWith(10); }); diff --git a/src/utils/processMaybePromise.ts b/src/utils/processMaybePromise.ts index 0a5b9c14..251a68df 100644 --- a/src/utils/processMaybePromise.ts +++ b/src/utils/processMaybePromise.ts @@ -2,16 +2,19 @@ import { isPromiseLike } from './isPromiseLike'; import type { MaybePromise } from './types'; interface MaybePromiseProcessor { - (value: T, callback: (resolvedValue: T) => void): T; - (value: Promise, callback: (resolvedValue: T) => void): Promise; - ( + (value: T, callback: (resolvedValue: T) => Promise): Promise; + ( + value: Promise, + callback: (resolvedValue: T) => Promise, + ): Promise; + ( value: MaybePromise, - callback: (resolvedValue: T) => void, - ): MaybePromise; + callback: (resolvedValue: T) => Promise, + ): Promise; } export const processMaybePromise: MaybePromiseProcessor = (input, callback) => { return isPromiseLike(input) - ? input.then((resolvedValue) => (callback(resolvedValue), resolvedValue)) - : (callback(input), input); + ? input.then((resolvedValue) => callback(resolvedValue)) + : callback(input); }; From bf0323b81c180ba9734777d5b0ea24cfc590d90a Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 22 Mar 2024 22:15:39 +0200 Subject: [PATCH 14/50] refactored options --- index.d.ts | 428 ++++++++++-------- src/metadata/docblockMapping.ts | 4 +- .../aggregateHelpersCustomizers.ts | 4 +- src/options/compose-options/index.ts | 4 +- ...aggregateLabelCustomizers.ts => labels.ts} | 24 +- .../{aggregateLinkCustomizers.ts => links.ts} | 10 +- src/options/compose-options/parameters.ts | 0 .../compose-options/reporterOptions.ts | 16 +- src/options/compose-options/testCase.ts | 26 +- src/options/compose-options/testStep.ts | 6 +- src/options/customizers/helpers.test.ts | 47 ++ src/options/customizers/helpers.ts | 30 ++ src/options/customizers/property.test.ts | 35 ++ src/options/customizers/property.ts | 46 ++ src/options/customizers/testCase.ts | 167 +++++++ src/options/customizers/testStep.test.ts | 113 +++++ src/options/customizers/testStep.ts | 100 ++++ src/options/default-options/index.ts | 1 - src/options/default-options/testCase.ts | 16 +- src/options/default-options/testFile.ts | 43 +- src/options/default-options/testRun.ts | 23 +- src/options/extractors/asExtractor.ts | 27 +- src/options/extractors/extractors.test.ts | 19 +- src/options/extractors/index.ts | 2 + src/options/extractors/isExtractor.ts | 12 + src/options/extractors/last.ts | 10 + src/options/index.ts | 14 +- src/options/override-options/attachments.ts | 22 + src/options/override-options/historyId.ts | 10 + src/options/override-options/index.ts | 2 + src/options/types/compositeExtractors.ts | 84 ++++ src/options/types/index.ts | 0 src/reporter/JestAllure2Reporter.ts | 354 ++------------- src/reporter/{fallback => }/ThreadService.ts | 0 src/reporter/allureCommons.ts | 127 ++++++ .../{fallback/index.ts => fallbacks.ts} | 2 +- src/reporter/overwriteDirectory.ts | 8 + src/reporter/postProcessMetadata.ts | 32 ++ src/utils/compactObject.test.ts | 58 +++ src/utils/compactObject.ts | 8 + src/utils/index.ts | 2 + src/utils/isDefined.ts | 3 + tsconfig.json | 2 +- 43 files changed, 1316 insertions(+), 625 deletions(-) rename src/options/compose-options/{aggregateLabelCustomizers.ts => labels.ts} (75%) rename src/options/compose-options/{aggregateLinkCustomizers.ts => links.ts} (78%) create mode 100644 src/options/compose-options/parameters.ts create mode 100644 src/options/customizers/helpers.test.ts create mode 100644 src/options/customizers/helpers.ts create mode 100644 src/options/customizers/property.test.ts create mode 100644 src/options/customizers/property.ts create mode 100644 src/options/customizers/testCase.ts create mode 100644 src/options/customizers/testStep.test.ts create mode 100644 src/options/customizers/testStep.ts create mode 100644 src/options/extractors/isExtractor.ts create mode 100644 src/options/extractors/last.ts create mode 100644 src/options/override-options/attachments.ts create mode 100644 src/options/override-options/historyId.ts create mode 100644 src/options/override-options/index.ts create mode 100644 src/options/types/compositeExtractors.ts create mode 100644 src/options/types/index.ts rename src/reporter/{fallback => }/ThreadService.ts (100%) create mode 100644 src/reporter/allureCommons.ts rename src/reporter/{fallback/index.ts => fallbacks.ts} (94%) create mode 100644 src/reporter/overwriteDirectory.ts create mode 100644 src/reporter/postProcessMetadata.ts create mode 100644 src/utils/compactObject.test.ts create mode 100644 src/utils/compactObject.ts create mode 100644 src/utils/isDefined.ts diff --git a/index.d.ts b/index.d.ts index 553df588..ee577d51 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,7 +4,7 @@ import type {AggregatedResult, Config, TestCaseResult, TestResult} from '@jest/r import JestMetadataReporter from 'jest-metadata/reporter'; declare module 'jest-allure2-reporter' { - // region Config + // region Options export interface ReporterOptions extends ReporterOptionsAugmentation { /** @@ -39,63 +39,39 @@ declare module 'jest-allure2-reporter' { * `Product defects`, `Test defects` based on the test case status: * `failed` and `broken` respectively. */ - categories?: Category[] | CategoriesCustomizer; + categories?: TestRunPropertyCustomizer; /** * Configures the environment information that will be reported. */ - environment?: Record | EnvironmentCustomizer; + environment?: TestRunPropertyCustomizer>; /** * Configures the executor information that will be reported. */ - executor?: ExecutorInfo | ExecutorCustomizer; + executor?: TestRunPropertyCustomizer; /** * Customize extractor helpers object to use later in the customizers. */ - helpers?: Partial; + helpers?: HelpersCustomizer; /** * Customize how to report test runs (sessions) as pseudo-test cases. * This is normally used to report broken global setup and teardown hooks, * and to provide additional information about the test run. */ - testRun?: Partial; + testRun?: TestRunCustomizer; /** * Customize how to report test files as pseudo-test cases. * This is normally used to report broken test files, so that you can be aware of them, * but advanced users may find other use cases. */ - testFile?: Partial; + testFile?: TestFileCustomizer; /** * Customize how test cases are reported: names, descriptions, labels, status, etc. */ - testCase?: Partial; + testCase?: TestCaseCustomizer; /** * Customize how individual test steps are reported. */ - testStep?: Partial; - } - - export interface ReporterConfig extends ReporterConfigAugmentation { - overwrite: boolean; - resultsDir: string; - injectGlobals: boolean; - attachments: Required; - categories: CategoriesCustomizer; - environment: EnvironmentCustomizer; - executor: ExecutorCustomizer; - helpers: TestRunExtractor; - testRun: Required & { - labels: TestRunExtractor; - links: TestRunExtractor; - }; - testFile: Required & { - labels: TestFileExtractor; - links: TestFileExtractor; - }; - testCase: Required & { - labels: TestCaseExtractor; - links: TestCaseExtractor; - }; - testStep: Required; + testStep?: TestStepCustomizer; } export interface AttachmentsOptions { @@ -131,36 +107,21 @@ declare module 'jest-allure2-reporter' { // endregion - // region Allure Test Data - - export interface AllureTestCaseResult { - hidden: boolean; - historyId: string; - name: string; - fullName: string; - start: number; - stop: number; - description: string; - descriptionHtml: string; - stage: Stage; - status: Status; - statusDetails: StatusDetails; - labels: Label[]; - links: Link[]; - attachments: Attachment[]; - parameters: Parameter[]; - } + // region Config - export interface AllureTestStepResult { - name: string; - start: number; - stop: number; - stage: Stage; - status: Status; - statusDetails: StatusDetails; - steps: AllureTestStepResult[]; - attachments: Attachment[]; - parameters: Parameter[]; + export interface ReporterConfig extends ReporterConfigAugmentation { + overwrite: boolean; + resultsDir: string; + injectGlobals: boolean; + attachments: Required; + categories: CategoriesExtractor; + environment: EnvironmentExtractor; + executor: ExecutorExtractor; + helpers: HelpersExtractor; + testCase: TestCaseExtractor; + testFile: TestFileExtractor; + testRun: TestRunExtractor; + testStep: TestStepExtractor; } // endregion @@ -174,60 +135,56 @@ declare module 'jest-allure2-reporter' { /** * Extractor to omit test file cases from the report. */ - hidden: TestCaseExtractor; - /** - * Extractor to augment the test case context and the $ helper object. - */ - $: TestFileExtractor; + hidden?: TestCasePropertyCustomizer; /** * Test case ID extractor to fine-tune Allure's history feature. * @example ({ package, file, test }) => `${package.name}:${file.path}:${test.fullName}` * @example ({ test }) => `${test.identifier}:${test.title}` * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/history/#test-case-id */ - historyId: TestCaseExtractor; + historyId?: TestCasePropertyCustomizer; /** * Extractor for the default test or step name. * @default ({ test }) => test.title */ - name: TestCaseExtractor; + name?: TestCasePropertyCustomizer; /** * Extractor for the full test case name. * @default ({ test }) => test.fullName */ - fullName: TestCaseExtractor; + fullName?: TestCasePropertyCustomizer; /** * Extractor for the test case start timestamp. */ - start: TestCaseExtractor; + start?: TestCasePropertyCustomizer; /** * Extractor for the test case stop timestamp. */ - stop: TestCaseExtractor; + stop?: TestCasePropertyCustomizer; /** * Extractor for the test case description. * @example ({ testCaseMetadata }) => '```js\n' + testCaseMetadata.sourceCode + '\n```' */ - description: TestCaseExtractor; + description?: TestCasePropertyCustomizer; /** * Extractor for the test case description in HTML format. * @example ({ testCaseMetadata }) => '
' + testCaseMetadata.sourceCode + '
' */ - descriptionHtml: TestCaseExtractor; + descriptionHtml?: TestCasePropertyCustomizer; /** * Extractor for the test case stage. */ - stage: TestCaseExtractor; + stage?: TestCasePropertyCustomizer; /** * Extractor for the test case status. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * @example ({ value }) => value === 'broken' ? 'failed' : value */ - status: TestCaseExtractor; + status?: TestCasePropertyCustomizer; /** * Extractor for the test case status details. */ - statusDetails: TestCaseExtractor; + statusDetails?: TestCasePropertyCustomizer; /** * Customize Allure labels for the test case. * @@ -237,12 +194,7 @@ declare module 'jest-allure2-reporter' { * subSuite: ({ test }) => test.ancestorTitles[0], * } */ - labels: - | TestCaseExtractor - | Record< - LabelName | string, - string | string[] | TestCaseExtractor - >; + labels?: TestCaseLabelsCustomizer; /** * Resolve issue links for the test case. * @@ -255,17 +207,15 @@ declare module 'jest-allure2-reporter' { * }), * } */ - links: - | TestCaseExtractor - | Record>; + links?: TestCaseLinksCustomizer; /** * Customize step or test case attachments. */ - attachments: TestCaseExtractor; + attachments?: TestCaseAttachmentsCustomizer; /** * Customize step or test case parameters. */ - parameters: TestCaseExtractor; + parameters?: TestCaseParametersCustomizer; } /** @@ -276,48 +226,44 @@ declare module 'jest-allure2-reporter' { /** * Extractor to omit test steps from the report. */ - hidden: TestStepExtractor; - /** - * Extractor to augment the test step context and the $ helper object. - */ - $: TestFileExtractor; + hidden?: TestStepPropertyCustomizer; /** * Extractor for the step name. * @example ({ value }) => value.replace(/(before|after)(Each|All)/, (_, p1, p2) => p1 + ' ' + p2.toLowerCase()) */ - name: TestStepExtractor; + name?: TestStepPropertyCustomizer; /** * Extractor for the test step start timestamp. */ - start: TestStepExtractor; + start?: TestStepPropertyCustomizer; /** * Extractor for the test step stop timestamp. */ - stop: TestStepExtractor; + stop?: TestStepPropertyCustomizer; /** * Extractor for the test step stage. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * TODO: add example */ - stage: TestStepExtractor; + stage?: TestStepPropertyCustomizer; /** * Extractor for the test step status. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * @example ({ value }) => value === 'broken' ? 'failed' : value */ - status: TestStepExtractor; + status?: TestStepPropertyCustomizer; /** * Extractor for the test step status details. */ - statusDetails: TestStepExtractor; + statusDetails?: TestStepPropertyCustomizer; /** * Customize step or test step attachments. */ - attachments: TestStepExtractor; + attachments?: TestStepAttachmentsCustomizer; /** * Customize step or test step parameters. */ - parameters: TestStepExtractor; + parameters?: TestStepParametersCustomizer; } /** @@ -327,58 +273,54 @@ declare module 'jest-allure2-reporter' { /** * Extractor to omit test file cases from the report. */ - hidden: TestFileExtractor; - /** - * Extractor to augment the test file context and the $ helper object. - */ - $: TestFileExtractor; + hidden?: TestFilePropertyCustomizer; /** * Test file ID extractor to fine-tune Allure's history feature. * @default ({ filePath }) => filePath.join('/') * @example ({ package, filePath }) => `${package.name}:${filePath.join('/')}` * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/history/#test-case-id */ - historyId: TestFileExtractor; + historyId?: TestFilePropertyCustomizer; /** * Extractor for test file name * @default ({ filePath }) => filePath.at(-1) */ - name: TestFileExtractor; + name?: TestFilePropertyCustomizer; /** * Extractor for the full test file name * @default ({ testFile }) => testFile.testFilePath */ - fullName: TestFileExtractor; + fullName?: TestFilePropertyCustomizer; /** * Extractor for the test file start timestamp. */ - start: TestFileExtractor; + start?: TestFilePropertyCustomizer; /** * Extractor for the test file stop timestamp. */ - stop: TestFileExtractor; + stop?: TestFilePropertyCustomizer; /** * Extractor for the test file description. */ - description: TestFileExtractor; + description?: TestFilePropertyCustomizer; /** * Extractor for the test file description in HTML format. */ - descriptionHtml: TestFileExtractor; + descriptionHtml?: TestFilePropertyCustomizer; /** * Extractor for the test file stage. */ - stage: TestFileExtractor; + stage?: TestFilePropertyCustomizer; /** * Extractor for the test file status. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * @example ({ value }) => value === 'broken' ? 'failed' : value */ - status: TestFileExtractor; + status?: TestFilePropertyCustomizer; /** * Extractor for the test file status details. */ - statusDetails: TestFileExtractor; + statusDetails?: TestFilePropertyCustomizer; /** * Customize Allure labels for the test file. * @@ -388,12 +330,7 @@ declare module 'jest-allure2-reporter' { * subSuite: ({ test }) => test.ancestorTitles[0], * } */ - labels: - | TestFileExtractor - | Record< - LabelName | string, - string | string[] | TestFileExtractor - >; + labels?: TestFileLabelsCustomizer; /** * Resolve issue links for the test file. * @@ -406,17 +343,15 @@ declare module 'jest-allure2-reporter' { * }), * } */ - links: - | TestFileExtractor - | Record>; + links?: TestFileLinksCustomizer; /** * Customize test file attachments. */ - attachments: TestFileExtractor; + attachments?: TestFileAttachmentsCustomizer; /** * Customize test case parameters. */ - parameters: TestFileExtractor; + parameters?: TestFileParametersCustomizer; } /** @@ -426,142 +361,202 @@ declare module 'jest-allure2-reporter' { /** * Extractor to omit pseudo-test cases for test runs from the report. */ - hidden: TestRunExtractor; - /** - * Extractor to augment the test run context and the $ helper object. - */ - $: TestRunExtractor; + hidden?: TestRunPropertyCustomizer; /** * Test run ID extractor to fine-tune Allure's history feature. * @default () => process.argv.slice(2).join(' ') */ - historyId: TestRunExtractor; + historyId?: TestRunPropertyCustomizer; /** * Extractor for test run name * @default () => '(test run)' */ - name: TestRunExtractor; + name?: TestRunPropertyCustomizer; /** * Extractor for the full test run name * @default () => process.argv.slice(2).join(' ') */ - fullName: TestRunExtractor; + fullName?: TestRunPropertyCustomizer; /** * Extractor for the test run start timestamp. */ - start: TestRunExtractor; + start?: TestRunPropertyCustomizer; /** * Extractor for the test run stop timestamp. */ - stop: TestRunExtractor; + stop?: TestRunPropertyCustomizer; /** * Extractor for the test run description. * Use this to provide additional information about the test run, * which is not covered by the default Allure reporter capabilities. */ - description: TestRunExtractor; + description?: TestRunPropertyCustomizer; /** * Extractor for the test run description in HTML format. * @see {@link TestRunCustomizer#description} */ - descriptionHtml: TestRunExtractor; + descriptionHtml?: TestRunPropertyCustomizer; /** * Extractor for the test run stage. * 'interrupted' is used for failures with `--bail` enabled. * Otherwise, 'finished' is used. */ - stage: TestRunExtractor; + stage?: TestRunPropertyCustomizer; /** * Extractor for the test run status. * Either 'passed' or 'failed'. */ - status: TestRunExtractor; + status?: TestRunPropertyCustomizer; /** * Extractor for the test file status details. */ - statusDetails: TestRunExtractor; + statusDetails?: TestRunPropertyCustomizer; /** * Customize Allure labels for the pseudo-test case representing the test run. */ - labels: - | TestRunExtractor - | Record< - LabelName | string, - string | string[] | TestRunExtractor - >; + labels?: TestRunLabelsCustomizer; /** * Customize Allure links for the pseudo-test case representing the test run. */ - links: - | TestRunExtractor - | Record>; + links?: TestRunLinksCustomizer; /** * Customize test run attachments. */ - attachments: TestRunExtractor; + attachments?: TestRunAttachmentsCustomizer; /** * Customize test run parameters. */ - parameters: TestRunExtractor; - } - - /** - * Override or add more helper functions to the default extractor helpers. - */ - export interface ExtractorHelpersCustomizer { - [key: keyof ExtractorHelpers]: ExtractorHelperExtractor; - } + parameters?: TestRunParametersCustomizer; + } + + export type HelpersCustomizer = HelpersExtractor> | Partial; + + export type TestRunAttachmentsCustomizer = TestRunPropertyCustomizer; + export type TestRunLabelsCustomizer = + | TestRunPropertyCustomizer + | Record>; + export type TestRunLinksCustomizer = + | TestRunPropertyCustomizer + | Record>; + export type TestRunParametersCustomizer = + | TestRunPropertyCustomizer + | Record[]>>; + + export type TestFileAttachmentsCustomizer = TestFilePropertyCustomizer; + export type TestFileLabelsCustomizer = + | TestFilePropertyCustomizer + | Record>; + export type TestFileLinksCustomizer = + | TestFilePropertyCustomizer + | Record>; + export type TestFileParametersCustomizer = + | TestFilePropertyCustomizer + | Record[]>>; + + export type TestCaseAttachmentsCustomizer = TestCasePropertyCustomizer; + export type TestCaseLabelsCustomizer = + | TestCasePropertyCustomizer + | Record>; + export type TestCaseLinksCustomizer = + | TestCasePropertyCustomizer + | Record>; + export type TestCaseParametersCustomizer = + | TestCasePropertyCustomizer + | Record[]>>; + + export type TestStepAttachmentsCustomizer = TestStepPropertyCustomizer; + export type TestStepParametersCustomizer = + | TestStepPropertyCustomizer + | Record[]>>; + + export type TestRunPropertyCustomizer = T | Ta | TestRunPropertyExtractor; + + export type TestFilePropertyCustomizer = T | Ta | TestFilePropertyExtractor; + + export type TestCasePropertyCustomizer = T | Ta | TestCasePropertyExtractor; + + export type TestStepPropertyCustomizer = T | Ta | TestStepPropertyExtractor; - export type EnvironmentCustomizer = TestRunExtractor>; - - export type ExecutorCustomizer = TestRunExtractor; + // endregion - export type CategoriesCustomizer = TestRunExtractor; + // region Extractors export type Extractor< - T = unknown, - C extends ExtractorContext = ExtractorContext, - R = T, - > = (context: Readonly) => R | undefined | Promise; - - export type ExtractorHelperExtractor = Extractor< - ExtractorHelpers[K], - ExtractorHelpers, - ExtractorHelpers[K] + T, + Ta = never, + C extends ExtractorContext = ExtractorContext, + > = (context: Readonly) => T | Ta | Promise; + + export type GlobalExtractor = Extractor< + T, + Ta, + GlobalExtractorContext >; - export type TestRunExtractor = Extractor< + export type TestRunPropertyExtractor = Extractor< T, - TestRunExtractorContext, - R + Ta, + TestRunExtractorContext >; - export type TestFileExtractor = Extractor< + export type TestFilePropertyExtractor = Extractor< T, - TestFileExtractorContext, - R + Ta, + TestFileExtractorContext >; - export type TestCaseExtractor = Extractor< + export type TestCasePropertyExtractor = Extractor< T, - TestCaseExtractorContext, - R + Ta, + TestCaseExtractorContext >; - export type TestStepExtractor = Extractor< + export type TestStepPropertyExtractor = Extractor< T, - TestStepExtractorContext, - R + Ta, + TestStepExtractorContext + >; + + export type HelpersExtractor = GlobalExtractor; + + export type EnvironmentExtractor = TestRunPropertyExtractor>; + + export type ExecutorExtractor = TestRunPropertyExtractor; + + export type CategoriesExtractor = TestRunPropertyExtractor; + + export type TestCaseExtractor = Extractor< + AllureTestCaseResult, + undefined, + TestCaseExtractorContext> + >; + + export type TestStepExtractor = Extractor< + AllureTestStepResult, + undefined, + TestStepExtractorContext> + >; + + export type TestFileExtractor = Extractor< + AllureTestCaseResult, + undefined, + TestFileExtractorContext> + >; + + export type TestRunExtractor = Extractor< + AllureTestCaseResult, + undefined, + TestRunExtractorContext> >; export interface ExtractorContext { - readonly value: T | undefined | Promise; + readonly value: T | Promise; } export interface GlobalExtractorContext extends ExtractorContext, GlobalExtractorContextAugmentation { - $: ExtractorHelpers; + $: Helpers; globalConfig: Config.GlobalConfig; config: ReporterConfig; } @@ -570,12 +565,14 @@ declare module 'jest-allure2-reporter' { extends GlobalExtractorContext, TestRunExtractorContextAugmentation { aggregatedResult: AggregatedResult; + result: Partial; } export interface TestFileExtractorContext extends GlobalExtractorContext, TestFileExtractorContextAugmentation { filePath: string[]; + result: Partial; testFile: TestResult; testFileMetadata: AllureTestFileMetadata; } @@ -583,6 +580,7 @@ declare module 'jest-allure2-reporter' { export interface TestCaseExtractorContext extends TestFileExtractorContext, TestCaseExtractorContextAugmentation { + result: Partial; testCase: TestCaseResult; testCaseMetadata: AllureTestCaseMetadata; } @@ -590,10 +588,11 @@ declare module 'jest-allure2-reporter' { export interface TestStepExtractorContext extends TestCaseExtractorContext, TestStepExtractorContextAugmentation { + result: Partial; testStepMetadata: AllureTestStepMetadata; } - export interface ExtractorHelpers extends ExtractorHelpersAugmentation { + export interface Helpers extends HelpersAugmentation { /** * Extracts the source code of the current test case, test step or a test file. * Pass `true` as the second argument to extract source code recursively from all steps. @@ -654,13 +653,15 @@ declare module 'jest-allure2-reporter' { export type ExtractorManifestHelperCallback = (manifest: Record) => T; export interface ExtractorHelperSourceCode { - fileName: string; code: string; language: string; + fileName: string; + lineNumber: number; + columnNumber: number; } export interface StripAnsiHelper { - (text: R): R; + (textOrObject: R): R; } // endregion @@ -787,8 +788,8 @@ declare module 'jest-allure2-reporter' { // Use to extend ReporterConfig } - export interface ExtractorHelpersAugmentation { - // Use to extend ExtractorHelpers + export interface HelpersAugmentation { + // Use to extend Helpers } export interface GlobalExtractorContextAugmentation { @@ -813,6 +814,43 @@ declare module 'jest-allure2-reporter' { // endregion + // region Allure Test Data + + export interface AllureTestCaseResult { + hidden: boolean; + historyId: string; + displayName: string; + fullName: string; + start: number; + stop: number; + description: string; + descriptionHtml: string; + stage: Stage; + status: Status; + statusDetails?: StatusDetails; + steps?: AllureTestStepResult[]; + labels?: Label[]; + links?: Link[]; + attachments?: Attachment[]; + parameters?: Parameter[]; + } + + export interface AllureTestStepResult { + hidden: boolean; + hookType: AllureTestStepMetadata['hookType']; + displayName: string; + start: number; + stop: number; + stage: Stage; + status: Status; + statusDetails: StatusDetails; + steps: AllureTestStepResult[]; + attachments: Attachment[]; + parameters: Parameter[]; + } + + // endregion + //region Allure Vendor types export interface Attachment { @@ -879,7 +917,7 @@ declare module 'jest-allure2-reporter' { export interface Parameter { name: string; - value: string; + value: Primitive; excluded?: boolean; mode?: 'hidden' | 'masked' | 'default'; } @@ -894,6 +932,8 @@ declare module 'jest-allure2-reporter' { } //endregion + + export type Primitive = string | number | boolean | null | undefined; } export default class JestAllure2Reporter extends JestMetadataReporter { diff --git a/src/metadata/docblockMapping.ts b/src/metadata/docblockMapping.ts index 766f494b..d26b193d 100644 --- a/src/metadata/docblockMapping.ts +++ b/src/metadata/docblockMapping.ts @@ -8,7 +8,7 @@ import type { LinkType, } from 'jest-allure2-reporter'; -import { asArray } from '../utils'; +import {asArray, isDefined} from '../utils'; const ALL_LABELS = Object.keys( assertType>({ @@ -60,7 +60,7 @@ export function mapTestCaseDocblock( const links = ALL_LINKS.flatMap((name) => asArray(pragmas[name]).map(createLinkMapper(name)), - ).filter(Boolean); + ).filter(isDefined); if (links.length > 0) metadata.links = links; diff --git a/src/options/compose-options/aggregateHelpersCustomizers.ts b/src/options/compose-options/aggregateHelpersCustomizers.ts index 7dfb45ca..d8355a1c 100644 --- a/src/options/compose-options/aggregateHelpersCustomizers.ts +++ b/src/options/compose-options/aggregateHelpersCustomizers.ts @@ -1,8 +1,8 @@ -import type { ExtractorHelpers, TestRunExtractor } from 'jest-allure2-reporter'; +import type { Helpers, TestRunPropertyExtractor } from 'jest-allure2-reporter'; export function aggregateHelpersCustomizers( a: any, -): TestRunExtractor { +): TestRunPropertyExtractor { // TODO: Implement return a; } diff --git a/src/options/compose-options/index.ts b/src/options/compose-options/index.ts index cd8bedb2..c23ef95b 100644 --- a/src/options/compose-options/index.ts +++ b/src/options/compose-options/index.ts @@ -1,5 +1,5 @@ -export { aggregateLabelCustomizers } from './aggregateLabelCustomizers'; +export { labels } from './labels'; -export { aggregateLinkCustomizers } from './aggregateLinkCustomizers'; +export { links } from './links'; export { reporterOptions } from './reporterOptions'; diff --git a/src/options/compose-options/aggregateLabelCustomizers.ts b/src/options/compose-options/labels.ts similarity index 75% rename from src/options/compose-options/aggregateLabelCustomizers.ts rename to src/options/compose-options/labels.ts index a81b6f45..5e7cb35c 100644 --- a/src/options/compose-options/aggregateLabelCustomizers.ts +++ b/src/options/compose-options/labels.ts @@ -2,29 +2,31 @@ import type { Extractor, ExtractorContext, - TestFileCustomizer, - TestCaseCustomizer, + TestFilePropertyCustomizer, + TestCasePropertyCustomizer, } from 'jest-allure2-reporter'; import type { Label } from 'jest-allure2-reporter'; import { flatMapAsync } from '../../utils/flatMapAsync'; -import { asArray } from '../../utils'; +import { asArray, constant } from '../../utils'; import { asExtractor } from '../extractors'; -type Customizer = TestFileCustomizer | TestCaseCustomizer; +type Customizer = TestFilePropertyCustomizer | TestCasePropertyCustomizer; -const constant = (value: T) => () => value; +function isExtractor(value: unknown): value is Extractor { + return typeof value === 'function'; +} -export function aggregateLabelCustomizers( - labels: C['labels'] | undefined, -): Extractor | undefined { - if (!labels || typeof labels === 'function') { - return labels as Extractor | undefined; +export function labels( + labels: C['labels'], +): Extractor { + if (isExtractor(labels)) { + return labels; } const extractors = Object.keys(labels).reduce( (accumulator, key) => { - const extractor = asExtractor(labels[key]); + const extractor = asExtractor(labels[key]); if (extractor) { accumulator[key] = extractor; } diff --git a/src/options/compose-options/aggregateLinkCustomizers.ts b/src/options/compose-options/links.ts similarity index 78% rename from src/options/compose-options/aggregateLinkCustomizers.ts rename to src/options/compose-options/links.ts index a6c4d465..e34abacf 100644 --- a/src/options/compose-options/aggregateLinkCustomizers.ts +++ b/src/options/compose-options/links.ts @@ -1,17 +1,17 @@ import type { Extractor, ExtractorContext, - TestRunCustomizer, - TestFileCustomizer, - TestCaseCustomizer, + TestRunPropertyCustomizer, + TestFilePropertyCustomizer, + TestCasePropertyCustomizer, } from 'jest-allure2-reporter'; import type { Link } from 'jest-allure2-reporter'; import { isPromiseLike } from '../../utils'; -type Customizer = TestFileCustomizer | TestCaseCustomizer | TestRunCustomizer; +type Customizer = TestFilePropertyCustomizer | TestCasePropertyCustomizer | TestRunPropertyCustomizer; -export function aggregateLinkCustomizers( +export function links( links: C['links'] | undefined, ): Extractor | undefined { if (!links || typeof links === 'function') { diff --git a/src/options/compose-options/parameters.ts b/src/options/compose-options/parameters.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/options/compose-options/reporterOptions.ts b/src/options/compose-options/reporterOptions.ts index 07d89cb1..2b290f04 100644 --- a/src/options/compose-options/reporterOptions.ts +++ b/src/options/compose-options/reporterOptions.ts @@ -1,14 +1,14 @@ import type { ReporterOptions, ReporterConfig, - TestStepCustomizer, + TestStepPropertyCustomizer, } from 'jest-allure2-reporter'; import { asExtractor, composeExtractors } from '../extractors'; import { composeAttachments } from './attachments'; -import { composeTestCaseCustomizers } from './testCase'; -import { composeTestStepCustomizers } from './testStep'; +import { composeTestCasePropertyCustomizers } from './testCase'; +import { composeTestStepPropertyCustomizers } from './testStep'; import { aggregateHelpersCustomizers } from './aggregateHelpersCustomizers'; export function reporterOptions( @@ -39,11 +39,11 @@ export function reporterOptions( aggregateHelpersCustomizers(custom.helpers), base.helpers, ), - testRun: composeTestCaseCustomizers(base.testRun, custom.testRun), - testCase: composeTestCaseCustomizers(base.testCase, custom.testCase), - testFile: composeTestCaseCustomizers(base.testFile, custom.testFile), - testStep: composeTestStepCustomizers( - base.testStep as TestStepCustomizer, + testRun: composeTestCasePropertyCustomizers(base.testRun, custom.testRun), + testCase: composeTestCasePropertyCustomizers(base.testCase, custom.testCase), + testFile: composeTestCasePropertyCustomizers(base.testFile, custom.testFile), + testStep: composeTestStepPropertyCustomizers( + base.testStep as TestStepPropertyCustomizer, custom.testStep, ), }; diff --git a/src/options/compose-options/testCase.ts b/src/options/compose-options/testCase.ts index aa39f494..898213e1 100644 --- a/src/options/compose-options/testCase.ts +++ b/src/options/compose-options/testCase.ts @@ -1,25 +1,25 @@ -import type { ReporterConfig, TestCaseCustomizer } from 'jest-allure2-reporter'; +import type { ReporterConfig, TestCasePropertyCustomizer } from 'jest-allure2-reporter'; import { composeExtractors } from '../extractors'; -import { aggregateLabelCustomizers } from './aggregateLabelCustomizers'; -import { aggregateLinkCustomizers } from './aggregateLinkCustomizers'; +import { labels } from './labels'; +import { links } from './links'; -export function composeTestCaseCustomizers( +export function composeTestCasePropertyCustomizers( base: ReporterConfig['testRun'], - custom: Partial | undefined, + custom: Partial | undefined, ): typeof base; -export function composeTestCaseCustomizers( +export function composeTestCasePropertyCustomizers( base: ReporterConfig['testFile'], - custom: Partial | undefined, + custom: Partial | undefined, ): typeof base; -export function composeTestCaseCustomizers( +export function composeTestCasePropertyCustomizers( base: ReporterConfig['testCase'], - custom: Partial | undefined, + custom: Partial | undefined, ): typeof base; -export function composeTestCaseCustomizers( +export function composeTestCasePropertyCustomizers( base: ReporterConfig['testRun' | 'testFile' | 'testCase'], - custom: Partial | undefined, + custom: Partial | undefined, ): typeof base { if (!custom) { return base; @@ -44,11 +44,11 @@ export function composeTestCaseCustomizers( attachments: composeExtractors(custom.attachments, base.attachments), parameters: composeExtractors(custom.parameters, base.parameters), labels: composeExtractors( - aggregateLabelCustomizers(custom.labels), + labels(custom.labels), base.labels, ), links: composeExtractors( - aggregateLinkCustomizers(custom.links), + links(custom.links), base.links, ), }; diff --git a/src/options/compose-options/testStep.ts b/src/options/compose-options/testStep.ts index e457dea6..2cda516b 100644 --- a/src/options/compose-options/testStep.ts +++ b/src/options/compose-options/testStep.ts @@ -1,10 +1,10 @@ -import type { ReporterConfig, TestStepCustomizer } from 'jest-allure2-reporter'; +import type { ReporterConfig, TestStepPropertyCustomizer } from 'jest-allure2-reporter'; import { composeExtractors } from '../extractors'; -export function composeTestStepCustomizers( +export function composeTestStepPropertyCustomizers( base: ReporterConfig['testStep'], - custom: Partial | undefined, + custom: Partial | undefined, ): typeof base { if (!custom) { return base; diff --git a/src/options/customizers/helpers.test.ts b/src/options/customizers/helpers.test.ts new file mode 100644 index 00000000..4a4cd01c --- /dev/null +++ b/src/options/customizers/helpers.test.ts @@ -0,0 +1,47 @@ +import type { + GlobalExtractorContext, + ExtractorContext, + Helpers, +} from 'jest-allure2-reporter'; + +import { helpersCustomizer } from './helpers'; + +describe('helpersCustomizer', () => { + it('should return undefined when the input is undefined or null', () => { + expect(helpersCustomizer(void 0)).toBeUndefined(); + expect(helpersCustomizer(null as any)).toBeUndefined(); + }); + + it('should return the input value if it is an extractor function', () => { + const extractorFunction = jest.fn(); + expect(helpersCustomizer(extractorFunction)).toBe(extractorFunction); + }); + + it('should return undefined if the input is an empty object', () => { + expect(helpersCustomizer({})).toBeUndefined(); + }); + + it('should return an extractor function that merges the input shape with the existing value', async () => { + const defaultHelpers: Partial = { + getExecutorInfo: jest.fn(), + extractSourceCode: jest.fn(), + }; + + const customHelpers: Partial = { + extractSourceCode: undefined, + stripAnsi: jest.fn(), + }; + + const context: ExtractorContext = { + value: defaultHelpers as Helpers, + }; + + const extractor = helpersCustomizer(customHelpers)!; + + const result = await extractor( + context as unknown as GlobalExtractorContext, + ); + + expect(result).toEqual(customHelpers); + }); +}); diff --git a/src/options/customizers/helpers.ts b/src/options/customizers/helpers.ts new file mode 100644 index 00000000..763fc2e0 --- /dev/null +++ b/src/options/customizers/helpers.ts @@ -0,0 +1,30 @@ +import type { + Helpers, + HelpersCustomizer, + HelpersExtractor, +} from 'jest-allure2-reporter'; + +import { + asExtractor, + isExtractorFn as isExtractorFunction, +} from '../extractors'; +import { compactObject } from '../../utils'; + +export function helpersCustomizer( + value: HelpersCustomizer | undefined, +): HelpersExtractor> | undefined { + if (isExtractorFunction>>(value)) { + return value; + } + + if (value == null) { + return; + } + + const compact = compactObject(value); + if (Object.keys(compact).length === 0) { + return; + } + + return asExtractor>(value); +} diff --git a/src/options/customizers/property.test.ts b/src/options/customizers/property.test.ts new file mode 100644 index 00000000..db97b4ed --- /dev/null +++ b/src/options/customizers/property.test.ts @@ -0,0 +1,35 @@ +import type { + Extractor, + TestCasePropertyCustomizer, + TestFilePropertyCustomizer, + TestRunPropertyCustomizer, + TestStepPropertyCustomizer, + TestCasePropertyExtractor, + TestFilePropertyExtractor, + TestRunPropertyExtractor, + TestStepPropertyExtractor, +} from 'jest-allure2-reporter'; + +import { asExtractor } from '../extractors'; + +export function propertyCustomizer( + value: TestRunPropertyCustomizer, +): TestRunPropertyExtractor | undefined; +export function propertyCustomizer( + value: TestFilePropertyCustomizer, +): TestFilePropertyExtractor | undefined; +export function propertyCustomizer( + value: TestCasePropertyCustomizer, +): TestCasePropertyExtractor | undefined; +export function propertyCustomizer( + value: TestStepPropertyCustomizer, +): TestStepPropertyExtractor | undefined; +export function propertyCustomizer( + value: T | Extractor, +): Extractor | undefined { + if (value == null) { + return; + } + + return asExtractor(value); +} diff --git a/src/options/customizers/property.ts b/src/options/customizers/property.ts new file mode 100644 index 00000000..d5b53d02 --- /dev/null +++ b/src/options/customizers/property.ts @@ -0,0 +1,46 @@ +import type { + TestCasePropertyCustomizer, + TestFilePropertyCustomizer, + TestRunPropertyCustomizer, + TestStepPropertyCustomizer, + TestCasePropertyExtractor, + TestFilePropertyExtractor, + TestRunPropertyExtractor, + TestStepPropertyExtractor, +} from 'jest-allure2-reporter'; + +import { asExtractor } from '../extractors'; + +export function propertyCustomizer( + value: TestRunPropertyCustomizer, +): TestRunPropertyExtractor; +export function propertyCustomizer( + value: TestFilePropertyCustomizer, +): TestFilePropertyExtractor; +export function propertyCustomizer( + value: TestCasePropertyCustomizer, +): TestCasePropertyExtractor; +export function propertyCustomizer( + value: TestStepPropertyCustomizer, +): TestStepPropertyExtractor; +export function propertyCustomizer( + value: + | TestRunPropertyCustomizer + | TestFilePropertyCustomizer + | TestCasePropertyCustomizer + | TestStepPropertyCustomizer + | T + | R + | undefined, +): + | TestCasePropertyExtractor + | TestFilePropertyExtractor + | TestRunPropertyExtractor + | TestStepPropertyExtractor + | undefined { + if (value == null) { + return; + } + + return asExtractor(value as any); +} diff --git a/src/options/customizers/testCase.ts b/src/options/customizers/testCase.ts new file mode 100644 index 00000000..b1d942c8 --- /dev/null +++ b/src/options/customizers/testCase.ts @@ -0,0 +1,167 @@ +import type { + AllureTestCaseResult, + TestCaseExtractor, + TestFileExtractor, + TestRunExtractor, + TestStepExtractor, +} from 'jest-allure2-reporter'; + +import type { + TestCaseCompositeExtractor, + TestFileCompositeExtractor, + TestRunCompositeExtractor, +} from '../types/compositeExtractors'; +import { isDefined } from '../../utils'; + +export function testItemCustomizer( + testCase: TestCaseCompositeExtractor, + testStep: TestStepExtractor, + metadataKey?: 'testCaseMetadata' | 'testFileMetadata', +): TestCaseExtractor { + const extractor: TestCaseExtractor = async (context) => { + const result: Partial = {}; + result.hidden = await testCase.hidden({ + ...context, + value: false, + result, + }); + + if (result.hidden) { + return; + } + + result.displayName = await testCase.displayName({ + ...context, + value: '', + result, + }); + + result.start = await testCase.start({ + ...context, + value: Number.NaN, + result, + }); + + result.stop = await testCase.stop({ + ...context, + value: Number.NaN, + result, + }); + + result.fullName = await testCase.fullName({ + ...context, + value: '', + result, + }); + + result.historyId = await testCase.historyId({ + ...context, + value: '', + result, + }); + + result.stage = await testCase.stage({ + ...context, + value: 'scheduled', + result, + }); + + result.status = await testCase.status({ + ...context, + value: 'unknown', + result, + }); + + result.statusDetails = await testCase.statusDetails({ + ...context, + value: {}, + result, + }); + + result.labels = await testCase.labels({ + ...context, + value: [], + result, + }); + + result.links = await testCase.links({ + ...context, + value: [], + result, + }); + + result.attachments = await testCase.attachments({ + ...context, + value: [], + result, + }); + + result.parameters = await testCase.parameters({ + ...context, + value: [], + result, + }); + + result.description = await testCase.description({ + ...context, + value: '', + result, + }); + + result.descriptionHtml = await testCase.descriptionHtml({ + ...context, + value: '', + result, + }); + + const steps = metadataKey ? context[metadataKey].steps : undefined; + if (steps && steps.length > 0) { + const allSteps = await Promise.all( + steps.map(async (testStepMetadata) => { + const stepResult = await testStep({ + ...context, + testStepMetadata, + value: {}, + result, + }); + + return stepResult; + }), + ); + + result.steps = allSteps.filter(isDefined); + } + + return result as AllureTestCaseResult; + }; + + return extractor; +} + +export function testCaseCustomizer( + testCase: TestCaseCompositeExtractor, + testStep: TestStepExtractor, +): TestCaseExtractor { + return testItemCustomizer(testCase, testStep, 'testCaseMetadata'); +} + +export function testFileCustomizer( + testFile: TestFileCompositeExtractor, + testStep: TestStepExtractor, +): TestFileExtractor { + return testItemCustomizer( + testFile as unknown as TestCaseCompositeExtractor, + testStep, + 'testFileMetadata', + ) as unknown as TestFileExtractor; +} + +export function testRunCustomizer( + testRun: TestRunCompositeExtractor, + testStep: TestStepExtractor, +): TestRunExtractor { + return testItemCustomizer( + testRun as unknown as TestCaseCompositeExtractor, + testStep, + ) as unknown as TestRunExtractor; +} diff --git a/src/options/customizers/testStep.test.ts b/src/options/customizers/testStep.test.ts new file mode 100644 index 00000000..3630fac5 --- /dev/null +++ b/src/options/customizers/testStep.test.ts @@ -0,0 +1,113 @@ +import type { + AllureTestStepResult, + TestStepExtractorContext, +} from 'jest-allure2-reporter'; + +import type { TestStepCompositeExtractor } from '../types/compositeExtractors'; + +import { testStepCustomizer } from './testStep'; + +describe('testStepCustomizer', () => { + const createContext = (): TestStepExtractorContext => ({ + value: {}, + result: {}, + testStepMetadata: {} as any, + testCase: {} as any, + testCaseMetadata: {} as any, + filePath: [], + testFile: {} as any, + testFileMetadata: {} as any, + $: {} as any, + globalConfig: {} as any, + config: {} as any, + }); + + const defaultCompositeExtractor: TestStepCompositeExtractor = { + attachments: ({ value }) => value, + displayName: ({ value }) => value, + hidden: ({ value }) => value, + hookType: ({ value }) => value, + parameters: ({ value }) => value, + stage: ({ value }) => value, + start: ({ value }) => value, + status: ({ value }) => value, + statusDetails: ({ value }) => value, + steps: ({ value }) => value, + stop: ({ value }) => value, + }; + + test.each` + property | defaultValue | extractedValue + ${'hidden'} | ${false} | ${false} + ${'hookType'} | ${undefined} | ${'beforeEach'} + ${'displayName'} | ${''} | ${'Custom Step'} + ${'start'} | ${Number.NaN} | ${1_234_567_890} + ${'stop'} | ${Number.NaN} | ${1_234_567_899} + ${'stage'} | ${'scheduled'} | ${'running'} + ${'status'} | ${'unknown'} | ${'passed'} + ${'statusDetails'} | ${{}} | ${{ message: 'Step passed' }} + ${'attachments'} | ${[]} | ${[{ name: 'attachment1' }, { name: 'attachment2' }]} + ${'parameters'} | ${[]} | ${[{ name: 'param1', value: 'value1' }, { name: 'param2', value: 'value2' }]} + `( + 'should extract $property with default value $defaultValue and extracted value $extractedValue', + async ({ + property, + defaultValue, + extractedValue, + }: { + property: keyof AllureTestStepResult; + defaultValue: any; + extractedValue: any; + }) => { + const extractor = jest.fn().mockResolvedValue(extractedValue); + const testStep = testStepCustomizer({ + ...defaultCompositeExtractor, + [property]: extractor, + }); + + const result = await testStep(createContext()); + + expect(extractor).toHaveBeenCalledWith( + expect.objectContaining({ + value: defaultValue, + result: expect.any(Object), + }), + ); + + expect(result?.[property]).toEqual(extractedValue); + }, + ); + + it('should return undefined when hidden is true', async () => { + const hiddenExtractor = jest.fn().mockResolvedValue(true); + const testStep = testStepCustomizer({ + ...defaultCompositeExtractor, + hidden: hiddenExtractor, + }); + const context = createContext(); + const result = await testStep(context); + + expect(hiddenExtractor).toHaveBeenCalledWith( + expect.objectContaining({ value: false, result: { hidden: true } }), + ); + expect(result).toBeUndefined(); + }); + + it('should extract nested steps', async () => { + let counter = 0; + const testStep = testStepCustomizer({ + ...defaultCompositeExtractor, + displayName: () => `Step ${++counter}`, + }); + + const context = createContext(); + context.testStepMetadata.steps = [{}, {}]; + + const result = await testStep(context); + + expect(result?.displayName).toBe('Step 1'); + expect(result?.steps).toHaveLength(2); + expect(result?.steps[0].displayName).toBe('Step 2'); + expect(result?.steps[1].displayName).toBe('Step 3'); + }); +}); diff --git a/src/options/customizers/testStep.ts b/src/options/customizers/testStep.ts new file mode 100644 index 00000000..974d9277 --- /dev/null +++ b/src/options/customizers/testStep.ts @@ -0,0 +1,100 @@ +import type { + AllureTestStepResult, + TestStepExtractor, +} from 'jest-allure2-reporter'; + +import type { TestStepCompositeExtractor } from '../types/compositeExtractors'; +import { isDefined } from '../../utils'; + +export function testStepCustomizer( + testStep: TestStepCompositeExtractor, +): TestStepExtractor { + const extractor: TestStepExtractor = async (context) => { + const result: Partial = {}; + result.hidden = await testStep.hidden({ + ...context, + value: false, + result, + }); + + if (result.hidden) { + return; + } + + result.hookType = await testStep.hookType({ + ...context, + value: undefined, + result, + }); + + result.displayName = await testStep.displayName({ + ...context, + value: '', + result, + }); + + result.start = await testStep.start({ + ...context, + value: Number.NaN, + result, + }); + + result.stop = await testStep.stop({ + ...context, + value: Number.NaN, + result, + }); + + result.stage = await testStep.stage({ + ...context, + value: 'scheduled', + result, + }); + + result.status = await testStep.status({ + ...context, + value: 'unknown', + result, + }); + + result.statusDetails = await testStep.statusDetails({ + ...context, + value: {}, + result, + }); + + result.attachments = await testStep.attachments({ + ...context, + value: [], + result, + }); + + result.parameters = await testStep.parameters({ + ...context, + value: [], + result, + }); + + const steps = context.testStepMetadata?.steps; + if (steps && steps.length > 0) { + const allSteps = await Promise.all( + steps.map(async (testStepMetadata) => { + const stepResult = await extractor({ + ...context, + testStepMetadata, + value: {}, + result, + }); + + return stepResult; + }), + ); + + result.steps = allSteps.filter(isDefined); + } + + return result as AllureTestStepResult; + }; + + return extractor; +} diff --git a/src/options/default-options/index.ts b/src/options/default-options/index.ts index aa8ccd9e..e5e307a8 100644 --- a/src/options/default-options/index.ts +++ b/src/options/default-options/index.ts @@ -1,6 +1,5 @@ import type { ExtractorContext, - PluginContext, ReporterConfig, } from 'jest-allure2-reporter'; diff --git a/src/options/default-options/testCase.ts b/src/options/default-options/testCase.ts index 40d77441..b4f32f85 100644 --- a/src/options/default-options/testCase.ts +++ b/src/options/default-options/testCase.ts @@ -4,16 +4,16 @@ import type { TestCaseResult } from '@jest/reporters'; import type { ExtractorContext, Label, - ReporterConfig, + TestCaseCustomizer, Stage, Status, - TestCaseCustomizer, + TestCasePropertyCustomizer, TestCaseExtractorContext, } from 'jest-allure2-reporter'; import { composeExtractors } from '../extractors'; -import { getStatusDetails } from '../../utils'; -import { aggregateLabelCustomizers } from '../compose-options'; +import { getStatusDetails, isDefined } from '../../utils'; +import { labels } from '../compose-options'; const identity = (context: ExtractorContext) => context.value; const last = async (context: ExtractorContext) => { @@ -22,7 +22,7 @@ const last = async (context: ExtractorContext) => { }; const all = identity; -export const testCase: ReporterConfig['testCase'] = { +export const testCase: Required = { hidden: () => false, $: ({ $ }) => $, historyId: ({ testCase, testCaseMetadata }) => @@ -35,7 +35,7 @@ export const testCase: ReporterConfig['testCase'] = { const text = testCaseMetadata.description?.join('\n\n') ?? ''; const codes = await $.extractSourceCode(testCaseMetadata, true); const snippets = codes.map($.sourceCode2Markdown); - return [text, ...snippets].filter(Boolean).join('\n\n'); + return [text, ...snippets].filter(isDefined).join('\n\n'); }, descriptionHtml: ({ testCaseMetadata }) => testCaseMetadata.descriptionHtml?.join('\n'), @@ -55,7 +55,7 @@ export const testCase: ReporterConfig['testCase'] = { attachments: ({ testCaseMetadata }) => testCaseMetadata.attachments ?? [], parameters: ({ testCaseMetadata }) => testCaseMetadata.parameters ?? [], labels: composeExtractors>( - aggregateLabelCustomizers({ + labels({ package: last, testClass: last, testMethod: last, @@ -70,7 +70,7 @@ export const testCase: ReporterConfig['testCase'] = { severity: last, tag: all, owner: last, - } as TestCaseCustomizer['labels']), + } as TestCasePropertyCustomizer['labels']), ({ testCaseMetadata }) => testCaseMetadata.labels ?? [], ), links: ({ testCaseMetadata }) => testCaseMetadata.links ?? [], diff --git a/src/options/default-options/testFile.ts b/src/options/default-options/testFile.ts index 6fa34ed7..a2c0e413 100644 --- a/src/options/default-options/testFile.ts +++ b/src/options/default-options/testFile.ts @@ -5,17 +5,15 @@ import type { Label, Link, ReporterConfig, - TestCaseCustomizer, + TestCasePropertyCustomizer, TestFileExtractorContext, } from 'jest-allure2-reporter'; -import { getStatusDetails } from '../../utils'; -import { aggregateLabelCustomizers } from '../compose-options'; -import { composeExtractors } from '../extractors'; +import {getStatusDetails, isDefined} from '../../utils'; +import { labels } from '../compose-options'; +import { composeExtractors, last } from '../extractors'; -const identity = (context: ExtractorContext) => context.value; -const last = (context: ExtractorContext) => context.value?.at(-1); -const all = identity; +const all = (context: ExtractorContext) => context.value; export const testFile: ReporterConfig['testFile'] = { hidden: ({ testFile }) => !testFile.testExecError, @@ -27,7 +25,7 @@ export const testFile: ReporterConfig['testFile'] = { description: async ({ $, testFileMetadata }) => { const text = testFileMetadata.description?.join('\n') ?? ''; const code = await $.extractSourceCode(testFileMetadata); - return [text, $.sourceCode2Markdown(code)].filter(Boolean).join('\n\n'); + return [text, $.sourceCode2Markdown(code)].filter(isDefined).join('\n\n'); }, descriptionHtml: ({ testFileMetadata }) => testFileMetadata.descriptionHtml?.join('\n'), @@ -41,24 +39,17 @@ export const testFile: ReporterConfig['testFile'] = { $.stripAnsi(getStatusDetails(testFile.testExecError)), attachments: ({ testFileMetadata }) => testFileMetadata.attachments ?? [], parameters: ({ testFileMetadata }) => testFileMetadata.parameters ?? [], - labels: composeExtractors>( - aggregateLabelCustomizers({ - package: last, - testClass: last, - testMethod: last, - parentSuite: last, - subSuite: last, - suite: () => '(test file execution)', - epic: all, - feature: all, - story: all, - thread: ({ testFileMetadata }) => testFileMetadata.workerId, - severity: last, - tag: all, - owner: last, - } as TestCaseCustomizer['labels']), - ({ testFileMetadata }) => testFileMetadata.labels ?? [], - ), + labels: labels({ + package: last, + testClass: last, + testMethod: last, + parentSuite: last, + subSuite: last, + suite: () => '(test file execution)', + thread: ({ testFileMetadata }) => testFileMetadata.workerId, + severity: last, + owner: last, + } as TestCasePropertyCustomizer['labels']), links: ({ testFileMetadata }: TestFileExtractorContext) => testFileMetadata.links ?? [], }; diff --git a/src/options/default-options/testRun.ts b/src/options/default-options/testRun.ts index b64701db..4a2c6fb8 100644 --- a/src/options/default-options/testRun.ts +++ b/src/options/default-options/testRun.ts @@ -1,19 +1,22 @@ -import type { - ReporterConfig, -} from 'jest-allure2-reporter'; +import type { ReporterConfig } from 'jest-allure2-reporter'; + +const fullName: ReporterConfig['testRun']['fullName'] = async ({ $ }) => + (await $.manifest((x) => x.name)) ?? ''; export const testRun: ReporterConfig['testRun'] = { hidden: ({ aggregatedResult }) => aggregatedResult.numFailedTestSuites > 0, $: ({ $ }) => $, - historyId: async ({ $ }) => (await $.manifest((x) => x.name)) ?? '', + historyId: fullName, + fullName, name: () => '(test run)', - fullName: async ({ $ }) => (await $.manifest((x) => x.name)) ?? '', - description: () => '', - descriptionHtml: () => '', + description: () => void 0, + descriptionHtml: () => void 0, start: ({ aggregatedResult }) => aggregatedResult.startTime, stop: () => Date.now(), - stage: ({ aggregatedResult }) => aggregatedResult.wasInterrupted ? 'interrupted' : 'finished', - status: ({ aggregatedResult }) => aggregatedResult.numFailedTestSuites > 0 ? 'failed' : 'passed', + stage: ({ aggregatedResult }) => + aggregatedResult.wasInterrupted ? 'interrupted' : 'finished', + status: ({ aggregatedResult }) => + aggregatedResult.numFailedTestSuites > 0 ? 'failed' : 'passed', statusDetails: () => void 0, attachments: () => void 0, parameters: ({ aggregatedResult }) => [ @@ -34,6 +37,6 @@ export const testRun: ReporterConfig['testRun'] = { value: `${aggregatedResult.numPendingTestSuites}`, }, ], - labels: () => [], + labels: () => [{ name: 'suite', value: '(test file execution)' }], links: () => [], }; diff --git a/src/options/extractors/asExtractor.ts b/src/options/extractors/asExtractor.ts index 10d08862..af1e3329 100644 --- a/src/options/extractors/asExtractor.ts +++ b/src/options/extractors/asExtractor.ts @@ -1,8 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Extractor, ExtractorContext } from 'jest-allure2-reporter'; +import type { Extractor } from 'jest-allure2-reporter'; import { isPromiseLike } from '../../utils'; +import { isExtractor } from './isExtractor'; + /** * Resolves the unknown value either as an extractor or it * builds a fallback extractor that returns the given value. @@ -15,19 +17,10 @@ import { isPromiseLike } from '../../utils'; * defaulting the values. The former is useful for tags, the latter * for the rest of the labels which don't support multiple occurrences. */ -export function asExtractor< - R, - E extends Extractor, R> = Extractor< - any, - ExtractorContext, - R - >, ->(maybeExtractor: R | E | undefined): E | undefined { - if (maybeExtractor == null) { - return undefined; - } - - if (isExtractor(maybeExtractor)) { +export function asExtractor( + maybeExtractor: T | Ta | Extractor, +): Extractor { + if (isExtractor(maybeExtractor)) { return maybeExtractor; } @@ -44,11 +37,7 @@ export function asExtractor< } return value ?? baseValue; - }) as E; + }) as Extractor; return extractor; } - -function isExtractor(value: unknown): value is E { - return typeof value === 'function'; -} diff --git a/src/options/extractors/extractors.test.ts b/src/options/extractors/extractors.test.ts index 0af68cb8..8d2bfb9b 100644 --- a/src/options/extractors/extractors.test.ts +++ b/src/options/extractors/extractors.test.ts @@ -1,4 +1,4 @@ -import { composeExtractors } from './composeExtractors'; +import { composeExtractors, last } from '.'; describe('extractors', () => { describe('composeExtractors', () => { @@ -23,6 +23,23 @@ describe('extractors', () => { expect(result).toBe(6); }); }); + + describe('last', () => { + it('should return the last element of an array', async () => { + const result = await last({ value: [1, 2, 3] }); + expect(result).toBe(3); + }); + + it('should return the last element of a promised array', async () => { + const result = await last({ value: Promise.resolve([3, 2, 1]) }); + expect(result).toBe(1); + }); + + it('should return undefined for a non-existent value', async () => { + const result = await last({ value: void 0 }); + expect(result).toBe(undefined); + }); + }); }); function assertEq(actual: unknown, expected: T): asserts actual is T { diff --git a/src/options/extractors/index.ts b/src/options/extractors/index.ts index 521a0d57..14e960ca 100644 --- a/src/options/extractors/index.ts +++ b/src/options/extractors/index.ts @@ -1,2 +1,4 @@ export * from './asExtractor'; export * from './composeExtractors'; +export * from './isExtractor'; +export * from './last'; diff --git a/src/options/extractors/isExtractor.ts b/src/options/extractors/isExtractor.ts new file mode 100644 index 00000000..0bdcf8da --- /dev/null +++ b/src/options/extractors/isExtractor.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Extractor } from 'jest-allure2-reporter'; + +export function isExtractor( + value: unknown, +): value is Extractor { + return typeof value === 'function'; +} + +export function isExtractorFn(value: unknown): value is T { + return typeof value === 'function'; +} diff --git a/src/options/extractors/last.ts b/src/options/extractors/last.ts new file mode 100644 index 00000000..18819b8c --- /dev/null +++ b/src/options/extractors/last.ts @@ -0,0 +1,10 @@ +import type { ExtractorContext } from 'jest-allure2-reporter'; + +import { isPromiseLike } from '../../utils'; + +export const last = async (context: ExtractorContext) => { + const value = isPromiseLike(context.value) + ? await context.value + : context.value; + return value?.at(-1); +}; diff --git a/src/options/index.ts b/src/options/index.ts index 82a5d058..5636e54b 100644 --- a/src/options/index.ts +++ b/src/options/index.ts @@ -1,19 +1,15 @@ import path from 'node:path'; -import type { - PluginContext, - ReporterOptions, - ReporterConfig, -} from 'jest-allure2-reporter'; +import type { ReporterOptions, ReporterConfig } from 'jest-allure2-reporter'; -import { composeOptions } from './compose-options'; +import { reporterOptions } from './compose-options'; import { defaultOptions } from './default-options'; export function resolveOptions( - context: PluginContext, - options?: ReporterOptions | undefined, + customOptions?: ReporterOptions | undefined, ): ReporterConfig { - const result = composeOptions(context, defaultOptions(context), options); + const result = reporterOptions(defaultOptions(), customOptions); result.resultsDir = path.resolve(result.resultsDir); + return result; } diff --git a/src/options/override-options/attachments.ts b/src/options/override-options/attachments.ts new file mode 100644 index 00000000..606a45f6 --- /dev/null +++ b/src/options/override-options/attachments.ts @@ -0,0 +1,22 @@ +import path from 'node:path'; + +import type { Attachment, GlobalExtractor } from 'jest-allure2-reporter'; + +export const attachments: GlobalExtractor = async ({ + config, + value, +}) => { + const attachments = (await value) ?? []; + + return attachments.map((attachment) => { + const source = path.relative(config.resultsDir, attachment.source); + if (source.startsWith('..')) { + return attachment; + } + + return { + ...attachment, + source, + }; + }); +}; diff --git a/src/options/override-options/historyId.ts b/src/options/override-options/historyId.ts new file mode 100644 index 00000000..5081c5a7 --- /dev/null +++ b/src/options/override-options/historyId.ts @@ -0,0 +1,10 @@ +import crypto from 'node:crypto'; + +import type { GlobalExtractor } from 'jest-allure2-reporter'; + +import { md5 } from '../../utils'; + +export const historyId: GlobalExtractor = async ({ value }) => { + const id = await value; + return md5(id ?? crypto.randomBytes(16).toString('hex')); +}; diff --git a/src/options/override-options/index.ts b/src/options/override-options/index.ts new file mode 100644 index 00000000..6925177c --- /dev/null +++ b/src/options/override-options/index.ts @@ -0,0 +1,2 @@ +export * from './attachments'; +export * from './historyId'; diff --git a/src/options/types/compositeExtractors.ts b/src/options/types/compositeExtractors.ts new file mode 100644 index 00000000..7a5a2fe3 --- /dev/null +++ b/src/options/types/compositeExtractors.ts @@ -0,0 +1,84 @@ +import type { + AllureTestStepMetadata, + AllureTestStepResult, + Attachment, + Label, + Link, + Parameter, + Primitive, + Stage, + Status, + StatusDetails, + TestCasePropertyExtractor, + TestFilePropertyExtractor, + TestRunPropertyExtractor, + TestStepPropertyExtractor, +} from 'jest-allure2-reporter'; + +export interface TestCaseCompositeExtractor { + hidden: TestCasePropertyExtractor; + historyId: TestCasePropertyExtractor; + displayName: TestCasePropertyExtractor; + fullName: TestCasePropertyExtractor; + start: TestCasePropertyExtractor; + stop: TestCasePropertyExtractor; + description: TestCasePropertyExtractor; + descriptionHtml: TestCasePropertyExtractor; + stage: TestCasePropertyExtractor; + status: TestCasePropertyExtractor; + statusDetails: TestCasePropertyExtractor; + labels: TestCasePropertyExtractor; + links: TestCasePropertyExtractor; + attachments: TestCasePropertyExtractor; + parameters: TestCasePropertyExtractor; +} + +export interface TestFileCompositeExtractor { + hidden: TestFilePropertyExtractor; + historyId: TestFilePropertyExtractor; + displayName: TestFilePropertyExtractor; + fullName: TestFilePropertyExtractor; + start: TestFilePropertyExtractor; + stop: TestFilePropertyExtractor; + description: TestFilePropertyExtractor; + descriptionHtml: TestFilePropertyExtractor; + stage: TestFilePropertyExtractor; + status: TestFilePropertyExtractor; + statusDetails: TestFilePropertyExtractor; + labels: TestFilePropertyExtractor; + links: TestFilePropertyExtractor; + attachments: TestFilePropertyExtractor; + parameters: TestFilePropertyExtractor; +} + +export interface TestRunCompositeExtractor { + hidden: TestRunPropertyExtractor; + historyId: TestRunPropertyExtractor; + displayName: TestRunPropertyExtractor; + fullName: TestRunPropertyExtractor; + start: TestRunPropertyExtractor; + stop: TestRunPropertyExtractor; + description: TestRunPropertyExtractor; + descriptionHtml: TestRunPropertyExtractor; + stage: TestRunPropertyExtractor; + status: TestRunPropertyExtractor; + statusDetails: TestRunPropertyExtractor; + labels: TestRunPropertyExtractor; + links: TestRunPropertyExtractor; + attachments: TestRunPropertyExtractor; + parameters: TestRunPropertyExtractor; +} + +export interface TestStepCompositeExtractor { + hidden: TestStepPropertyExtractor; + hookType: TestStepPropertyExtractor; + displayName: TestStepPropertyExtractor; + start: TestStepPropertyExtractor; + stop: TestStepPropertyExtractor; + stage: TestStepPropertyExtractor; + status: TestStepPropertyExtractor; + statusDetails: TestStepPropertyExtractor; + steps: TestStepPropertyExtractor; + attachments: TestStepPropertyExtractor; + parameters: TestStepPropertyExtractor; +} diff --git a/src/options/types/index.ts b/src/options/types/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index f940129f..f1bb12fc 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs/promises'; import path from 'node:path'; import type { @@ -12,40 +11,30 @@ import type { } from '@jest/reporters'; import { state } from 'jest-metadata'; import JestMetadataReporter from 'jest-metadata/reporter'; -import rimraf from 'rimraf'; -import type { - AllureGroup, - Attachment, - Category, - ExecutableItemWrapper, - Stage, - Status, -} from '@noomorph/allure-js-commons'; +import type { Category } from '@noomorph/allure-js-commons'; import { AllureRuntime } from '@noomorph/allure-js-commons'; import type { AllureGlobalMetadata, AllureTestCaseMetadata, - AllureTestCaseResult, AllureTestFileMetadata, - AllureTestStepMetadata, - AllureTestStepResult, - ExtractorHelpers, + Helpers, ReporterConfig, ReporterOptions, TestCaseExtractorContext, TestFileExtractorContext, - TestStepExtractorContext, TestRunExtractorContext, } from 'jest-allure2-reporter'; import { resolveOptions } from '../options'; import { AllureMetadataProxy, MetadataSquasher } from '../metadata'; -import { md5 } from '../utils'; -import * as fallbackHooks from './fallback'; +import * as fallbacks from './fallbacks'; +import { overwriteDirectory } from './overwriteDirectory'; +import { postProcessMetadata } from './postProcessMetadata'; +import { writeTest } from './allureCommons'; export class JestAllure2Reporter extends JestMetadataReporter { - private readonly _$: Partial = {}; + private readonly _$: Partial = {}; private readonly _allure: AllureRuntime; private readonly _config: ReporterConfig; private readonly _globalConfig: Config.GlobalConfig; @@ -54,8 +43,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { super(globalConfig); this._globalConfig = globalConfig; - const pluginContext = { globalConfig }; - this._config = resolveOptions(pluginContext, options); + this._config = resolveOptions(options); this._allure = new AllureRuntime({ resultsDir: this._config.resultsDir, }); @@ -76,20 +64,19 @@ export class JestAllure2Reporter extends JestMetadataReporter { await super.onRunStart(aggregatedResult, options); if (this._config.overwrite) { - await rimraf(this._config.resultsDir); - await fs.mkdir(this._config.resultsDir, { recursive: true }); + await overwriteDirectory(this._config.resultsDir); } } async onTestFileStart(test: Test) { super.onTestFileStart(test); - const postProcessMetadata = JestAllure2Reporter.query.test(test); + const rawMetadata = JestAllure2Reporter.query.test(test); const testFileMetadata = new AllureMetadataProxy( - postProcessMetadata, + rawMetadata, ); - fallbackHooks.onTestFileStart(test, testFileMetadata); + fallbacks.onTestFileStart(test, testFileMetadata); } async onTestCaseResult(test: Test, testCaseResult: TestCaseResult) { @@ -99,7 +86,7 @@ export class JestAllure2Reporter extends JestMetadataReporter { JestAllure2Reporter.query.testCaseResult(testCaseResult).lastInvocation!, ); - fallbackHooks.onTestCaseResult(testCaseMetadata); + fallbacks.onTestCaseResult(testCaseMetadata); } async onTestFileResult( @@ -107,28 +94,33 @@ export class JestAllure2Reporter extends JestMetadataReporter { testResult: TestResult, aggregatedResult: AggregatedResult, ) { + await super.onTestFileResult(test, testResult, aggregatedResult); + + const rawMetadata = JestAllure2Reporter.query.test(test); const testFileMetadata = new AllureMetadataProxy( - JestAllure2Reporter.query.test(test), + rawMetadata, ); - fallbackHooks.onTestFileResult(test, testFileMetadata); - - return super.onTestFileResult(test, testResult, aggregatedResult); + fallbacks.onTestFileResult(test, testFileMetadata); + await postProcessMetadata(this._$ as Helpers, rawMetadata); } async onRunComplete( testContexts: Set, - results: AggregatedResult, + aggregatedResult: AggregatedResult, ): Promise { - await super.onRunComplete(testContexts, results); + await super.onRunComplete(testContexts, aggregatedResult); const config = this._config; const globalContext: TestRunExtractorContext = { - globalConfig: this._globalConfig, + $: this._$ as Helpers, + aggregatedResult, config, - default: () => void 0, - $: this._$ as ExtractorHelpers, + globalConfig: this._globalConfig, + get value() { + return void 0; + }, }; const environment = await config.environment(globalContext); @@ -146,11 +138,18 @@ export class JestAllure2Reporter extends JestMetadataReporter { this._allure.writeCategoriesDefinitions(categories as Category[]); } - await this._postProcessMetadata(); // Run before squashing - const squasher = new MetadataSquasher(); - for (const testResult of results.testResults) { + const allureRunTest = await config.testRun(globalContext); + if (allureRunTest) { + writeTest({ + containerName: `Test Run (${process.pid})`, + runtime: this._allure, + test: allureRunTest, + }); + } + + for (const testResult of aggregatedResult.testResults) { const testFileContext: TestFileExtractorContext = { ...globalContext, filePath: path @@ -162,35 +161,11 @@ export class JestAllure2Reporter extends JestMetadataReporter { ), }; - if (!config.testFile.hidden(testFileContext)) { - // pseudo-test entity, used for reporting file-level errors and other obscure purposes - const attachments = await config.testFile.attachments(testFileContext); - const allureFileTest: AllurePayloadTest = { - name: await config.testFile.name(testFileContext), - start: await config.testFile.start(testFileContext), - stop: await config.testFile.stop(testFileContext), - historyId: await config.testFile.historyId(testFileContext), - fullName: await config.testFile.fullName(testFileContext), - description: await config.testFile.description(testFileContext), - descriptionHtml: - await config.testFile.descriptionHtml(testFileContext), - // TODO: merge @noomorph/allure-js-commons into this package and remove casting - stage: (await config.testFile.stage( - testFileContext, - )) as string as Stage, - status: (await config.testFile.status( - testFileContext, - )) as string as Status, - statusDetails: await config.testFile.statusDetails(testFileContext), - links: await config.testFile.links(testFileContext), - labels: await config.testFile.labels(testFileContext), - parameters: await config.testFile.parameters(testFileContext), - attachments: attachments?.map(this._relativizeAttachment), - }; - - await this._renderHtmlDescription(testFileContext, allureFileTest); - await this._createTest({ + const allureFileTest = await config.testFile(testFileContext); + if (allureFileTest) { + writeTest({ containerName: `${testResult.testFilePath}`, + runtime: this._allure, test: allureFileTest, }); } @@ -211,261 +186,22 @@ export class JestAllure2Reporter extends JestMetadataReporter { testCaseMetadata, }; - if (config.testCase.hidden(testCaseContext)) { + const allureTest = await config.testCase(testCaseContext); + if (!allureTest) { continue; } - const testCaseSteps = testCaseMetadata.steps ?? []; - const visibleTestStepContexts = testCaseSteps - .map( - (testStepMetadata) => - ({ - ...testCaseContext, - testStepMetadata, - }) as TestStepExtractorContext, - ) - .filter((testStepMetadataContext) => { - return !config.testStep.hidden(testStepMetadataContext); - }); - - if (testCaseMetadata.steps) { - testCaseMetadata.steps = visibleTestStepContexts.map( - (c) => c.testStepMetadata, - ); - } - - let allureSteps: AllurePayloadStep[] = await Promise.all( - visibleTestStepContexts.map(async (testStepContext) => { - const attachments = - await config.testStep.attachments(testStepContext); - const result: AllurePayloadStep = { - hookType: testStepContext.testStepMetadata.hookType, - name: await config.testStep.name(testStepContext), - start: await config.testStep.start(testStepContext), - stop: await config.testStep.stop(testStepContext), - stage: (await config.testStep.stage( - testStepContext, - )) as string as Stage, - status: (await config.testStep.status( - testStepContext, - )) as string as Status, - statusDetails: - await config.testStep.statusDetails(testStepContext), - parameters: await config.testStep.parameters(testStepContext), - attachments: attachments?.map(this._relativizeAttachment), - }; - - return result; - }), - ); - - const attachments = - await config.testCase.attachments(testCaseContext); - const allureTest: AllurePayloadTest = { - name: await config.testCase.name(testCaseContext), - start: await config.testCase.start(testCaseContext), - stop: await config.testCase.stop(testCaseContext), - historyId: await config.testCase.historyId(testCaseContext), - fullName: await config.testCase.fullName(testCaseContext), - description: await config.testCase.description(testCaseContext), - descriptionHtml: - await config.testCase.descriptionHtml(testCaseContext), - // TODO: merge @noomorph/allure-js-commons into this package and remove casting - stage: (await config.testCase.stage( - testCaseContext, - )) as string as Stage, - status: (await config.testCase.status( - testCaseContext, - )) as string as Status, - statusDetails: await config.testCase.statusDetails(testCaseContext), - links: await config.testCase.links(testCaseContext), - labels: await config.testCase.labels(testCaseContext), - parameters: await config.testCase.parameters(testCaseContext), - attachments: attachments?.map(this._relativizeAttachment), - steps: allureSteps.filter((step) => !step.hookType), - }; - - allureSteps = allureSteps.filter((step) => step.hookType); - - await this._renderHtmlDescription(testCaseContext, allureTest); - const invocationIndex = allInvocations.indexOf( testInvocationMetadata, ); - await this._createTest({ + writeTest({ containerName: `${testCaseResult.fullName} (${invocationIndex})`, + runtime: this._allure, test: allureTest, - steps: allureSteps, }); } } } } - - private async _createTest({ test, containerName, steps }: AllurePayload) { - const allure = this._allure; - const allureGroup = allure.startGroup(containerName); - const allureTest = allureGroup.startTest(); - - this._fillStep(allureTest, test); - - if (test.historyId) { - allureTest.historyId = md5(test.historyId); - } - if (test.fullName) { - allureTest.fullName = test.fullName; - } - if (test.description) { - allureTest.description = test.description; - } - if (test.descriptionHtml) { - allureTest.descriptionHtml = test.descriptionHtml; - } - if (test.links) { - for (const link of test.links) { - allureTest.addLink(link.url, link.name, link.type); - } - } - if (test.labels) { - for (const label of test.labels) { - allureTest.addLabel(label.name, label.value); - } - } - if (steps) { - for (const step of steps) { - const executable = this._createStepExecutable( - allureGroup, - step.hookType, - ); - await this._fillStep(executable, step); - } - } - - allureTest.endTest(test.stop); - allureGroup.endGroup(); - } - - private async _renderHtmlDescription( - context: TestRunExtractorContext, - test: AllurePayloadTest, - ) { - if (test.description) { - test.descriptionHtml = - (test.descriptionHtml ? test.descriptionHtml + '\n' : '') + - (await context.$.markdown2html(test.description)); - } - } - - private _createStepExecutable( - parent: AllureGroup, - hookType: AllureTestStepMetadata['hookType'], - ) { - switch (hookType) { - case 'beforeAll': - case 'beforeEach': { - return parent.addBefore(); - } - case 'afterEach': - case 'afterAll': { - return parent.addAfter(); - } - default: { - throw new Error(`Cannot create step executable for ${hookType}`); - } - } - } - - private _fillStep( - executable: ExecutableItemWrapper, - step: AllurePayloadStep, - ) { - if (step.name !== undefined) { - executable.name = step.name; - } - if (step.start !== undefined) { - executable.wrappedItem.start = step.start; - } - if (step.stop !== undefined) { - executable.wrappedItem.stop = step.stop; - } - if (step.stage !== undefined) { - executable.stage = step.stage as string as Stage; - } - if (step.status !== undefined) { - executable.status = step.status as string as Status; - } - if (step.statusDetails !== undefined) { - executable.statusDetails = step.statusDetails; - } - if (step.attachments !== undefined) { - executable.wrappedItem.attachments = step.attachments; - } - if (step.parameters) { - executable.wrappedItem.parameters = step.parameters; - } - if (step.steps) { - for (const innerStep of step.steps) { - this._fillStep( - executable.startStep(innerStep.name ?? '', innerStep.start), - innerStep, - ); - } - } - } - - private async _postProcessMetadata() { - const batch = state.testFiles.flatMap((testFile) => { - const allDescribeBlocks = [...testFile.allDescribeBlocks()]; - const allHooks = allDescribeBlocks.flatMap((describeBlock) => [ - ...describeBlock.hookDefinitions(), - ]); - - return [ - testFile, - ...allDescribeBlocks, - ...allHooks, - ...testFile.allTestEntries(), - ]; - }); - - await Promise.all( - batch.map(async (metadata) => { - const allureProxy = new AllureMetadataProxy(metadata); - allureProxy.get(); // TODO: remove this line - // await this._callPlugins('postProcessMetadata', { - // $: this._$ as ExtractorHelpers, - // metadata: allureProxy.assign({}).get(), - // }); - }), - ); - } - - private _relativizeAttachment = (attachment: Attachment) => { - const source = path.relative(this._config.resultsDir, attachment.source); - if (source.startsWith('..')) { - return attachment; - } - - return { - ...attachment, - source, - }; - }; -} - -type AllurePayload = { - containerName: string; - test: AllurePayloadTest; - steps?: AllurePayloadStep[]; -}; - -interface AllurePayloadStep - extends Partial> { - hookType?: 'beforeAll' | 'beforeEach' | 'afterEach' | 'afterAll'; - steps?: AllurePayloadStep[]; -} - -interface AllurePayloadTest extends Partial { - steps?: AllurePayloadStep[]; } diff --git a/src/reporter/fallback/ThreadService.ts b/src/reporter/ThreadService.ts similarity index 100% rename from src/reporter/fallback/ThreadService.ts rename to src/reporter/ThreadService.ts diff --git a/src/reporter/allureCommons.ts b/src/reporter/allureCommons.ts new file mode 100644 index 00000000..4d0a8a83 --- /dev/null +++ b/src/reporter/allureCommons.ts @@ -0,0 +1,127 @@ +import type { + AllureTestStepResult, + AllureTestCaseResult, + Parameter, +} from 'jest-allure2-reporter'; +import type { + AllureGroup, + AllureRuntime, + Parameter as AllureParameter, + ExecutableItemWrapper, + Stage, + Status, +} from '@noomorph/allure-js-commons'; + +import { md5 } from '../utils'; + +type CreateTestArguments = { + runtime: AllureRuntime; + containerName: string; + test: AllureTestCaseResult; +}; + +export function writeTest({ + runtime, + test, + containerName, +}: CreateTestArguments) { + const allureGroup = runtime.startGroup(containerName); + const allureTest = allureGroup.startTest(); + const steps = test.steps; + + fillStep(allureTest, test); + + if (test.historyId) { + allureTest.historyId = md5(test.historyId); + } + if (test.fullName) { + allureTest.fullName = test.fullName; + } + if (test.description) { + allureTest.description = test.description; + } + if (test.descriptionHtml) { + allureTest.descriptionHtml = test.descriptionHtml; + } + if (test.links) { + for (const link of test.links) { + allureTest.addLink(link.url, link.name, link.type); + } + } + if (test.labels) { + for (const label of test.labels) { + allureTest.addLabel(label.name, label.value); + } + } + if (steps) { + for (const step of steps) { + const executable = createStepExecutable(allureGroup, step.hookType); + fillStep(executable, step); + } + } + + allureTest.endTest(test.stop); + allureGroup.endGroup(); +} + +function fillStep( + executable: ExecutableItemWrapper, + step: AllureTestCaseResult | AllureTestStepResult, +) { + if (step.name !== undefined) { + executable.name = step.name; + } + if (step.start !== undefined) { + executable.wrappedItem.start = step.start; + } + if (step.stop !== undefined) { + executable.wrappedItem.stop = step.stop; + } + if (step.stage !== undefined) { + executable.stage = step.stage as string as Stage; + } + if (step.status !== undefined) { + executable.status = step.status as string as Status; + } + if (step.statusDetails !== undefined) { + executable.statusDetails = step.statusDetails; + } + if (step.attachments !== undefined) { + executable.wrappedItem.attachments = step.attachments; + } + if (step.parameters) { + executable.wrappedItem.parameters = step.parameters.map(stringifyParameter); + } + if (step.steps) { + for (const innerStep of step.steps) { + fillStep( + executable.startStep(innerStep.name ?? '', innerStep.start), + innerStep, + ); + } + } +} + +function stringifyParameter(parameter: Parameter): AllureParameter { + return { ...parameter, value: String(parameter.value) }; +} + +function createStepExecutable( + parent: AllureGroup, + hookType: AllureTestStepResult['hookType'], +) { + switch (hookType) { + case 'beforeAll': + case 'beforeEach': { + return parent.addBefore(); + } + case 'afterEach': + case 'afterAll': { + return parent.addAfter(); + } + default: { + // TODO: throw a more specific error + throw new Error(`Cannot create step executable for ${hookType}`); + } + } +} diff --git a/src/reporter/fallback/index.ts b/src/reporter/fallbacks.ts similarity index 94% rename from src/reporter/fallback/index.ts rename to src/reporter/fallbacks.ts index 3d177efd..3e526a34 100644 --- a/src/reporter/fallback/index.ts +++ b/src/reporter/fallbacks.ts @@ -4,7 +4,7 @@ import type { AllureTestFileMetadata, } from 'jest-allure2-reporter'; -import type { AllureMetadataProxy } from '../../metadata'; +import type { AllureMetadataProxy } from '../metadata'; import { ThreadService } from './ThreadService'; diff --git a/src/reporter/overwriteDirectory.ts b/src/reporter/overwriteDirectory.ts new file mode 100644 index 00000000..d7a46780 --- /dev/null +++ b/src/reporter/overwriteDirectory.ts @@ -0,0 +1,8 @@ +import fs from 'node:fs/promises'; + +import rimraf from 'rimraf'; + +export async function overwriteDirectory(directoryPath: string) { + await rimraf(directoryPath); + await fs.mkdir(directoryPath, { recursive: true }); +} diff --git a/src/reporter/postProcessMetadata.ts b/src/reporter/postProcessMetadata.ts new file mode 100644 index 00000000..89abcbef --- /dev/null +++ b/src/reporter/postProcessMetadata.ts @@ -0,0 +1,32 @@ +import type { AllureTestItemMetadata, Helpers } from 'jest-allure2-reporter'; +import type { TestFileMetadata } from 'jest-metadata'; + +import { AllureMetadataProxy } from '../metadata'; + +export async function postProcessMetadata($: Helpers, testFile: TestFileMetadata) { + const allDescribeBlocks = [...testFile.allDescribeBlocks()]; + const allHooks = allDescribeBlocks.flatMap((describeBlock) => [ + ...describeBlock.hookDefinitions(), + ]); + + const batch = [ + testFile, + ...allDescribeBlocks, + ...allHooks, + ...testFile.allTestEntries(), + ]; + + await Promise.all( + batch.map(async (metadata) => { + const allureProxy = new AllureMetadataProxy( + metadata, + ); + // TODO: do it for real + $.extractSourceCode(allureProxy.get('sourceLocation', {})); + // await this._callPlugins('postProcessMetadata', { + // $: this._$ as Helpers, + // metadata: allureProxy.assign({}).get(), + // }); + }), + ); +} diff --git a/src/utils/compactObject.test.ts b/src/utils/compactObject.test.ts new file mode 100644 index 00000000..7d1fd027 --- /dev/null +++ b/src/utils/compactObject.test.ts @@ -0,0 +1,58 @@ +import { compactObject } from './compactObject'; + +describe('compactObject', () => { + it('should remove undefined values from the object', () => { + const object = { + a: 1, + b: undefined, + c: 'hello', + d: null, + e: {}, + f: [], + g: undefined, + }; + + const result = compactObject(object); + + expect(result).toEqual({ + a: 1, + c: 'hello', + d: null, + e: {}, + f: [], + }); + }); + + it('should return an empty object when all values are undefined', () => { + const object = { + a: undefined, + b: undefined, + }; + + const result = compactObject(object); + + expect(result).toEqual({}); + }); + + it('should preserve the original object type', () => { + interface MyObject { + a?: number; + b?: string; + c?: boolean; + } + + const object: MyObject = { + a: 1, + b: undefined, + c: true, + }; + + const result = compactObject(object); + + expect(result).toEqual({ + a: 1, + c: true, + }); + expect(result).toBeInstanceOf(Object); + }); +}); diff --git a/src/utils/compactObject.ts b/src/utils/compactObject.ts new file mode 100644 index 00000000..f989591d --- /dev/null +++ b/src/utils/compactObject.ts @@ -0,0 +1,8 @@ +export function compactObject(object: T): Partial { + return Object.entries(object).reduce((result, [key, value]) => { + if (value !== undefined) { + result[key as keyof T] = value; + } + return result; + }, {} as Partial); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 584585ae..6d645c14 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,10 +1,12 @@ export * from './asArray'; export * from './attempt'; export * from './autoIndent'; +export * from './compactObject'; export * from './constant'; export * from './formatString'; export * from './getStatusDetails'; export * from './hijackFunction'; +export * from './isDefined'; export * from './isObject'; export * from './isError'; export * from './isJestAssertionError'; diff --git a/src/utils/isDefined.ts b/src/utils/isDefined.ts new file mode 100644 index 00000000..31f9397d --- /dev/null +++ b/src/utils/isDefined.ts @@ -0,0 +1,3 @@ +export function isDefined(value: T | null | undefined): value is T { + return value != null; +} diff --git a/tsconfig.json b/tsconfig.json index c9909306..5e7e0ae0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,7 @@ "noFallthroughCasesInSwitch": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "skipLibCheck": true, + "skipLibCheck": false, "forceConsistentCasingInFileNames": true, "importsNotUsedAsValues": "error", "rootDir": "src", From 4bf382fbc703772eb5bd6266d5a7510979ac1f9e Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 23 Mar 2024 10:09:42 +0200 Subject: [PATCH 15/50] simplification of types --- index.d.ts | 446 +++++--------------- src/metadata/docblockMapping.ts | 2 +- src/options/customizers/helpers.test.ts | 19 +- src/options/customizers/helpers.ts | 21 +- src/options/customizers/property.test.ts | 35 -- src/options/customizers/property.ts | 46 -- src/options/customizers/testCase.test.ts | 145 +++++++ src/options/customizers/testCase.ts | 77 ++-- src/options/customizers/testStep.test.ts | 46 +- src/options/customizers/testStep.ts | 20 +- src/options/extractors/asExtractor.ts | 56 +-- src/options/extractors/composeExtractors.ts | 19 +- src/options/extractors/extractors.test.ts | 10 +- src/options/extractors/index.ts | 1 + src/options/extractors/isExtractor.ts | 10 +- src/options/extractors/last.ts | 6 +- src/options/extractors/novalue.ts | 6 + src/options/types/compositeExtractors.ts | 98 ++--- src/reporter/JestAllure2Reporter.ts | 28 +- src/reporter/allureCommons.ts | 6 +- src/reporter/postProcessMetadata.ts | 5 +- 21 files changed, 454 insertions(+), 648 deletions(-) delete mode 100644 src/options/customizers/property.test.ts delete mode 100644 src/options/customizers/property.ts create mode 100644 src/options/customizers/testCase.test.ts create mode 100644 src/options/extractors/novalue.ts diff --git a/index.d.ts b/index.d.ts index ee577d51..deb19087 100644 --- a/index.d.ts +++ b/index.d.ts @@ -39,15 +39,15 @@ declare module 'jest-allure2-reporter' { * `Product defects`, `Test defects` based on the test case status: * `failed` and `broken` respectively. */ - categories?: TestRunPropertyCustomizer; + categories?: CategoriesCustomizer; /** * Configures the environment information that will be reported. */ - environment?: TestRunPropertyCustomizer>; + environment?: EnvironmentCustomizer; /** * Configures the executor information that will be reported. */ - executor?: TestRunPropertyCustomizer; + executor?: ExecutorCustomizer; /** * Customize extractor helpers object to use later in the customizers. */ @@ -57,21 +57,21 @@ declare module 'jest-allure2-reporter' { * This is normally used to report broken global setup and teardown hooks, * and to provide additional information about the test run. */ - testRun?: TestRunCustomizer; + testRun?: TestCaseCustomizer; /** * Customize how to report test files as pseudo-test cases. * This is normally used to report broken test files, so that you can be aware of them, * but advanced users may find other use cases. */ - testFile?: TestFileCustomizer; + testFile?: TestCaseCustomizer; /** * Customize how test cases are reported: names, descriptions, labels, status, etc. */ - testCase?: TestCaseCustomizer; + testCase?: TestCaseCustomizer; /** * Customize how individual test steps are reported. */ - testStep?: TestStepCustomizer; + testStep?: TestStepCustomizer; } export interface AttachmentsOptions { @@ -118,10 +118,10 @@ declare module 'jest-allure2-reporter' { environment: EnvironmentExtractor; executor: ExecutorExtractor; helpers: HelpersExtractor; - testCase: TestCaseExtractor; - testFile: TestFileExtractor; - testRun: TestRunExtractor; - testStep: TestStepExtractor; + testCase: TestCaseExtractor + testFile: TestCaseExtractor; + testRun: TestCaseExtractor; + testStep: TestStepExtractor } // endregion @@ -131,60 +131,60 @@ declare module 'jest-allure2-reporter' { /** * Global customizations for how test cases are reported */ - export interface TestCaseCustomizer { + export interface TestCaseCustomizer { /** * Extractor to omit test file cases from the report. */ - hidden?: TestCasePropertyCustomizer; + hidden?: PropertyCustomizer; /** * Test case ID extractor to fine-tune Allure's history feature. * @example ({ package, file, test }) => `${package.name}:${file.path}:${test.fullName}` * @example ({ test }) => `${test.identifier}:${test.title}` * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/history/#test-case-id */ - historyId?: TestCasePropertyCustomizer; + historyId?: PropertyCustomizer; /** * Extractor for the default test or step name. * @default ({ test }) => test.title */ - name?: TestCasePropertyCustomizer; + name?: PropertyCustomizer; /** * Extractor for the full test case name. * @default ({ test }) => test.fullName */ - fullName?: TestCasePropertyCustomizer; + fullName?: PropertyCustomizer; /** * Extractor for the test case start timestamp. */ - start?: TestCasePropertyCustomizer; + start?: PropertyCustomizer; /** * Extractor for the test case stop timestamp. */ - stop?: TestCasePropertyCustomizer; + stop?: PropertyCustomizer; /** * Extractor for the test case description. * @example ({ testCaseMetadata }) => '```js\n' + testCaseMetadata.sourceCode + '\n```' */ - description?: TestCasePropertyCustomizer; + description?: PropertyCustomizer; /** * Extractor for the test case description in HTML format. * @example ({ testCaseMetadata }) => '
' + testCaseMetadata.sourceCode + '
' */ - descriptionHtml?: TestCasePropertyCustomizer; + descriptionHtml?: PropertyCustomizer; /** * Extractor for the test case stage. */ - stage?: TestCasePropertyCustomizer; + stage?: PropertyCustomizer; /** * Extractor for the test case status. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * @example ({ value }) => value === 'broken' ? 'failed' : value */ - status?: TestCasePropertyCustomizer; + status?: PropertyCustomizer; /** * Extractor for the test case status details. */ - statusDetails?: TestCasePropertyCustomizer; + statusDetails?: PropertyCustomizer; /** * Customize Allure labels for the test case. * @@ -194,7 +194,7 @@ declare module 'jest-allure2-reporter' { * subSuite: ({ test }) => test.ancestorTitles[0], * } */ - labels?: TestCaseLabelsCustomizer; + labels?: LabelsCustomizer; /** * Resolve issue links for the test case. * @@ -207,389 +207,161 @@ declare module 'jest-allure2-reporter' { * }), * } */ - links?: TestCaseLinksCustomizer; + links?: LinksCustomizer; /** * Customize step or test case attachments. */ - attachments?: TestCaseAttachmentsCustomizer; + attachments?: AttachmentsCustomizer; /** * Customize step or test case parameters. */ - parameters?: TestCaseParametersCustomizer; + parameters?: ParametersCustomizer; } /** * Global customizations for how test steps are reported, e.g. * beforeAll, beforeEach, afterEach, afterAll hooks and custom steps. */ - export interface TestStepCustomizer { + export interface TestStepCustomizer { /** * Extractor to omit test steps from the report. */ - hidden?: TestStepPropertyCustomizer; + hidden?: PropertyCustomizer; /** * Extractor for the step name. * @example ({ value }) => value.replace(/(before|after)(Each|All)/, (_, p1, p2) => p1 + ' ' + p2.toLowerCase()) */ - name?: TestStepPropertyCustomizer; + name?: PropertyCustomizer; /** * Extractor for the test step start timestamp. */ - start?: TestStepPropertyCustomizer; + start?: PropertyCustomizer; /** * Extractor for the test step stop timestamp. */ - stop?: TestStepPropertyCustomizer; + stop?: PropertyCustomizer; /** * Extractor for the test step stage. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * TODO: add example */ - stage?: TestStepPropertyCustomizer; + stage?: PropertyCustomizer; /** * Extractor for the test step status. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * @example ({ value }) => value === 'broken' ? 'failed' : value */ - status?: TestStepPropertyCustomizer; + status?: PropertyCustomizer; /** * Extractor for the test step status details. */ - statusDetails?: TestStepPropertyCustomizer; + statusDetails?: PropertyCustomizer; /** * Customize step or test step attachments. */ - attachments?: TestStepAttachmentsCustomizer; + attachments?: AttachmentsCustomizer; /** * Customize step or test step parameters. */ - parameters?: TestStepParametersCustomizer; + parameters?: ParametersCustomizer; } - /** - * Global customizations for how test files are reported (as pseudo-test cases). - */ - export interface TestFileCustomizer { - /** - * Extractor to omit test file cases from the report. - */ - hidden?: TestFilePropertyCustomizer; - /** - * Test file ID extractor to fine-tune Allure's history feature. - * @default ({ filePath }) => filePath.join('/') - * @example ({ package, filePath }) => `${package.name}:${filePath.join('/')}` - * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/history/#test-case-id - */ - historyId?: TestFilePropertyCustomizer; - /** - * Extractor for test file name - * @default ({ filePath }) => filePath.at(-1) - */ - name?: TestFilePropertyCustomizer; - /** - * Extractor for the full test file name - * @default ({ testFile }) => testFile.testFilePath - */ - fullName?: TestFilePropertyCustomizer; - /** - * Extractor for the test file start timestamp. - */ - start?: TestFilePropertyCustomizer; - /** - * Extractor for the test file stop timestamp. - */ - stop?: TestFilePropertyCustomizer; - /** - * Extractor for the test file description. - */ - description?: TestFilePropertyCustomizer; - /** - * Extractor for the test file description in HTML format. - */ - descriptionHtml?: TestFilePropertyCustomizer; - /** - * Extractor for the test file stage. - */ - stage?: TestFilePropertyCustomizer; - /** - * Extractor for the test file status. - * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ - * @example ({ value }) => value === 'broken' ? 'failed' : value - */ - status?: TestFilePropertyCustomizer; - /** - * Extractor for the test file status details. - */ - statusDetails?: TestFilePropertyCustomizer; - /** - * Customize Allure labels for the test file. - * - * @example - * { - * suite: ({ file }) => file.path, - * subSuite: ({ test }) => test.ancestorTitles[0], - * } - */ - labels?: TestFileLabelsCustomizer; - /** - * Resolve issue links for the test file. - * - * @example - * { - * issue: ({ value }) => ({ - * type: 'issue', - * name: value.name ?? `Open ${value.url} in JIRA`, - * url: `https://jira.company.com/${value.url}`, - * }), - * } - */ - links?: TestFileLinksCustomizer; - /** - * Customize test file attachments. - */ - attachments?: TestFileAttachmentsCustomizer; - /** - * Customize test case parameters. - */ - parameters?: TestFileParametersCustomizer; - } + export type CategoriesCustomizer = PropertyCustomizer; - /** - * Global customizations for how test runs (sessions) are reported (as pseudo-test cases). - */ - export interface TestRunCustomizer { - /** - * Extractor to omit pseudo-test cases for test runs from the report. - */ - hidden?: TestRunPropertyCustomizer; - /** - * Test run ID extractor to fine-tune Allure's history feature. - * @default () => process.argv.slice(2).join(' ') - */ - historyId?: TestRunPropertyCustomizer; - /** - * Extractor for test run name - * @default () => '(test run)' - */ - name?: TestRunPropertyCustomizer; - /** - * Extractor for the full test run name - * @default () => process.argv.slice(2).join(' ') - */ - fullName?: TestRunPropertyCustomizer; - /** - * Extractor for the test run start timestamp. - */ - start?: TestRunPropertyCustomizer; - /** - * Extractor for the test run stop timestamp. - */ - stop?: TestRunPropertyCustomizer; - /** - * Extractor for the test run description. - * Use this to provide additional information about the test run, - * which is not covered by the default Allure reporter capabilities. - */ - description?: TestRunPropertyCustomizer; - /** - * Extractor for the test run description in HTML format. - * @see {@link TestRunCustomizer#description} - */ - descriptionHtml?: TestRunPropertyCustomizer; - /** - * Extractor for the test run stage. - * 'interrupted' is used for failures with `--bail` enabled. - * Otherwise, 'finished' is used. - */ - stage?: TestRunPropertyCustomizer; - /** - * Extractor for the test run status. - * Either 'passed' or 'failed'. - */ - status?: TestRunPropertyCustomizer; - /** - * Extractor for the test file status details. - */ - statusDetails?: TestRunPropertyCustomizer; - /** - * Customize Allure labels for the pseudo-test case representing the test run. - */ - labels?: TestRunLabelsCustomizer; - /** - * Customize Allure links for the pseudo-test case representing the test run. - */ - links?: TestRunLinksCustomizer; - /** - * Customize test run attachments. - */ - attachments?: TestRunAttachmentsCustomizer; - /** - * Customize test run parameters. - */ - parameters?: TestRunParametersCustomizer; - } + export type EnvironmentCustomizer = PropertyCustomizer, GlobalExtractorContext>; + + export type ExecutorCustomizer = PropertyCustomizer; export type HelpersCustomizer = HelpersExtractor> | Partial; - export type TestRunAttachmentsCustomizer = TestRunPropertyCustomizer; - export type TestRunLabelsCustomizer = - | TestRunPropertyCustomizer - | Record>; - export type TestRunLinksCustomizer = - | TestRunPropertyCustomizer - | Record>; - export type TestRunParametersCustomizer = - | TestRunPropertyCustomizer - | Record[]>>; - - export type TestFileAttachmentsCustomizer = TestFilePropertyCustomizer; - export type TestFileLabelsCustomizer = - | TestFilePropertyCustomizer - | Record>; - export type TestFileLinksCustomizer = - | TestFilePropertyCustomizer - | Record>; - export type TestFileParametersCustomizer = - | TestFilePropertyCustomizer - | Record[]>>; - - export type TestCaseAttachmentsCustomizer = TestCasePropertyCustomizer; - export type TestCaseLabelsCustomizer = - | TestCasePropertyCustomizer - | Record>; - export type TestCaseLinksCustomizer = - | TestCasePropertyCustomizer - | Record>; - export type TestCaseParametersCustomizer = - | TestCasePropertyCustomizer - | Record[]>>; - - export type TestStepAttachmentsCustomizer = TestStepPropertyCustomizer; - export type TestStepParametersCustomizer = - | TestStepPropertyCustomizer - | Record[]>>; - - export type TestRunPropertyCustomizer = T | Ta | TestRunPropertyExtractor; - - export type TestFilePropertyCustomizer = T | Ta | TestFilePropertyExtractor; - - export type TestCasePropertyCustomizer = T | Ta | TestCasePropertyExtractor; - - export type TestStepPropertyCustomizer = T | Ta | TestStepPropertyExtractor; + export type AttachmentsCustomizer = PropertyCustomizer; + + export type LabelsCustomizer = + | PropertyCustomizer + | Record>; + + export type LinksCustomizer = + | PropertyCustomizer + | Record>; + + export type ParametersCustomizer = + | PropertyCustomizer + | Record | Primitive, Context>>; + + export type PropertyCustomizer< + R, + Ra = never, + Context = never, + V = R + > = R | Ra | PropertyExtractor; // endregion // region Extractors - export type Extractor< - T, - Ta = never, - C extends ExtractorContext = ExtractorContext, - > = (context: Readonly) => T | Ta | Promise; - - export type GlobalExtractor = Extractor< - T, - Ta, - GlobalExtractorContext - >; - - export type TestRunPropertyExtractor = Extractor< - T, - Ta, - TestRunExtractorContext - >; - - export type TestFilePropertyExtractor = Extractor< - T, - Ta, - TestFileExtractorContext - >; - - export type TestCasePropertyExtractor = Extractor< - T, - Ta, - TestCaseExtractorContext - >; - - export type TestStepPropertyExtractor = Extractor< - T, - Ta, - TestStepExtractorContext - >; - - export type HelpersExtractor = GlobalExtractor; - - export type EnvironmentExtractor = TestRunPropertyExtractor>; - - export type ExecutorExtractor = TestRunPropertyExtractor; - - export type CategoriesExtractor = TestRunPropertyExtractor; - - export type TestCaseExtractor = Extractor< - AllureTestCaseResult, - undefined, - TestCaseExtractorContext> - >; - - export type TestStepExtractor = Extractor< - AllureTestStepResult, - undefined, - TestStepExtractorContext> - >; - - export type TestFileExtractor = Extractor< - AllureTestCaseResult, - undefined, - TestFileExtractorContext> - >; - - export type TestRunExtractor = Extractor< - AllureTestCaseResult, - undefined, - TestRunExtractorContext> - >; - - export interface ExtractorContext { - readonly value: T | Promise; - } - - export interface GlobalExtractorContext - extends ExtractorContext, - GlobalExtractorContextAugmentation { + export type PropertyExtractor< + R, + Ra = never, + Context = {}, + V = R, + > = (context: PropertyExtractorContext) => R | Ra | Promise; + + export type PropertyExtractorContext = Readonly }>; + + export type HelpersExtractor = PropertyExtractor; + + export type EnvironmentExtractor = PropertyExtractor; + + export type ExecutorExtractor = PropertyExtractor; + + export type CategoriesExtractor = PropertyExtractor; + + export type TestCaseExtractor = PropertyExtractor; + + export type TestStepExtractor = PropertyExtractor; + + export interface GlobalExtractorContext extends GlobalExtractorContextAugmentation { $: Helpers; globalConfig: Config.GlobalConfig; config: ReporterConfig; } - export interface TestRunExtractorContext - extends GlobalExtractorContext, - TestRunExtractorContextAugmentation { + export interface TestRunExtractorContext extends GlobalExtractorContext, TestRunExtractorContextAugmentation { aggregatedResult: AggregatedResult; result: Partial; + testRunMetadata: AllureTestRunMetadata; } - export interface TestFileExtractorContext - extends GlobalExtractorContext, - TestFileExtractorContextAugmentation { + export interface TestFileExtractorContext extends GlobalExtractorContext, TestFileExtractorContextAugmentation { + aggregatedResult: AggregatedResult; filePath: string[]; - result: Partial; + testRunMetadata: AllureTestRunMetadata; testFile: TestResult; testFileMetadata: AllureTestFileMetadata; + result: Partial; } - export interface TestCaseExtractorContext - extends TestFileExtractorContext, - TestCaseExtractorContextAugmentation { - result: Partial; + export interface TestCaseExtractorContext extends GlobalExtractorContext, TestCaseExtractorContextAugmentation { + aggregatedResult: AggregatedResult; + filePath: string[]; + testRunMetadata: AllureTestRunMetadata; + testFile: TestResult; + testFileMetadata: AllureTestFileMetadata; testCase: TestCaseResult; testCaseMetadata: AllureTestCaseMetadata; + result: Partial; } - export interface TestStepExtractorContext - extends TestCaseExtractorContext, - TestStepExtractorContextAugmentation { - result: Partial; + export interface TestStepExtractorContext extends GlobalExtractorContext, TestStepExtractorContextAugmentation { + aggregatedResult: AggregatedResult; + filePath: string[]; + testRunMetadata: AllureTestRunMetadata; + testFile: TestResult; + testFileMetadata: AllureTestFileMetadata; + testCase: TestCaseResult; + testCaseMetadata: AllureTestCaseMetadata; testStepMetadata: AllureTestStepMetadata; + result: Partial; } export interface Helpers extends HelpersAugmentation { @@ -765,6 +537,12 @@ declare module 'jest-allure2-reporter' { /** @inheritDoc */ export interface AllureTestFileMetadata extends AllureTestCaseMetadata {} + /** @inheritDoc */ + export interface AllureTestRunMetadata extends AllureTestCaseMetadata { + sourceLocation?: never; + transformedCode?: never; + } + export interface AllureTestItemSourceLocation { fileName?: string; lineNumber?: number; @@ -869,6 +647,8 @@ declare module 'jest-allure2-reporter' { flaky?: boolean; } + export type EnvironmentInfo = Record; + export interface ExecutorInfo { name?: string; type?: diff --git a/src/metadata/docblockMapping.ts b/src/metadata/docblockMapping.ts index d26b193d..4db805f9 100644 --- a/src/metadata/docblockMapping.ts +++ b/src/metadata/docblockMapping.ts @@ -8,7 +8,7 @@ import type { LinkType, } from 'jest-allure2-reporter'; -import {asArray, isDefined} from '../utils'; +import { asArray, isDefined } from '../utils'; const ALL_LABELS = Object.keys( assertType>({ diff --git a/src/options/customizers/helpers.test.ts b/src/options/customizers/helpers.test.ts index 4a4cd01c..ee9e1f47 100644 --- a/src/options/customizers/helpers.test.ts +++ b/src/options/customizers/helpers.test.ts @@ -1,8 +1,4 @@ -import type { - GlobalExtractorContext, - ExtractorContext, - Helpers, -} from 'jest-allure2-reporter'; +import type { Helpers } from 'jest-allure2-reporter'; import { helpersCustomizer } from './helpers'; @@ -32,15 +28,14 @@ describe('helpersCustomizer', () => { stripAnsi: jest.fn(), }; - const context: ExtractorContext = { - value: defaultHelpers as Helpers, - }; - const extractor = helpersCustomizer(customHelpers)!; - const result = await extractor( - context as unknown as GlobalExtractorContext, - ); + const result = await extractor({ + $: defaultHelpers as Helpers, + globalConfig: {} as any, + config: {} as any, + value: Promise.resolve(defaultHelpers as Helpers), + }); expect(result).toEqual(customHelpers); }); diff --git a/src/options/customizers/helpers.ts b/src/options/customizers/helpers.ts index 763fc2e0..d8ad72bf 100644 --- a/src/options/customizers/helpers.ts +++ b/src/options/customizers/helpers.ts @@ -4,27 +4,24 @@ import type { HelpersExtractor, } from 'jest-allure2-reporter'; -import { - asExtractor, - isExtractorFn as isExtractorFunction, -} from '../extractors'; +import { asExtractor } from '../extractors'; import { compactObject } from '../../utils'; export function helpersCustomizer( value: HelpersCustomizer | undefined, ): HelpersExtractor> | undefined { - if (isExtractorFunction>>(value)) { - return value; - } - if (value == null) { return; } - const compact = compactObject(value); - if (Object.keys(compact).length === 0) { - return; + if (typeof value === 'object') { + const compact = compactObject(value); + if (Object.keys(compact).length === 0) { + return; + } + + return asExtractor>(compact); } - return asExtractor>(value); + return value; } diff --git a/src/options/customizers/property.test.ts b/src/options/customizers/property.test.ts deleted file mode 100644 index db97b4ed..00000000 --- a/src/options/customizers/property.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { - Extractor, - TestCasePropertyCustomizer, - TestFilePropertyCustomizer, - TestRunPropertyCustomizer, - TestStepPropertyCustomizer, - TestCasePropertyExtractor, - TestFilePropertyExtractor, - TestRunPropertyExtractor, - TestStepPropertyExtractor, -} from 'jest-allure2-reporter'; - -import { asExtractor } from '../extractors'; - -export function propertyCustomizer( - value: TestRunPropertyCustomizer, -): TestRunPropertyExtractor | undefined; -export function propertyCustomizer( - value: TestFilePropertyCustomizer, -): TestFilePropertyExtractor | undefined; -export function propertyCustomizer( - value: TestCasePropertyCustomizer, -): TestCasePropertyExtractor | undefined; -export function propertyCustomizer( - value: TestStepPropertyCustomizer, -): TestStepPropertyExtractor | undefined; -export function propertyCustomizer( - value: T | Extractor, -): Extractor | undefined { - if (value == null) { - return; - } - - return asExtractor(value); -} diff --git a/src/options/customizers/property.ts b/src/options/customizers/property.ts deleted file mode 100644 index d5b53d02..00000000 --- a/src/options/customizers/property.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { - TestCasePropertyCustomizer, - TestFilePropertyCustomizer, - TestRunPropertyCustomizer, - TestStepPropertyCustomizer, - TestCasePropertyExtractor, - TestFilePropertyExtractor, - TestRunPropertyExtractor, - TestStepPropertyExtractor, -} from 'jest-allure2-reporter'; - -import { asExtractor } from '../extractors'; - -export function propertyCustomizer( - value: TestRunPropertyCustomizer, -): TestRunPropertyExtractor; -export function propertyCustomizer( - value: TestFilePropertyCustomizer, -): TestFilePropertyExtractor; -export function propertyCustomizer( - value: TestCasePropertyCustomizer, -): TestCasePropertyExtractor; -export function propertyCustomizer( - value: TestStepPropertyCustomizer, -): TestStepPropertyExtractor; -export function propertyCustomizer( - value: - | TestRunPropertyCustomizer - | TestFilePropertyCustomizer - | TestCasePropertyCustomizer - | TestStepPropertyCustomizer - | T - | R - | undefined, -): - | TestCasePropertyExtractor - | TestFilePropertyExtractor - | TestRunPropertyExtractor - | TestStepPropertyExtractor - | undefined { - if (value == null) { - return; - } - - return asExtractor(value as any); -} diff --git a/src/options/customizers/testCase.test.ts b/src/options/customizers/testCase.test.ts new file mode 100644 index 00000000..91938dba --- /dev/null +++ b/src/options/customizers/testCase.test.ts @@ -0,0 +1,145 @@ +import type { + AllureTestCaseResult, + PropertyExtractorContext, + TestCaseExtractorContext, + TestStepExtractor, + TestStepExtractorContext, +} from 'jest-allure2-reporter'; + +import type { TestCaseCompositeExtractor } from '../types/compositeExtractors'; +import { novalue } from '../extractors'; + +import { testCaseCustomizer } from './testCase'; + +describe('testCaseCustomizer', () => { + const createContext = (): PropertyExtractorContext< + TestCaseExtractorContext, + never + > => ({ + value: novalue(), + result: {}, + aggregatedResult: {} as any, + testRunMetadata: {} as any, + testCase: {} as any, + testCaseMetadata: {} as any, + filePath: [], + testFile: {} as any, + testFileMetadata: {} as any, + $: {} as any, + globalConfig: {} as any, + config: {} as any, + }); + + const defaultTestCaseExtractor: TestCaseCompositeExtractor = + { + attachments: ({ value }) => value, + description: ({ value }) => value, + descriptionHtml: ({ value }) => value, + displayName: ({ value }) => value, + fullName: ({ value }) => value, + hidden: ({ value }) => value, + historyId: ({ value }) => value, + labels: ({ value }) => value, + links: ({ value }) => value, + parameters: ({ value }) => value, + stage: ({ value }) => value, + start: ({ value }) => value, + status: ({ value }) => value, + statusDetails: ({ value }) => value, + stop: ({ value }) => value, + }; + + test.each` + property | defaultValue | extractedValue + ${'hidden'} | ${false} | ${false} + ${'displayName'} | ${''} | ${'Custom Test Case'} + ${'fullName'} | ${''} | ${'Custom Full Name'} + ${'historyId'} | ${''} | ${'custom-history-id'} + ${'start'} | ${Number.NaN} | ${1_234_567_890} + ${'stop'} | ${Number.NaN} | ${1_234_567_899} + ${'stage'} | ${'scheduled'} | ${'running'} + ${'status'} | ${'unknown'} | ${'passed'} + ${'statusDetails'} | ${{}} | ${{ message: 'Test case passed' }} + ${'labels'} | ${[]} | ${[{ name: 'label1', value: 'value1' }, { name: 'label2', value: 'value2' }]} + ${'links'} | ${[]} | ${[{ url: 'http://example.com', type: 'issue' }]} + ${'attachments'} | ${[]} | ${[{ name: 'attachment1' }, { name: 'attachment2' }]} + ${'parameters'} | ${[]} | ${[{ name: 'param1', value: 'value1' }, { name: 'param2', value: 'value2' }]} + ${'description'} | ${''} | ${'Custom description'} + ${'descriptionHtml'} | ${''} | ${'

Custom description

'} + `( + 'should extract $property with default value $defaultValue and extracted value $extractedValue', + async ({ + property, + defaultValue, + extractedValue, + }: { + property: keyof AllureTestCaseResult; + defaultValue: any; + extractedValue: any; + }) => { + const extractor = jest.fn().mockResolvedValue(extractedValue); + const testCase = testCaseCustomizer( + { + ...defaultTestCaseExtractor, + [property]: extractor, + }, + createTestStepExtractor(), + ); + + const result = await testCase(createContext()); + + expect(extractor).toHaveBeenCalledWith( + expect.objectContaining({ + value: defaultValue, + result: expect.any(Object), + }), + ); + + expect(result?.[property]).toEqual(extractedValue); + }, + ); + + it('should return undefined when hidden is true', async () => { + const hiddenExtractor = jest.fn().mockResolvedValue(true); + const testCase = testCaseCustomizer( + { + ...defaultTestCaseExtractor, + hidden: hiddenExtractor, + }, + createTestStepExtractor(), + ); + const context = createContext(); + const result = await testCase(context); + + expect(hiddenExtractor).toHaveBeenCalledWith( + expect.objectContaining({ value: false, result: { hidden: true } }), + ); + expect(result).toBeUndefined(); + }); + + it('should extract nested steps', async () => { + let counter = 0; + const testStep = jest.fn().mockImplementation(() => ({ + displayName: `Step ${++counter}`, + })); + + const testCase = testCaseCustomizer(defaultTestCaseExtractor, testStep); + + const context = createContext(); + context.testCaseMetadata.steps = [{}, {}]; + + const result = await testCase(context); + + expect(testStep).toHaveBeenCalledTimes(2); + expect(result?.steps).toHaveLength(2); + expect(result?.steps?.[0]?.displayName).toBe('Step 1'); + expect(result?.steps?.[1]?.displayName).toBe('Step 2'); + }); +}); + +function createTestStepExtractor(): TestStepExtractor { + let counter = 0; + return jest.fn().mockImplementation(() => ({ + displayName: `Step ${++counter}`, + })); +} diff --git a/src/options/customizers/testCase.ts b/src/options/customizers/testCase.ts index b1d942c8..c72803aa 100644 --- a/src/options/customizers/testCase.ts +++ b/src/options/customizers/testCase.ts @@ -1,24 +1,26 @@ import type { AllureTestCaseResult, + PropertyExtractorContext, TestCaseExtractor, - TestFileExtractor, - TestRunExtractor, + TestCaseExtractorContext, + TestFileExtractorContext, + TestRunExtractorContext, + TestStepExtractorContext, TestStepExtractor, } from 'jest-allure2-reporter'; -import type { - TestCaseCompositeExtractor, - TestFileCompositeExtractor, - TestRunCompositeExtractor, -} from '../types/compositeExtractors'; +import type { TestCaseCompositeExtractor } from '../types/compositeExtractors'; import { isDefined } from '../../utils'; - -export function testItemCustomizer( - testCase: TestCaseCompositeExtractor, - testStep: TestStepExtractor, - metadataKey?: 'testCaseMetadata' | 'testFileMetadata', -): TestCaseExtractor { - const extractor: TestCaseExtractor = async (context) => { +import { novalue } from '../extractors'; + +export function testItemCustomizer< + Context extends Partial, +>( + testCase: TestCaseCompositeExtractor, + testStep: TestStepExtractor, + metadataKey?: 'testCaseMetadata' | 'testFileMetadata' | 'testRunMetadata', +): TestCaseExtractor { + return async (context) => { const result: Partial = {}; result.hidden = await testCase.hidden({ ...context, @@ -114,19 +116,17 @@ export function testItemCustomizer( result, }); - const steps = metadataKey ? context[metadataKey].steps : undefined; + const steps = metadataKey ? context[metadataKey]?.steps : undefined; if (steps && steps.length > 0) { const allSteps = await Promise.all( - steps.map(async (testStepMetadata) => { - const stepResult = await testStep({ + steps.map(async (testStepMetadata) => + testStep({ ...context, testStepMetadata, - value: {}, - result, - }); - - return stepResult; - }), + value: novalue(), + result: {}, + } as PropertyExtractorContext), + ), ); result.steps = allSteps.filter(isDefined); @@ -134,34 +134,25 @@ export function testItemCustomizer( return result as AllureTestCaseResult; }; - - return extractor; } export function testCaseCustomizer( - testCase: TestCaseCompositeExtractor, - testStep: TestStepExtractor, -): TestCaseExtractor { + testCase: TestCaseCompositeExtractor, + testStep: TestStepExtractor, +): TestCaseExtractor { return testItemCustomizer(testCase, testStep, 'testCaseMetadata'); } export function testFileCustomizer( - testFile: TestFileCompositeExtractor, - testStep: TestStepExtractor, -): TestFileExtractor { - return testItemCustomizer( - testFile as unknown as TestCaseCompositeExtractor, - testStep, - 'testFileMetadata', - ) as unknown as TestFileExtractor; + testFile: TestCaseCompositeExtractor, + testStep: TestStepExtractor, +): TestCaseExtractor { + return testItemCustomizer(testFile, testStep, 'testFileMetadata'); } export function testRunCustomizer( - testRun: TestRunCompositeExtractor, - testStep: TestStepExtractor, -): TestRunExtractor { - return testItemCustomizer( - testRun as unknown as TestCaseCompositeExtractor, - testStep, - ) as unknown as TestRunExtractor; + testRun: TestCaseCompositeExtractor, + testStep: TestStepExtractor, +): TestCaseExtractor { + return testItemCustomizer(testRun, testStep, 'testRunMetadata'); } diff --git a/src/options/customizers/testStep.test.ts b/src/options/customizers/testStep.test.ts index 3630fac5..9377cfe1 100644 --- a/src/options/customizers/testStep.test.ts +++ b/src/options/customizers/testStep.test.ts @@ -1,17 +1,24 @@ import type { AllureTestStepResult, + PropertyExtractorContext, TestStepExtractorContext, } from 'jest-allure2-reporter'; import type { TestStepCompositeExtractor } from '../types/compositeExtractors'; +import { novalue } from '../extractors'; import { testStepCustomizer } from './testStep'; describe('testStepCustomizer', () => { - const createContext = (): TestStepExtractorContext => ({ - value: {}, + const createContext = (): PropertyExtractorContext< + TestStepExtractorContext, + never + > => ({ + value: novalue(), result: {}, - testStepMetadata: {} as any, + aggregatedResult: {} as any, + testRunMetadata: {} as any, + testStepMetadata: { hookType: 'beforeEach' } as any, testCase: {} as any, testCaseMetadata: {} as any, filePath: [], @@ -22,24 +29,22 @@ describe('testStepCustomizer', () => { config: {} as any, }); - const defaultCompositeExtractor: TestStepCompositeExtractor = { - attachments: ({ value }) => value, - displayName: ({ value }) => value, - hidden: ({ value }) => value, - hookType: ({ value }) => value, - parameters: ({ value }) => value, - stage: ({ value }) => value, - start: ({ value }) => value, - status: ({ value }) => value, - statusDetails: ({ value }) => value, - steps: ({ value }) => value, - stop: ({ value }) => value, - }; + const defaultCompositeExtractor: TestStepCompositeExtractor = + { + attachments: ({ value }) => value, + displayName: ({ value }) => value, + hidden: ({ value }) => value, + parameters: ({ value }) => value, + stage: ({ value }) => value, + start: ({ value }) => value, + status: ({ value }) => value, + statusDetails: ({ value }) => value, + stop: ({ value }) => value, + }; test.each` property | defaultValue | extractedValue ${'hidden'} | ${false} | ${false} - ${'hookType'} | ${undefined} | ${'beforeEach'} ${'displayName'} | ${''} | ${'Custom Step'} ${'start'} | ${Number.NaN} | ${1_234_567_890} ${'stop'} | ${Number.NaN} | ${1_234_567_899} @@ -93,6 +98,13 @@ describe('testStepCustomizer', () => { expect(result).toBeUndefined(); }); + it('should extract hookType directly from testStepMetadata', async () => { + const testStep = testStepCustomizer(defaultCompositeExtractor); + const context = createContext(); + const result = await testStep(context); + expect(result?.hookType).toBe('beforeEach'); + }); + it('should extract nested steps', async () => { let counter = 0; const testStep = testStepCustomizer({ diff --git a/src/options/customizers/testStep.ts b/src/options/customizers/testStep.ts index 974d9277..b5d63f72 100644 --- a/src/options/customizers/testStep.ts +++ b/src/options/customizers/testStep.ts @@ -1,15 +1,17 @@ import type { AllureTestStepResult, TestStepExtractor, + TestStepExtractorContext, } from 'jest-allure2-reporter'; import type { TestStepCompositeExtractor } from '../types/compositeExtractors'; import { isDefined } from '../../utils'; +import { novalue } from '../extractors'; export function testStepCustomizer( - testStep: TestStepCompositeExtractor, -): TestStepExtractor { - const extractor: TestStepExtractor = async (context) => { + testStep: TestStepCompositeExtractor, +): TestStepExtractor { + return async function testStepExtractor(context) { const result: Partial = {}; result.hidden = await testStep.hidden({ ...context, @@ -21,11 +23,7 @@ export function testStepCustomizer( return; } - result.hookType = await testStep.hookType({ - ...context, - value: undefined, - result, - }); + result.hookType = context.testStepMetadata.hookType; result.displayName = await testStep.displayName({ ...context, @@ -79,10 +77,10 @@ export function testStepCustomizer( if (steps && steps.length > 0) { const allSteps = await Promise.all( steps.map(async (testStepMetadata) => { - const stepResult = await extractor({ + const stepResult = await testStepExtractor({ ...context, testStepMetadata, - value: {}, + value: novalue(), result, }); @@ -95,6 +93,4 @@ export function testStepCustomizer( return result as AllureTestStepResult; }; - - return extractor; } diff --git a/src/options/extractors/asExtractor.ts b/src/options/extractors/asExtractor.ts index af1e3329..9c6ad5c4 100644 --- a/src/options/extractors/asExtractor.ts +++ b/src/options/extractors/asExtractor.ts @@ -1,43 +1,23 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Extractor } from 'jest-allure2-reporter'; - -import { isPromiseLike } from '../../utils'; +import type { + PropertyCustomizer, + PropertyExtractor, +} from 'jest-allure2-reporter'; import { isExtractor } from './isExtractor'; -/** - * Resolves the unknown value either as an extractor or it - * builds a fallback extractor that returns the given value. - * - * Since Allure 2 has a quirky convention that the first value - * in an array takes precedence, we on purpose put the custom - * value first and the default value second. - * - * The fallback extractor is capable both of merging arrays and - * defaulting the values. The former is useful for tags, the latter - * for the rest of the labels which don't support multiple occurrences. - */ -export function asExtractor( - maybeExtractor: T | Ta | Extractor, -): Extractor { - if (isExtractor(maybeExtractor)) { - return maybeExtractor; - } - - const value = maybeExtractor; - const extractor = (async ({ value: maybePromise }) => { - const baseValue = isPromiseLike(maybePromise) - ? await maybePromise - : maybePromise; - - if (Array.isArray(baseValue)) { - return Array.isArray(value) - ? [...baseValue, ...value] - : [...baseValue, value]; - } - - return value ?? baseValue; - }) as Extractor; - - return extractor; +export function asExtractor( + maybeExtractor: PropertyCustomizer, +): PropertyExtractor { + return isExtractor(maybeExtractor) + ? maybeExtractor + : () => maybeExtractor as R; } + +// export function asOptionalExtractor( +// maybeExtractor: PropertyCustomizer, +// ): PropertyExtractor { +// return isExtractor(maybeExtractor) +// ? maybeExtractor +// : ({ value }) => (maybeExtractor as R) ?? value; +// } diff --git a/src/options/extractors/composeExtractors.ts b/src/options/extractors/composeExtractors.ts index df984c67..63270de6 100644 --- a/src/options/extractors/composeExtractors.ts +++ b/src/options/extractors/composeExtractors.ts @@ -1,16 +1,19 @@ -import type { Extractor, ExtractorContext } from 'jest-allure2-reporter'; +import type { + PropertyExtractor, + PropertyExtractorContext, +} from 'jest-allure2-reporter'; import { once } from '../../utils'; -export function composeExtractors>( - a: Extractor | undefined, - b: Extractor, -): Extractor { +export function composeExtractors( + a: PropertyExtractor | undefined, + b: PropertyExtractor, +): PropertyExtractor { if (!a) { - return b; + return b as PropertyExtractor; } - return (context: any) => { + return (context) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { value, ...newContext } = context; Object.defineProperty(newContext, 'value', { @@ -18,6 +21,6 @@ export function composeExtractors>( enumerable: true, }); - return a(newContext as typeof context); + return a(newContext as PropertyExtractorContext); }; } diff --git a/src/options/extractors/extractors.test.ts b/src/options/extractors/extractors.test.ts index 8d2bfb9b..5edc960f 100644 --- a/src/options/extractors/extractors.test.ts +++ b/src/options/extractors/extractors.test.ts @@ -1,9 +1,11 @@ -import { composeExtractors, last } from '.'; +import type { PropertyExtractor } from 'jest-allure2-reporter'; + +import { composeExtractors, last, novalue } from '.'; describe('extractors', () => { describe('composeExtractors', () => { it('should compose extractors correctly in a complex scenario', async () => { - const one = () => 1; + const one: PropertyExtractor = () => 1; const two = composeExtractors(({ value }) => { assertEq(value, 1); return value * 2; @@ -19,7 +21,7 @@ describe('extractors', () => { await expect(value).resolves.toBe(6); return value; }, three); - const result = await threeAlso({ value: void 0 }); + const result = await threeAlso({ value: novalue() }); expect(result).toBe(6); }); }); @@ -36,7 +38,7 @@ describe('extractors', () => { }); it('should return undefined for a non-existent value', async () => { - const result = await last({ value: void 0 }); + const result = await last({ value: undefined }); expect(result).toBe(undefined); }); }); diff --git a/src/options/extractors/index.ts b/src/options/extractors/index.ts index 14e960ca..6e047581 100644 --- a/src/options/extractors/index.ts +++ b/src/options/extractors/index.ts @@ -2,3 +2,4 @@ export * from './asExtractor'; export * from './composeExtractors'; export * from './isExtractor'; export * from './last'; +export * from './novalue'; diff --git a/src/options/extractors/isExtractor.ts b/src/options/extractors/isExtractor.ts index 0bdcf8da..7d244fdb 100644 --- a/src/options/extractors/isExtractor.ts +++ b/src/options/extractors/isExtractor.ts @@ -1,12 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Extractor } from 'jest-allure2-reporter'; +import type { PropertyExtractor } from 'jest-allure2-reporter'; -export function isExtractor( +export function isExtractor( value: unknown, -): value is Extractor { +): value is PropertyExtractor { return typeof value === 'function'; } -export function isExtractorFn(value: unknown): value is T { +export function isExtractorFunction( + value: unknown, +): value is T { return typeof value === 'function'; } diff --git a/src/options/extractors/last.ts b/src/options/extractors/last.ts index 18819b8c..1090d32b 100644 --- a/src/options/extractors/last.ts +++ b/src/options/extractors/last.ts @@ -1,8 +1,10 @@ -import type { ExtractorContext } from 'jest-allure2-reporter'; +import type { PropertyExtractorContext } from 'jest-allure2-reporter'; import { isPromiseLike } from '../../utils'; -export const last = async (context: ExtractorContext) => { +export const last = async ( + context: PropertyExtractorContext, +): Promise => { const value = isPromiseLike(context.value) ? await context.value : context.value; diff --git a/src/options/extractors/novalue.ts b/src/options/extractors/novalue.ts new file mode 100644 index 00000000..be75f28a --- /dev/null +++ b/src/options/extractors/novalue.ts @@ -0,0 +1,6 @@ +export function novalue(): Promise { + // TODO: better error subclass and message + const promise = Promise.reject('Cannot use base value'); + promise.catch(() => {}); + return promise; +} diff --git a/src/options/types/compositeExtractors.ts b/src/options/types/compositeExtractors.ts index 7a5a2fe3..d884b506 100644 --- a/src/options/types/compositeExtractors.ts +++ b/src/options/types/compositeExtractors.ts @@ -1,84 +1,40 @@ import type { - AllureTestStepMetadata, - AllureTestStepResult, Attachment, Label, Link, Parameter, - Primitive, + PropertyExtractor, Stage, Status, StatusDetails, - TestCasePropertyExtractor, - TestFilePropertyExtractor, - TestRunPropertyExtractor, - TestStepPropertyExtractor, } from 'jest-allure2-reporter'; -export interface TestCaseCompositeExtractor { - hidden: TestCasePropertyExtractor; - historyId: TestCasePropertyExtractor; - displayName: TestCasePropertyExtractor; - fullName: TestCasePropertyExtractor; - start: TestCasePropertyExtractor; - stop: TestCasePropertyExtractor; - description: TestCasePropertyExtractor; - descriptionHtml: TestCasePropertyExtractor; - stage: TestCasePropertyExtractor; - status: TestCasePropertyExtractor; - statusDetails: TestCasePropertyExtractor; - labels: TestCasePropertyExtractor; - links: TestCasePropertyExtractor; - attachments: TestCasePropertyExtractor; - parameters: TestCasePropertyExtractor; +export interface TestCaseCompositeExtractor { + hidden: PropertyExtractor; + historyId: PropertyExtractor; + displayName: PropertyExtractor; + fullName: PropertyExtractor; + start: PropertyExtractor; + stop: PropertyExtractor; + description: PropertyExtractor; + descriptionHtml: PropertyExtractor; + stage: PropertyExtractor; + status: PropertyExtractor; + statusDetails: PropertyExtractor; + labels: PropertyExtractor; + links: PropertyExtractor; + attachments: PropertyExtractor; + parameters: PropertyExtractor; } -export interface TestFileCompositeExtractor { - hidden: TestFilePropertyExtractor; - historyId: TestFilePropertyExtractor; - displayName: TestFilePropertyExtractor; - fullName: TestFilePropertyExtractor; - start: TestFilePropertyExtractor; - stop: TestFilePropertyExtractor; - description: TestFilePropertyExtractor; - descriptionHtml: TestFilePropertyExtractor; - stage: TestFilePropertyExtractor; - status: TestFilePropertyExtractor; - statusDetails: TestFilePropertyExtractor; - labels: TestFilePropertyExtractor; - links: TestFilePropertyExtractor; - attachments: TestFilePropertyExtractor; - parameters: TestFilePropertyExtractor; -} - -export interface TestRunCompositeExtractor { - hidden: TestRunPropertyExtractor; - historyId: TestRunPropertyExtractor; - displayName: TestRunPropertyExtractor; - fullName: TestRunPropertyExtractor; - start: TestRunPropertyExtractor; - stop: TestRunPropertyExtractor; - description: TestRunPropertyExtractor; - descriptionHtml: TestRunPropertyExtractor; - stage: TestRunPropertyExtractor; - status: TestRunPropertyExtractor; - statusDetails: TestRunPropertyExtractor; - labels: TestRunPropertyExtractor; - links: TestRunPropertyExtractor; - attachments: TestRunPropertyExtractor; - parameters: TestRunPropertyExtractor; -} - -export interface TestStepCompositeExtractor { - hidden: TestStepPropertyExtractor; - hookType: TestStepPropertyExtractor; - displayName: TestStepPropertyExtractor; - start: TestStepPropertyExtractor; - stop: TestStepPropertyExtractor; - stage: TestStepPropertyExtractor; - status: TestStepPropertyExtractor; - statusDetails: TestStepPropertyExtractor; - steps: TestStepPropertyExtractor; - attachments: TestStepPropertyExtractor; - parameters: TestStepPropertyExtractor; +export interface TestStepCompositeExtractor { + hidden: PropertyExtractor; + displayName: PropertyExtractor; + start: PropertyExtractor; + stop: PropertyExtractor; + stage: PropertyExtractor; + status: PropertyExtractor; + statusDetails: PropertyExtractor; + attachments: PropertyExtractor; + parameters: PropertyExtractor; } diff --git a/src/reporter/JestAllure2Reporter.ts b/src/reporter/JestAllure2Reporter.ts index f1bb12fc..af85a9a5 100644 --- a/src/reporter/JestAllure2Reporter.ts +++ b/src/reporter/JestAllure2Reporter.ts @@ -18,6 +18,7 @@ import type { AllureTestCaseMetadata, AllureTestFileMetadata, Helpers, + PropertyExtractorContext, ReporterConfig, ReporterOptions, TestCaseExtractorContext, @@ -27,6 +28,7 @@ import type { import { resolveOptions } from '../options'; import { AllureMetadataProxy, MetadataSquasher } from '../metadata'; +import { novalue } from '../options/extractors'; import * as fallbacks from './fallbacks'; import { overwriteDirectory } from './overwriteDirectory'; @@ -113,14 +115,22 @@ export class JestAllure2Reporter extends JestMetadataReporter { const config = this._config; - const globalContext: TestRunExtractorContext = { + const globalMetadata = JestAllure2Reporter.query.globalMetadata(); + const globalMetadataProxy = new AllureMetadataProxy( + globalMetadata, + ); + + const globalContext: PropertyExtractorContext< + TestRunExtractorContext, + never + > = { $: this._$ as Helpers, aggregatedResult, config, globalConfig: this._globalConfig, - get value() { - return void 0; - }, + testRunMetadata: globalMetadataProxy.get(), + result: {}, + value: novalue(), }; const environment = await config.environment(globalContext); @@ -150,7 +160,10 @@ export class JestAllure2Reporter extends JestMetadataReporter { } for (const testResult of aggregatedResult.testResults) { - const testFileContext: TestFileExtractorContext = { + const testFileContext: PropertyExtractorContext< + TestFileExtractorContext, + never + > = { ...globalContext, filePath: path .relative(globalContext.globalConfig.rootDir, testResult.testFilePath) @@ -180,7 +193,10 @@ export class JestAllure2Reporter extends JestMetadataReporter { testInvocationMetadata, ); - const testCaseContext: TestCaseExtractorContext = { + const testCaseContext: PropertyExtractorContext< + TestCaseExtractorContext, + never + > = { ...testFileContext, testCase: testCaseResult, testCaseMetadata, diff --git a/src/reporter/allureCommons.ts b/src/reporter/allureCommons.ts index 4d0a8a83..ca28d106 100644 --- a/src/reporter/allureCommons.ts +++ b/src/reporter/allureCommons.ts @@ -68,8 +68,8 @@ function fillStep( executable: ExecutableItemWrapper, step: AllureTestCaseResult | AllureTestStepResult, ) { - if (step.name !== undefined) { - executable.name = step.name; + if (step.displayName !== undefined) { + executable.name = step.displayName; } if (step.start !== undefined) { executable.wrappedItem.start = step.start; @@ -95,7 +95,7 @@ function fillStep( if (step.steps) { for (const innerStep of step.steps) { fillStep( - executable.startStep(innerStep.name ?? '', innerStep.start), + executable.startStep(innerStep.displayName ?? '', innerStep.start), innerStep, ); } diff --git a/src/reporter/postProcessMetadata.ts b/src/reporter/postProcessMetadata.ts index 89abcbef..f51f495c 100644 --- a/src/reporter/postProcessMetadata.ts +++ b/src/reporter/postProcessMetadata.ts @@ -3,7 +3,10 @@ import type { TestFileMetadata } from 'jest-metadata'; import { AllureMetadataProxy } from '../metadata'; -export async function postProcessMetadata($: Helpers, testFile: TestFileMetadata) { +export async function postProcessMetadata( + $: Helpers, + testFile: TestFileMetadata, +) { const allDescribeBlocks = [...testFile.allDescribeBlocks()]; const allHooks = allDescribeBlocks.flatMap((describeBlock) => [ ...describeBlock.hookDefinitions(), From 0e849bb7d08dfb3a1b18339ef03bdbe3f319d933 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 23 Mar 2024 18:11:12 +0200 Subject: [PATCH 16/50] advance my path to testStep customizing --- index.d.ts | 140 ++++++++-------- package.json | 4 +- src/metadata/docblockMapping.ts | 4 +- src/metadata/squasher/MetadataSelector.ts | 4 +- src/metadata/squasher/__tests__/fixtures.ts | 2 +- src/options/compose-options/labels.ts | 4 +- .../compose-options/reporterOptions.ts | 50 ------ src/options/compose-options/testCase.ts | 61 +++---- src/options/compose-options/testStep.ts | 49 ++++-- src/options/custom-extractors/index.ts | 1 + .../custom-extractors/parameters.test.ts | 102 ++++++++++++ src/options/custom-extractors/parameters.ts | 154 ++++++++++++++++++ src/options/customizers/helpers.test.ts | 42 ----- src/options/customizers/helpers.ts | 27 --- src/options/customizers/index.ts | 4 + src/options/customizers/testCase.test.ts | 7 +- src/options/customizers/testCase.ts | 18 +- src/options/customizers/testStep.test.ts | 13 +- src/options/customizers/testStep.ts | 29 ++-- src/options/default-options/testCase.ts | 8 +- src/options/default-options/testFile.ts | 6 +- .../extractors/appenderExtractor.test.ts | 66 ++++++++ src/options/extractors/appenderExtractor.ts | 27 +++ src/options/extractors/asExtractor.ts | 23 --- .../extractors/composeExtractors2.test.ts | 33 ++++ ...oseExtractors.ts => composeExtractors2.ts} | 12 +- .../extractors/composeExtractors3.test.ts | 52 ++++++ src/options/extractors/composeExtractors3.ts | 18 ++ .../extractors/constantExtractor.test.ts | 40 +++++ src/options/extractors/constantExtractor.ts | 23 +++ src/options/extractors/extractors.test.ts | 49 ------ src/options/extractors/index.ts | 13 +- src/options/extractors/isExtractor.ts | 10 +- src/options/extractors/last.test.ts | 20 +++ .../extractors/mergerExtractor.test.ts | 62 +++++++ src/options/extractors/mergerExtractor.ts | 25 +++ .../extractors/optionalExtractor.test.ts | 73 +++++++++ src/options/extractors/optionalExtractor.ts | 32 ++++ src/options/index.ts | 78 ++++++++- src/options/types.ts | 124 ++++++++++++++ src/options/types/compositeExtractors.ts | 40 ----- src/options/types/index.ts | 0 src/realms/AllureRealm.ts | 4 +- src/reporter/JestAllure2Reporter.ts | 24 +-- src/runtime/AllureRuntimeContext.ts | 4 +- src/runtime/types.ts | 4 +- src/utils/asArray.ts | 6 +- src/utils/attempt.ts | 7 - src/utils/constant.ts | 4 - src/utils/getStatusDetails.ts | 5 +- src/utils/index.ts | 21 ++- src/utils/isDefined.ts | 3 - src/utils/isError.ts | 3 - src/utils/isJestAssertionError.ts | 5 +- src/utils/isNonNullish.ts | 3 + src/utils/isObject.ts | 5 - src/utils/last.ts | 4 - src/utils/mapValues.test.ts | 59 +++++++ src/utils/mapValues.ts | 12 ++ src/utils/once.ts | 15 -- src/utils/stringifyValues.test.ts | 28 ++++ src/utils/stringifyValues.ts | 11 ++ 62 files changed, 1276 insertions(+), 500 deletions(-) delete mode 100644 src/options/compose-options/reporterOptions.ts create mode 100644 src/options/custom-extractors/index.ts create mode 100644 src/options/custom-extractors/parameters.test.ts create mode 100644 src/options/custom-extractors/parameters.ts delete mode 100644 src/options/customizers/helpers.test.ts delete mode 100644 src/options/customizers/helpers.ts create mode 100644 src/options/customizers/index.ts create mode 100644 src/options/extractors/appenderExtractor.test.ts create mode 100644 src/options/extractors/appenderExtractor.ts delete mode 100644 src/options/extractors/asExtractor.ts create mode 100644 src/options/extractors/composeExtractors2.test.ts rename src/options/extractors/{composeExtractors.ts => composeExtractors2.ts} (74%) create mode 100644 src/options/extractors/composeExtractors3.test.ts create mode 100644 src/options/extractors/composeExtractors3.ts create mode 100644 src/options/extractors/constantExtractor.test.ts create mode 100644 src/options/extractors/constantExtractor.ts delete mode 100644 src/options/extractors/extractors.test.ts create mode 100644 src/options/extractors/last.test.ts create mode 100644 src/options/extractors/mergerExtractor.test.ts create mode 100644 src/options/extractors/mergerExtractor.ts create mode 100644 src/options/extractors/optionalExtractor.test.ts create mode 100644 src/options/extractors/optionalExtractor.ts create mode 100644 src/options/types.ts delete mode 100644 src/options/types/compositeExtractors.ts delete mode 100644 src/options/types/index.ts delete mode 100644 src/utils/attempt.ts delete mode 100644 src/utils/constant.ts delete mode 100644 src/utils/isDefined.ts delete mode 100644 src/utils/isError.ts create mode 100644 src/utils/isNonNullish.ts delete mode 100644 src/utils/isObject.ts delete mode 100644 src/utils/last.ts create mode 100644 src/utils/mapValues.test.ts create mode 100644 src/utils/mapValues.ts delete mode 100644 src/utils/once.ts create mode 100644 src/utils/stringifyValues.test.ts create mode 100644 src/utils/stringifyValues.ts diff --git a/index.d.ts b/index.d.ts index deb19087..8c4cbc13 100644 --- a/index.d.ts +++ b/index.d.ts @@ -107,25 +107,6 @@ declare module 'jest-allure2-reporter' { // endregion - // region Config - - export interface ReporterConfig extends ReporterConfigAugmentation { - overwrite: boolean; - resultsDir: string; - injectGlobals: boolean; - attachments: Required; - categories: CategoriesExtractor; - environment: EnvironmentExtractor; - executor: ExecutorExtractor; - helpers: HelpersExtractor; - testCase: TestCaseExtractor - testFile: TestCaseExtractor; - testRun: TestCaseExtractor; - testStep: TestStepExtractor - } - - // endregion - // region Customizers /** @@ -135,56 +116,56 @@ declare module 'jest-allure2-reporter' { /** * Extractor to omit test file cases from the report. */ - hidden?: PropertyCustomizer; + hidden?: PropertyCustomizer; /** * Test case ID extractor to fine-tune Allure's history feature. * @example ({ package, file, test }) => `${package.name}:${file.path}:${test.fullName}` * @example ({ test }) => `${test.identifier}:${test.title}` * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/history/#test-case-id */ - historyId?: PropertyCustomizer; + historyId?: PropertyCustomizer; /** * Extractor for the default test or step name. * @default ({ test }) => test.title */ - name?: PropertyCustomizer; + displayName?: PropertyCustomizer; /** * Extractor for the full test case name. * @default ({ test }) => test.fullName */ - fullName?: PropertyCustomizer; + fullName?: PropertyCustomizer; /** * Extractor for the test case start timestamp. */ - start?: PropertyCustomizer; + start?: PropertyCustomizer; /** * Extractor for the test case stop timestamp. */ - stop?: PropertyCustomizer; + stop?: PropertyCustomizer; /** * Extractor for the test case description. * @example ({ testCaseMetadata }) => '```js\n' + testCaseMetadata.sourceCode + '\n```' */ - description?: PropertyCustomizer; + description?: PropertyCustomizer; /** * Extractor for the test case description in HTML format. * @example ({ testCaseMetadata }) => '
' + testCaseMetadata.sourceCode + '
' */ - descriptionHtml?: PropertyCustomizer; + descriptionHtml?: PropertyCustomizer; /** * Extractor for the test case stage. */ - stage?: PropertyCustomizer; + stage?: PropertyCustomizer; /** * Extractor for the test case status. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * @example ({ value }) => value === 'broken' ? 'failed' : value */ - status?: PropertyCustomizer; + status?: PropertyCustomizer; /** * Extractor for the test case status details. */ - statusDetails?: PropertyCustomizer; + statusDetails?: PropertyCustomizer; /** * Customize Allure labels for the test case. * @@ -226,74 +207,106 @@ declare module 'jest-allure2-reporter' { /** * Extractor to omit test steps from the report. */ - hidden?: PropertyCustomizer; + hidden?: PropertyCustomizer; /** * Extractor for the step name. * @example ({ value }) => value.replace(/(before|after)(Each|All)/, (_, p1, p2) => p1 + ' ' + p2.toLowerCase()) */ - name?: PropertyCustomizer; + displayName?: PropertyCustomizer; /** * Extractor for the test step start timestamp. */ - start?: PropertyCustomizer; + start?: PropertyCustomizer; /** * Extractor for the test step stop timestamp. */ - stop?: PropertyCustomizer; + stop?: PropertyCustomizer; /** * Extractor for the test step stage. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * TODO: add example */ - stage?: PropertyCustomizer; + stage?: PropertyCustomizer; /** * Extractor for the test step status. * @see https://wix-incubator.github.io/jest-allure2-reporter/docs/config/statuses/ * @example ({ value }) => value === 'broken' ? 'failed' : value */ - status?: PropertyCustomizer; + status?: PropertyCustomizer; /** * Extractor for the test step status details. */ - statusDetails?: PropertyCustomizer; + statusDetails?: PropertyCustomizer, Context>; /** * Customize step or test step attachments. */ - attachments?: AttachmentsCustomizer; + attachments?: AttachmentsCustomizer; /** * Customize step or test step parameters. */ - parameters?: ParametersCustomizer; + parameters?: ParametersCustomizer; } - export type CategoriesCustomizer = PropertyCustomizer; + export type CategoriesCustomizer = Category[] | PropertyExtractor; - export type EnvironmentCustomizer = PropertyCustomizer, GlobalExtractorContext>; + export type EnvironmentCustomizer = PropertyCustomizer, undefined, GlobalExtractorContext>; - export type ExecutorCustomizer = PropertyCustomizer; + export type ExecutorCustomizer = PropertyExtractor | Partial; - export type HelpersCustomizer = HelpersExtractor> | Partial; + export type HelpersCustomizer = PropertyExtractor | Partial; - export type AttachmentsCustomizer = PropertyCustomizer; + export type AttachmentsCustomizer = PropertyCustomizer; export type LabelsCustomizer = - | PropertyCustomizer - | Record>; + | PropertyCustomizer + | Record>; + + export type KeyedLabelCustomizer = + | undefined + | null + | string + | PropertyExtractor< + Array | string>, + Partial