diff --git a/e2e/src/programmatic/grouping/client/auth/LoginScreen.test.ts b/e2e/src/programmatic/grouping/client/auth/LoginScreen.test.ts index 7485de4..e08f881 100644 --- a/e2e/src/programmatic/grouping/client/auth/LoginScreen.test.ts +++ b/e2e/src/programmatic/grouping/client/auth/LoginScreen.test.ts @@ -7,7 +7,7 @@ */ import LoginHelper from '../../../../utils/LoginHelper'; -import {allure} from "../../../../../../src/api"; +import {$Tag, $Epic, $Feature, $Story, allure} from 'jest-allure2-reporter/api'; $Tag('client'); $Epic('Authentication'); @@ -38,6 +38,7 @@ describe('Login screen', () => { }); it('should show error on short or invalid password format', () => { + allure.status('failed', { message: 'The password is too short' }); // ... }); }); diff --git a/e2e/src/programmatic/grouping/server/controllers/__snapshots__/forgotPassword.test.ts.snap b/e2e/src/programmatic/grouping/server/controllers/__snapshots__/forgotPassword.test.ts.snap new file mode 100644 index 0000000..1150bae --- /dev/null +++ b/e2e/src/programmatic/grouping/server/controllers/__snapshots__/forgotPassword.test.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`POST /forgot-password should return 401 if user is not found 1`] = ` +{ + "code": 401, + "seed": 0.06426173461501827, +} +`; diff --git a/e2e/src/programmatic/grouping/server/controllers/forgotPassword.test.ts b/e2e/src/programmatic/grouping/server/controllers/forgotPassword.test.ts index 0fd8f04..1a460f6 100644 --- a/e2e/src/programmatic/grouping/server/controllers/forgotPassword.test.ts +++ b/e2e/src/programmatic/grouping/server/controllers/forgotPassword.test.ts @@ -1,10 +1,12 @@ +import { $Tag, $Epic, $Feature, $Story } from 'jest-allure2-reporter/api'; + $Tag('server'); $Epic('Authentication'); $Feature('Restore account'); describe('POST /forgot-password', () => { $Story('Validation'); it('should return 401 if user is not found', () => { - // ... + expect({ code: 401, seed: Math.random() }).toMatchSnapshot(); }); $Story('Happy path'); diff --git a/e2e/src/programmatic/grouping/statuses.test.ts b/e2e/src/programmatic/grouping/statuses.test.ts new file mode 100644 index 0000000..3e6f415 --- /dev/null +++ b/e2e/src/programmatic/grouping/statuses.test.ts @@ -0,0 +1,40 @@ +import { allure } from 'jest-allure2-reporter/api'; + +const dummyTest = () => expect(true).toBe(true); +const passingAssertion = () => expect(2 + 2).toBe(4); +const failingAssertion = () => expect(2 + 2).toBe(5); +const failingSnapshot = () => expect({ seed: Math.random() }).toMatchSnapshot(); +const brokenAssertion = () => { throw new Error('This assertion is broken'); }; + +type Fn = () => any; + +describe('Status tests', () => { + describe('Simple', () => { + test('passed', passingAssertion); + test('failed assertion', failingAssertion); + test('failed snapshot', failingSnapshot); + test('broken', brokenAssertion); + test.skip('skipped', passingAssertion); + test.todo('todo'); + }); + + describe.each([ + ['passing', passingAssertion], + ['failed assertion in a ', failingAssertion], + ['failed snapshot in a ', failingSnapshot], + ['broken', brokenAssertion], + ])('Status override in a %s', (_parentSuite, callback) => { + describe.each([ + ['test', (callback: Fn) => (test('', callback), void 0)], + ['beforeAll hook', (callback: Fn) => (beforeAll(callback), test('', dummyTest), void 0)], + ['beforeEach hook', (callback: Fn) => (beforeEach(callback), test('', dummyTest), void 0)], + ['afterEach hook', (callback: Fn) => (afterEach(callback), test('', dummyTest), void 0)], + ['afterAll hook', (callback: Fn) => (afterAll(callback), test('', dummyTest), void 0)], + ])('%s', (_suite, hook) => { + hook(function () { + allure.status('unknown', { message: 'Custom message', trace: 'Custom trace' }); + callback(); + }); + }); + }); +}); diff --git a/index.d.ts b/index.d.ts index 730b318..61a6fc0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -476,14 +476,6 @@ declare module 'jest-allure2-reporter' { * @example ['steps', '0'] */ currentStep?: AllureTestStepPath; - /** - * 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. - */ - sourceLocation?: AllureTestItemSourceLocation; /** * Markdown description of the test case or test file, or plain text description of a test step. */ @@ -492,6 +484,14 @@ 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. + */ + sourceLocation?: AllureTestItemSourceLocation; /** * Indicates test item execution progress. */ diff --git a/package.json b/package.json index 12cfc73..f1fe26c 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "dependencies": { "@noomorph/allure-js-commons": "^2.3.0", "ci-info": "^3.8.0", - "jest-metadata": "^1.3.1", + "jest-metadata": "^1.4.1", "lodash.snakecase": "^4.1.1", "node-fetch": "^2.6.7", "pkg-up": "^3.1.0", diff --git a/src/environment/listener.ts b/src/environment/listener.ts index adaaced..9c82a8e 100644 --- a/src/environment/listener.ts +++ b/src/environment/listener.ts @@ -1,9 +1,4 @@ import type { Circus } from '@jest/types'; -import type { - AllureTestCaseMetadata, - AllureTestStepMetadata, - Status, -} from 'jest-allure2-reporter'; import type { EnvironmentListenerFn, TestEnvironmentCircusEvent, @@ -13,6 +8,7 @@ import * as StackTrace from 'stacktrace-js'; import * as api from '../api'; import realm from '../realms'; +import { getStatusDetails, isJestAssertionError } from '../utils'; const listener: EnvironmentListenerFn = (context) => { context.testEvents @@ -33,7 +29,7 @@ const listener: EnvironmentListenerFn = (context) => { .on('add_hook', addSourceLocation) .on('add_hook', addHookType) .on('add_test', addSourceLocation) - .on('test_start', executableStart) + .on('test_start', testStart) .on('test_todo', testSkip) .on('test_skip', testSkip) .on('test_done', testDone) @@ -111,80 +107,79 @@ function addSourceCode({ event }: TestEnvironmentCircusEvent) { } // eslint-disable-next-line no-empty-pattern -function executableStart({}: TestEnvironmentCircusEvent) { - const metadata: AllureTestStepMetadata = { - start: Date.now(), +function executableStart( + _event: TestEnvironmentCircusEvent< + Circus.Event & { name: 'hook_start' | 'test_fn_start' } + >, +) { + realm.runtimeContext.getCurrentMetadata().assign({ stage: 'running', - }; - - realm.runtimeContext.getCurrentMetadata().assign(metadata); + start: Date.now(), + }); } function executableFailure({ - event, + event: { error }, }: TestEnvironmentCircusEvent< Circus.Event & { name: 'test_fn_failure' | 'hook_failure' } >) { - const metadata: AllureTestStepMetadata = { - stop: Date.now(), + realm.runtimeContext.getCurrentMetadata().assign({ stage: 'interrupted', - status: 'failed', - }; - - if (event.error) { - const message = event.error.message ?? `${event.error}`; - const trace = event.error.stack; - - metadata.statusDetails = { message, trace }; - } - - realm.runtimeContext.getCurrentMetadata().assign(metadata); + status: isJestAssertionError(error) ? 'failed' : 'broken', + statusDetails: getStatusDetails(error), + stop: Date.now(), + }); } -// eslint-disable-next-line no-empty-pattern -function executableSuccess({}: TestEnvironmentCircusEvent) { - const metadata: AllureTestStepMetadata = { - stop: Date.now(), +function executableSuccess( + _event: TestEnvironmentCircusEvent< + Circus.Event & { name: 'test_fn_success' | 'hook_success' } + >, +) { + realm.runtimeContext.getCurrentMetadata().assign({ stage: 'finished', - status: 'passed', - }; + stop: Date.now(), + }); +} - realm.runtimeContext.getCurrentMetadata().assign(metadata); +function testStart( + _event: TestEnvironmentCircusEvent, +) { + realm.runtimeContext.getCurrentMetadata().assign({ + start: Date.now(), + }); } -function testSkip() { - const metadata: AllureTestCaseMetadata = { +function testSkip( + _event: TestEnvironmentCircusEvent< + Circus.Event & { name: 'test_skip' | 'test_todo' } + >, +) { + realm.runtimeContext.getCurrentMetadata().assign({ stop: Date.now(), - stage: 'pending', - status: 'skipped', - }; - - realm.runtimeContext.getCurrentMetadata().assign(metadata); + }); } function testDone({ event, }: TestEnvironmentCircusEvent) { - const hasErrors = event.test.errors.length > 0; - const errorStatus: Status = event.test.errors.some((errors) => { - return Array.isArray(errors) - ? errors.some(isMatcherError) - : isMatcherError(errors); - }) - ? 'failed' - : 'broken'; - - const metadata: AllureTestCaseMetadata = { - stop: Date.now(), - stage: hasErrors ? 'interrupted' : 'finished', - status: hasErrors ? errorStatus : 'passed', - }; - - realm.runtimeContext.getCurrentMetadata().assign(metadata); -} + const current = realm.runtimeContext.getCurrentMetadata(); + + if (event.test.errors.length > 0) { + const hasFailedAssertions = event.test.errors.some((errors) => { + return Array.isArray(errors) + ? errors.some(isJestAssertionError) + : isJestAssertionError(errors); + }); + + current.assign({ + status: hasFailedAssertions ? 'failed' : 'broken', + }); + } -function isMatcherError(error: any) { - return Boolean(error?.matcherResult); + realm.runtimeContext.getCurrentMetadata().assign({ + stop: Date.now(), + }); } export default listener; diff --git a/src/metadata/MetadataSquasher.ts b/src/metadata/MetadataSquasher.ts index aaee65f..0a55d9f 100644 --- a/src/metadata/MetadataSquasher.ts +++ b/src/metadata/MetadataSquasher.ts @@ -6,7 +6,7 @@ import type { AllureTestStepMetadata, } from 'jest-allure2-reporter'; -import { getStart, getStop } from './utils'; +import { getStage, getStart, getStatusAndDetails, getStop } from './utils'; import { PREFIX } from './constants'; import { mergeTestCaseMetadata, @@ -42,10 +42,12 @@ export class MetadataSquasher { resolveTestStep, ); const steps = result.steps ?? []; + const stage = getStage(metadata); return { ...result, - + ...getStatusAndDetails(metadata), + stage, start: getStart(metadata), stop: getStop(metadata), steps: [...befores, ...steps, ...afters], diff --git a/src/metadata/constants.ts b/src/metadata/constants.ts index ec1b020..807f76e 100644 --- a/src/metadata/constants.ts +++ b/src/metadata/constants.ts @@ -1,8 +1,10 @@ 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; diff --git a/src/metadata/proxies/AllureMetadataProxy.ts b/src/metadata/proxies/AllureMetadataProxy.ts index 8cabaf5..5f0ecfb 100644 --- a/src/metadata/proxies/AllureMetadataProxy.ts +++ b/src/metadata/proxies/AllureMetadataProxy.ts @@ -35,6 +35,11 @@ export class AllureMetadataProxy { return this; } + defaults(values: Partial): this { + this.$metadata.defaults(this.$localPath(), values); + return this; + } + protected $localPath(key?: keyof T, ...innerKeys: string[]): string[] { const allKeys = key ? [key as string, ...innerKeys] : innerKeys; return [PREFIX, ...allKeys]; diff --git a/src/metadata/proxies/AllureTestItemMetadataProxy.ts b/src/metadata/proxies/AllureTestItemMetadataProxy.ts index d7885e4..d13b643 100644 --- a/src/metadata/proxies/AllureTestItemMetadataProxy.ts +++ b/src/metadata/proxies/AllureTestItemMetadataProxy.ts @@ -39,7 +39,7 @@ export class AllureTestItemMetadataProxy< } $stopStep(): this { - const currentStep = this.$metadata.get(CURRENT_STEP, []) as string[]; + const currentStep = this.$metadata.get(CURRENT_STEP, [] as string[]); this.$metadata.set(CURRENT_STEP, currentStep.slice(0, -2)); return this; } diff --git a/src/metadata/utils/getStage.ts b/src/metadata/utils/getStage.ts new file mode 100644 index 0000000..f990b36 --- /dev/null +++ b/src/metadata/utils/getStage.ts @@ -0,0 +1,20 @@ +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/getStatusAndDetails.ts b/src/metadata/utils/getStatusAndDetails.ts new file mode 100644 index 0000000..2ed15b0 --- /dev/null +++ b/src/metadata/utils/getStatusAndDetails.ts @@ -0,0 +1,51 @@ +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/index.ts b/src/metadata/utils/index.ts index ac3d3e4..ec7e9ab 100644 --- a/src/metadata/utils/index.ts +++ b/src/metadata/utils/index.ts @@ -1,2 +1,4 @@ +export * from './getStage'; export * from './getStart'; +export * from './getStatusAndDetails'; export * from './getStop'; diff --git a/src/options/default-options/testCase.ts b/src/options/default-options/testCase.ts index da077d0..438cb64 100644 --- a/src/options/default-options/testCase.ts +++ b/src/options/default-options/testCase.ts @@ -2,17 +2,14 @@ import path from 'node:path'; import type { TestCaseResult } from '@jest/reporters'; import type { + AllureTestStepMetadata, ExtractorContext, - ResolvedTestCaseCustomizer, - TestCaseCustomizer, - TestCaseExtractorContext, -} from 'jest-allure2-reporter'; -import type { Label, + ResolvedTestCaseCustomizer, Stage, Status, - StatusDetails, - AllureTestStepMetadata, + TestCaseCustomizer, + TestCaseExtractorContext, } from 'jest-allure2-reporter'; import { @@ -20,6 +17,7 @@ import { composeExtractors, stripStatusDetails, } from '../utils'; +import { getStatusDetails } from '../../utils'; const identity = (context: ExtractorContext) => context.value; const last = (context: ExtractorContext) => context.value?.at(-1); @@ -64,12 +62,16 @@ export const testCase: ResolvedTestCaseCustomizer = { testCaseMetadata.start ?? (testCaseMetadata.stop ?? Date.now()) - (testCase.duration ?? 0), stop: ({ testCaseMetadata }) => testCaseMetadata.stop ?? Date.now(), - stage: ({ testCase }) => getTestCaseStage(testCase), + stage: ({ testCase, testCaseMetadata }) => + testCaseMetadata.stage ?? getTestCaseStage(testCase), status: ({ testCase, testCaseMetadata }) => testCaseMetadata.status ?? getTestCaseStatus(testCase), statusDetails: ({ testCase, testCaseMetadata }) => stripStatusDetails( - testCaseMetadata.statusDetails ?? getTestCaseStatusDetails(testCase), + testCaseMetadata.statusDetails ?? + stripStatusDetails( + getStatusDetails((testCase.failureMessages ?? []).join('\n')), + ), ), attachments: ({ testCaseMetadata }) => testCaseMetadata.attachments ?? [], parameters: ({ testCaseMetadata }) => testCaseMetadata.parameters ?? [], @@ -97,12 +99,15 @@ export const testCase: ResolvedTestCaseCustomizer = { function getTestCaseStatus(testCase: TestCaseResult): Status { const hasErrors = testCase.failureMessages?.length > 0; + const hasBuiltinErrors = + hasErrors && testCase.failureMessages.some(looksLikeBroken); + switch (testCase.status) { case 'passed': { return 'passed'; } case 'failed': { - return 'failed'; + return hasBuiltinErrors ? 'broken' : 'failed'; } case 'skipped': { return 'skipped'; @@ -121,11 +126,23 @@ function getTestCaseStatus(testCase: TestCaseResult): Status { } } +// TODO: include JestAllure2Error as well +function looksLikeBroken(errorMessage: string): boolean { + return errorMessage + ? errorMessage.startsWith('Error: \n') || + errorMessage.startsWith('EvalError:') || + errorMessage.startsWith('RangeError:') || + errorMessage.startsWith('ReferenceError:') || + errorMessage.startsWith('SyntaxError:') || + errorMessage.startsWith('TypeError:') || + errorMessage.startsWith('URIError:') + : true; +} + function getTestCaseStage(testCase: TestCaseResult): Stage { switch (testCase.status) { case 'passed': - case 'focused': - case 'failed': { + case 'focused': { return 'finished'; } case 'todo': @@ -139,15 +156,3 @@ function getTestCaseStage(testCase: TestCaseResult): Stage { } } } - -function getTestCaseStatusDetails( - testCase: TestCaseResult, -): StatusDetails | undefined { - const message = (testCase.failureMessages ?? []).join('\n'); - return message - ? stripStatusDetails({ - message: message.split('\n', 1)[0], - trace: message, - }) - : undefined; -} diff --git a/src/options/default-options/testFile.ts b/src/options/default-options/testFile.ts index de78ddd..009f4cb 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 { TestResult } from '@jest/reporters'; import type { ExtractorContext, TestFileExtractorContext, ResolvedTestFileCustomizer, TestCaseCustomizer, } from 'jest-allure2-reporter'; -import type { Label, Link, StatusDetails } from 'jest-allure2-reporter'; +import type { Label, Link } from 'jest-allure2-reporter'; import { aggregateLabelCustomizers, composeExtractors, stripStatusDetails, } from '../utils'; +import { getStatusDetails } from '../../utils'; const identity = (context: ExtractorContext) => context.value; const last = (context: ExtractorContext) => context.value?.at(-1); @@ -37,11 +37,12 @@ export const testFile: ResolvedTestFileCustomizer = { descriptionHtml: () => void 0, start: ({ testFileMetadata }) => testFileMetadata.start, stop: ({ testFileMetadata }) => testFileMetadata.stop, - stage: () => 'finished', - status: ({ testFile }: TestFileExtractorContext) => - testFile.testExecError ? 'broken' : 'passed', + stage: ({ testFile }) => + testFile.testExecError == null ? 'finished' : 'interrupted', + status: ({ testFile }) => + testFile.testExecError == null ? 'passed' : 'broken', statusDetails: ({ testFile }) => - stripStatusDetails(getTestFileStatusDetails(testFile)), + stripStatusDetails(getStatusDetails(testFile.testExecError)), attachments: ({ testFileMetadata }) => testFileMetadata.attachments ?? [], parameters: ({ testFileMetadata }) => testFileMetadata.parameters ?? [], labels: composeExtractors>( @@ -65,17 +66,3 @@ export const testFile: ResolvedTestFileCustomizer = { links: ({ testFileMetadata }: TestFileExtractorContext) => testFileMetadata.links ?? [], }; - -function getTestFileStatusDetails( - testFile: TestResult, -): StatusDetails | undefined { - const message = - testFile.testExecError?.stack || `${testFile.testExecError || ''}`; - - return message - ? stripStatusDetails({ - message: message.split('\n', 2).join('\n'), - trace: message, - }) - : undefined; -} diff --git a/src/options/default-options/testStep.ts b/src/options/default-options/testStep.ts index 2be5724..36a970b 100644 --- a/src/options/default-options/testStep.ts +++ b/src/options/default-options/testStep.ts @@ -1,4 +1,9 @@ -import type { ResolvedTestStepCustomizer } from 'jest-allure2-reporter'; +import type { + AllureTestStepMetadata, + Stage, + Status, + ResolvedTestStepCustomizer, +} from 'jest-allure2-reporter'; import { stripStatusDetails } from '../utils'; @@ -9,9 +14,19 @@ export const testStep: ResolvedTestStepCustomizer = { start: ({ testStepMetadata }) => testStepMetadata.start, stop: ({ testStepMetadata }) => testStepMetadata.stop, stage: ({ testStepMetadata }) => testStepMetadata.stage, - status: ({ testStepMetadata }) => testStepMetadata.status, + status: ({ testStepMetadata }) => + testStepMetadata.status ?? inferStatus(testStepMetadata), statusDetails: ({ testStepMetadata }) => stripStatusDetails(testStepMetadata.statusDetails), attachments: ({ testStepMetadata }) => testStepMetadata.attachments ?? [], parameters: ({ testStepMetadata }) => testStepMetadata.parameters ?? [], }; + +function inferStatus({ stage }: AllureTestStepMetadata): Status { + return (stage && statuses[stage]) || 'unknown'; +} + +const statuses: Partial> = { + finished: 'passed', + interrupted: 'broken', +}; diff --git a/src/realms/AllureRealm.ts b/src/realms/AllureRealm.ts index 88c6b58..516d274 100644 --- a/src/realms/AllureRealm.ts +++ b/src/realms/AllureRealm.ts @@ -16,7 +16,9 @@ export class AllureRealm { 'config', ); if (!config) { - throw new Error('Shared reporter config is not defined'); + throw new Error( + "Cannot receive jest-allure2-reporter's config from the parent process. Have you set up Jest test environment correctly?", + ); } return config as SharedReporterConfig; diff --git a/src/runtime/AllureRuntime.test.ts b/src/runtime/AllureRuntime.test.ts index 10a1a1e..e5c71a7 100644 --- a/src/runtime/AllureRuntime.test.ts +++ b/src/runtime/AllureRuntime.test.ts @@ -55,6 +55,8 @@ describe('AllureRuntime', () => { runtime.attachment('attachment2', 'second', 'text/plain'); const error = new Error('Sync error'); error.stack = 'Test stack'; + // Simulating Jest assertion error + Object.assign(error, { matcherResult: undefined }); throw error; }); } catch { diff --git a/src/runtime/AllureRuntime.ts b/src/runtime/AllureRuntime.ts index 0d9cc5d..57d204d 100644 --- a/src/runtime/AllureRuntime.ts +++ b/src/runtime/AllureRuntime.ts @@ -57,22 +57,30 @@ export class AllureRuntime implements IAllureRuntime { flush = () => this.#context.flush(); description: IAllureRuntime['description'] = (value) => { + // TODO: assert is a string this.#coreModule.description(value); }; descriptionHtml: IAllureRuntime['descriptionHtml'] = (value) => { + // TODO: assert is a string this.#coreModule.descriptionHtml(value); }; label: IAllureRuntime['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) => { + // 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 }); }; parameter: IAllureRuntime['parameter'] = (name, value, options) => { + // TODO: assert name is a string this.#coreModule.parameter({ name, value: String(value), @@ -92,6 +100,7 @@ export class AllureRuntime implements IAllureRuntime { }; status: IAllureRuntime['status'] = (status, statusDetails) => { + // TODO: assert string literal this.#coreModule.status(status); if (isObject(statusDetails)) { this.#coreModule.statusDetails(statusDetails); @@ -99,10 +108,13 @@ export class AllureRuntime implements IAllureRuntime { }; statusDetails: IAllureRuntime['statusDetails'] = (statusDetails) => { - this.#coreModule.statusDetails(statusDetails || {}); + // TODO: assert is not nullish + this.#coreModule.statusDetails(statusDetails); }; step: IAllureRuntime['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 @@ -111,6 +123,7 @@ export class AllureRuntime implements IAllureRuntime { maybeParameters, maybeFunction, ) => { + // TODO: assert nameFormat is a string const function_: any = maybeFunction ?? maybeParameters; if (typeof function_ !== 'function') { throw new TypeError( @@ -130,6 +143,7 @@ export class AllureRuntime implements IAllureRuntime { }; attachment: IAllureRuntime['attachment'] = (name, content, mimeType) => + // TODO: assert name is a string this.#contentAttachmentsModule.attachment(content, { name, mimeType, @@ -140,6 +154,7 @@ export class AllureRuntime implements IAllureRuntime { filePath, nameOrOptions, ) => { + // TODO: assert filePath is a string const options = typeof nameOrOptions === 'string' ? { name: nameOrOptions } @@ -152,6 +167,8 @@ export class AllureRuntime implements IAllureRuntime { function_, nameOrOptions, ) => { + // TODO: assert function_ is a function + // TODO: assert nameOrOptions is a string or an object const options = typeof nameOrOptions === 'string' ? { name: nameOrOptions } @@ -164,6 +181,8 @@ export class AllureRuntime implements IAllureRuntime { function_, nameOrOptions, ) => { + // TODO: assert function_ is a function + // TODO: assert nameOrOptions is a string or an object const options = typeof nameOrOptions === 'string' ? { name: nameOrOptions } diff --git a/src/runtime/__snapshots__/AllureRuntime.test.ts.snap b/src/runtime/__snapshots__/AllureRuntime.test.ts.snap index b668dfd..ac67315 100644 --- a/src/runtime/__snapshots__/AllureRuntime.test.ts.snap +++ b/src/runtime/__snapshots__/AllureRuntime.test.ts.snap @@ -30,7 +30,6 @@ exports[`AllureRuntime should add attachments within the steps 1`] = ` "stage": "finished", "start": 0, "status": "passed", - "statusDetails": undefined, "steps": [ { "attachments": [ @@ -43,12 +42,11 @@ exports[`AllureRuntime should add attachments within the steps 1`] = ` "description": [ "inner step 1", ], - "stage": "finished", + "stage": "interrupted", "start": 1, "status": "failed", "statusDetails": { - "message": "Sync error", - "trace": "Test stack", + "message": "Error: Sync error", }, "stop": 2, }, @@ -59,7 +57,6 @@ exports[`AllureRuntime should add attachments within the steps 1`] = ` "stage": "finished", "start": 3, "status": "passed", - "statusDetails": undefined, "stop": 4, }, { @@ -79,12 +76,11 @@ exports[`AllureRuntime should add attachments within the steps 1`] = ` "value": "fourth", }, ], - "stage": "finished", + "stage": "interrupted", "start": 5, - "status": "failed", + "status": "broken", "statusDetails": { - "message": "Async error", - "trace": "Test stack", + "message": "Error: Async error", }, "stop": 6, }, diff --git a/src/runtime/modules/StepsModule.ts b/src/runtime/modules/StepsModule.ts index e436c7d..1a8a048 100644 --- a/src/runtime/modules/StepsModule.ts +++ b/src/runtime/modules/StepsModule.ts @@ -1,10 +1,8 @@ -import type { - AllureTestCaseMetadata, - Status, - StatusDetails, -} from 'jest-allure2-reporter'; - -import { isPromiseLike } from '../../utils'; +import { + isPromiseLike, + isJestAssertionError, + getStatusDetails, +} from '../../utils'; import type { AllureTestItemMetadataProxy } from '../../metadata'; import type { AllureRuntimeContext } from '../AllureRuntimeContext'; @@ -39,20 +37,16 @@ export class StepsModule { this.context.metadata.set('stage', 'running'); result.then( - () => end('passed'), - (error) => - end('failed', { message: error.message, trace: error.stack }), + () => end(), + (error: unknown) => end(error), ); } else { - end('passed'); + end(); } return result; } catch (error: unknown) { - end('failed', { - message: (error as Error).message, - trace: (error as Error).stack, - }); + end(error); throw error; } } @@ -65,20 +59,19 @@ export class StepsModule { }); }; - #stopStep = (status: Status, statusDetails?: StatusDetails) => { - const metadata = this.context.metadata; - const existing = metadata.get>( - undefined, - {}, - ); - - metadata - .assign({ - stage: 'finished', - status: existing.status ?? status, - statusDetails: existing.statusDetails ?? statusDetails, + #stopStep = (error?: unknown) => { + if (error === undefined) { + this.context.metadata + .defaults({ status: 'passed' }) + .assign({ stage: 'finished', stop: this.context.now }); + } else { + this.context.metadata.assign({ + stage: 'interrupted', + status: isJestAssertionError(error) ? 'failed' : 'broken', + statusDetails: getStatusDetails(error), stop: this.context.now, - }) - .$stopStep(); + }); + } + this.context.metadata.$stopStep(); }; } diff --git a/src/utils/getStatusDetails.ts b/src/utils/getStatusDetails.ts new file mode 100644 index 0000000..2355137 --- /dev/null +++ b/src/utils/getStatusDetails.ts @@ -0,0 +1,27 @@ +import type { StatusDetails } from 'jest-allure2-reporter'; + +import { isError } from './isError'; + +export function getStatusDetails( + maybeError: unknown, +): StatusDetails | undefined { + if (maybeError) { + const error = maybeError as Error; + const trace = + isError(maybeError) || typeof error === 'string' + ? String(error) + : error.stack || error.message || JSON.stringify(error); + + const stackIndex = trace.indexOf('\n at '); + return stackIndex === -1 + ? { + message: trace, + } + : { + message: trace.slice(0, stackIndex), + trace: ' ' + trace.slice(stackIndex).trimStart(), + }; + } + + return; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index bc431ec..d5735f6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,9 +1,11 @@ export * from './attempt'; export * from './constant'; export * from './formatString'; +export * from './getStatusDetails'; export * from './hijackFunction'; export * from './isObject'; export * from './isError'; +export * from './isJestAssertionError'; export * from './isPromiseLike'; export * from './last'; export * from './once'; diff --git a/src/utils/isError.ts b/src/utils/isError.ts index 4fdc6ba..e5aeb94 100644 --- a/src/utils/isError.ts +++ b/src/utils/isError.ts @@ -1,3 +1,3 @@ -export default function isError(error: unknown): error is Error { +export function isError(error: unknown): error is Error { return error instanceof Error; } diff --git a/src/utils/isJestAssertionError.ts b/src/utils/isJestAssertionError.ts new file mode 100644 index 0000000..ac7957e --- /dev/null +++ b/src/utils/isJestAssertionError.ts @@ -0,0 +1,7 @@ +import type { JestAssertionError } from 'expect'; + +export function isJestAssertionError( + error: unknown, +): error is JestAssertionError { + return error ? 'matcherResult' in (error as JestAssertionError) : false; +}