From a6a31b14754e7c7f716ebded635a10f5b03c1dde Mon Sep 17 00:00:00 2001 From: epszaw Date: Tue, 4 Feb 2025 17:57:13 +0100 Subject: [PATCH 1/3] fix problem when parent step markes as passed, but contains failed child --- .../src/sdk/reporter/utils.ts | 1 + packages/allure-playwright/src/index.ts | 10 +++- .../allure-playwright/test/spec/hooks.spec.ts | 60 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/allure-js-commons/src/sdk/reporter/utils.ts b/packages/allure-js-commons/src/sdk/reporter/utils.ts index 1fbdbf672..c27867107 100644 --- a/packages/allure-js-commons/src/sdk/reporter/utils.ts +++ b/packages/allure-js-commons/src/sdk/reporter/utils.ts @@ -54,6 +54,7 @@ export const getWorstTestStepResult = (steps: StepResult[]): StepResult | undefi if (steps.length === 0) { return; } + return [...steps].sort((a, b) => statusToPriority(a.status) - statusToPriority(b.status))[0]; }; diff --git a/packages/allure-playwright/src/index.ts b/packages/allure-playwright/src/index.ts index dc264898f..1efc78ed1 100644 --- a/packages/allure-playwright/src/index.ts +++ b/packages/allure-playwright/src/index.ts @@ -40,6 +40,7 @@ import { getLanguageLabel, getPackageLabel, getThreadLabel, + getWorstTestStepResult, md5, parseTestPlan, readImageAsBase64, @@ -313,14 +314,16 @@ export class AllureReporter implements ReporterV2 { } const testUuid = this.allureResultsUuids.get(test.id)!; - const currentStep = this.allureRuntime!.currentStep(testUuid); + if (!currentStep) { return; } this.allureRuntime!.updateStep(currentStep, (stepResult) => { - stepResult.status = step.error ? Status.FAILED : Status.PASSED; + const { status = Status.PASSED } = getWorstTestStepResult(stepResult.steps) ?? {}; + + stepResult.status = step.error ? Status.FAILED : status; stepResult.stage = Stage.FINISHED; if (step.error) { @@ -361,6 +364,7 @@ export class AllureReporter implements ReporterV2 { const skipReason = test.annotations?.find( (annotation) => annotation.type === "skip" || annotation.type === "fixme", )?.description; + if (skipReason) { testResult.statusDetails = { ...testResult.statusDetails, message: skipReason }; } @@ -371,9 +375,11 @@ export class AllureReporter implements ReporterV2 { }); const attachmentSteps = this.attachmentSteps.get(testUuid) ?? []; + for (let i = 0; i < result.attachments.length; i++) { const attachment = result.attachments[i]; const attachmentStep = attachmentSteps.length > i ? attachmentSteps[i] : undefined; + await this.processAttachment(testUuid, attachmentStep, attachment); } diff --git a/packages/allure-playwright/test/spec/hooks.spec.ts b/packages/allure-playwright/test/spec/hooks.spec.ts index 17c05dcef..eb8ff9dbe 100644 --- a/packages/allure-playwright/test/spec/hooks.spec.ts +++ b/packages/allure-playwright/test/spec/hooks.spec.ts @@ -1,4 +1,5 @@ import { expect, it } from "vitest"; +import { Status } from "allure-js-commons"; import { runPlaywrightInlineTest } from "../utils.js"; it("handles before hooks", async () => { @@ -62,3 +63,62 @@ it("handles after hooks", async () => { }), ); }); + +it("should mark step as failed when any child step is failed", async () => { + const results = await runPlaywrightInlineTest({ + "sample.test.js": ` + import test from '@playwright/test'; + + test("should contain hooks", async ({ page }) => { + await page.waitForEvent("en_event"); + }); + `, + "playwright.config.js": ` + module.exports = { + reporter: [ + [ + "allure-playwright", + { + resultsDir: "./allure-results", + }, + ], + ["dot"], + ], + projects: [ + { + name: "project", + }, + ], + timeout: 1000, + screenshot: "on", + }; + `, + }); + + expect(results.tests[0]).toEqual( + expect.objectContaining({ + name: "should contain hooks", + status: Status.BROKEN, + steps: [ + expect.objectContaining({ + name: "Before Hooks", + status: Status.PASSED, + }), + expect.objectContaining({ + name: "page.waitForEvent", + status: Status.FAILED, + steps: [ + expect.objectContaining({ + name: "After Hooks", + status: Status.FAILED, + }), + ], + }), + expect.objectContaining({ + name: "Worker Cleanup", + status: Status.PASSED, + }), + ], + }), + ); +}); From cec96d3efd65486194448b180af7e1a4fa70938a Mon Sep 17 00:00:00 2001 From: epszaw Date: Fri, 7 Feb 2025 13:25:39 +0100 Subject: [PATCH 2/3] add shallow stack class fix problem with incorrect before/after hooks reporting in playwright --- .../src/sdk/reporter/ReporterRuntime.ts | 64 ++++++++- .../src/sdk/reporter/index.ts | 2 +- .../test/sdk/reporter/ReporterRuntime.spec.ts | 134 +++++++++++++++++- packages/allure-playwright/src/index.ts | 100 ++++++++++++- packages/allure-playwright/src/utils.ts | 23 +++ .../allure-playwright/test/spec/hooks.spec.ts | 68 ++++++++- 6 files changed, 378 insertions(+), 13 deletions(-) diff --git a/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts b/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts index c468ecd9e..d800dd71c 100644 --- a/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts +++ b/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts @@ -36,7 +36,7 @@ import { deepClone, formatLinks, getTestResultHistoryId, getTestResultTestCaseId import { buildAttachmentFileName } from "./utils/attachments.js"; import { resolveWriter } from "./writer/loader.js"; -interface StepStack { +export interface StepStack { clear(): void; removeRoot(rootUuid: string): void; @@ -48,7 +48,67 @@ interface StepStack { removeStep(stepUuid: string): void; } -class DefaultStepStack implements StepStack { +/** + * Simpler steps stack implementation that contains only the current steps without root nodes + * Useful, when you need to create steps without binding them to a specific test or fixture + * @example + * ```js + * const stack = new ShallowStepsStack(); + * + * stack.startStep({ name: "step 1" }); + * stack.startStep({ name: "step 1.1" }); + * stack.stopStep({ status: Status.FAILED }); + * stack.stopStep({ status: Status.PASSED }); + * stack.steps // => [{ name: "step 1", status: Status.PASSED, steps: [{ name: "step 1.1", status: Status.FAILED }] }] + * ``` + */ +export class ShallowStepsStack { + steps: StepResult[] = []; + + #runningSteps: StepResult[] = []; + + get #currentStep() { + return this.#runningSteps[this.#runningSteps.length - 1]; + } + + startStep(step: Partial) { + const stepResult: StepResult = { + ...createStepResult(), + ...step, + }; + + if (this.#currentStep) { + this.#currentStep.steps.push(stepResult); + } else { + this.steps.push(stepResult); + } + + this.#runningSteps.push(stepResult); + } + + updateStep(step?: Partial) { + if (!this.#currentStep) { + throw new Error("there is no step to stop"); + } + + Object.assign(this.#currentStep, step); + } + + stopStep(step?: Partial) { + if (!this.#currentStep) { + throw new Error("there is no step to stop"); + } + + Object.assign(this.#currentStep, { + stage: Stage.FINISHED, + ...step, + }); + + this.#runningSteps.pop(); + } +} + +export class DefaultStepStack implements StepStack { private stepsByRoot: Map = new Map(); private rootsByStep: Map = new Map(); diff --git a/packages/allure-js-commons/src/sdk/reporter/index.ts b/packages/allure-js-commons/src/sdk/reporter/index.ts index 47ad0b5d5..41033a577 100644 --- a/packages/allure-js-commons/src/sdk/reporter/index.ts +++ b/packages/allure-js-commons/src/sdk/reporter/index.ts @@ -4,7 +4,7 @@ export * from "./utils.js"; export * from "./testplan.js"; export * from "./factory.js"; export { LifecycleState } from "./LifecycleState.js"; -export { ReporterRuntime } from "./ReporterRuntime.js"; +export { type StepStack, DefaultStepStack, ReporterRuntime, ShallowStepsStack } from "./ReporterRuntime.js"; export { InMemoryWriter } from "./writer/InMemoryWriter.js"; export { FileSystemWriter } from "./writer/FileSystemWriter.js"; export { MessageWriter } from "./writer/MessageWriter.js"; diff --git a/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts b/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts index 609070c16..19b5fe6b5 100644 --- a/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts +++ b/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { type Link, Stage, Status, type TestResult } from "../../../src/model.js"; import { getTestResultHistoryId } from "../../../src/sdk/reporter"; -import { ReporterRuntime } from "../../../src/sdk/reporter/ReporterRuntime.js"; +import { ReporterRuntime, ShallowStepsStack } from "../../../src/sdk/reporter/ReporterRuntime.js"; import { mockWriter } from "../../utils/writer.js"; const fixtures = { @@ -788,4 +788,136 @@ describe("ReporterRuntime", () => { new ReporterRuntime({ writer: "InMemoryWriter" }); }); }); + + describe("ShallowStepsStack", () => { + it("should start and stop single step", () => { + const stack = new ShallowStepsStack(); + + stack.startStep({ name: "step1" }); + stack.stopStep({ status: Status.PASSED }); + + expect(stack.steps).toEqual([ + expect.objectContaining({ + name: "step1", + status: Status.PASSED, + stage: Stage.FINISHED, + steps: [], + }), + ]); + }); + + it("should start and stop multiple same level steps", () => { + const stack = new ShallowStepsStack(); + + stack.startStep({ name: "step1" }); + stack.stopStep(); + stack.startStep({ name: "step2" }); + stack.stopStep(); + stack.startStep({ name: "step3" }); + stack.stopStep(); + + expect(stack.steps).toEqual([ + expect.objectContaining({ + name: "step1", + steps: [], + }), + expect.objectContaining({ + name: "step2", + steps: [], + }), + expect.objectContaining({ + name: "step3", + steps: [], + }), + ]); + }); + + it("should start and stop nested steps", () => { + const stack = new ShallowStepsStack(); + + stack.startStep({ name: "step1" }); + stack.startStep({ name: "step1.1" }); + stack.startStep({ name: "step1.1.1" }); + stack.stopStep(); + stack.stopStep(); + stack.stopStep(); + + expect(stack.steps).toEqual([ + expect.objectContaining({ + name: "step1", + steps: [ + expect.objectContaining({ + name: "step1.1", + steps: [ + expect.objectContaining({ + name: "step1.1.1", + steps: [], + }), + ], + }), + ], + }), + ]); + }); + + it("should start and stop nested and same level steps", () => { + const stack = new ShallowStepsStack(); + + stack.startStep({ name: "step1" }); + stack.startStep({ name: "step1.1" }); + stack.startStep({ name: "step1.1.1" }); + stack.stopStep(); + stack.stopStep(); + stack.stopStep(); + stack.startStep({ name: "step2" }); + stack.stopStep(); + stack.startStep({ name: "step3" }); + stack.startStep({ name: "step3.1" }); + stack.startStep({ name: "step3.1.1" }); + stack.stopStep(); + stack.stopStep(); + stack.startStep({ name: "step3.2" }); + stack.stopStep(); + stack.stopStep(); + + expect(stack.steps).toEqual([ + expect.objectContaining({ + name: "step1", + steps: [ + expect.objectContaining({ + name: "step1.1", + steps: [ + expect.objectContaining({ + name: "step1.1.1", + steps: [], + }), + ], + }), + ], + }), + expect.objectContaining({ + name: "step2", + steps: [], + }), + expect.objectContaining({ + name: "step3", + steps: [ + expect.objectContaining({ + name: "step3.1", + steps: [ + expect.objectContaining({ + name: "step3.1.1", + steps: [], + }), + ], + }), + expect.objectContaining({ + name: "step3.2", + steps: [], + }), + ], + }), + ]); + }); + }); }); diff --git a/packages/allure-playwright/src/index.ts b/packages/allure-playwright/src/index.ts index 1efc78ed1..0d6fbcb07 100644 --- a/packages/allure-playwright/src/index.ts +++ b/packages/allure-playwright/src/index.ts @@ -18,6 +18,7 @@ import { LinkType, Stage, Status, + type StepResult, type TestResult, } from "allure-js-commons"; import type { RuntimeMessage, TestPlanV1Test } from "allure-js-commons/sdk"; @@ -31,7 +32,9 @@ import { import { ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE, ReporterRuntime, + ShallowStepsStack, createDefaultWriter, + createStepResult, escapeRegExp, formatLink, getEnvironmentLabels, @@ -47,7 +50,14 @@ import { } from "allure-js-commons/sdk/reporter"; import { allurePlaywrightLegacyApi } from "./legacy.js"; import type { AllurePlaywrightReporterConfig } from "./model.js"; -import { statusToAllureStats } from "./utils.js"; +import { + AFTER_HOOKS_ROOT_STEP_TITLE, + BEFORE_HOOKS_ROOT_STEP_TITLE, + isAfterHookStep, + isBeforeHookStep, + isDescendantOfStepWithTitle, + statusToAllureStats, +} from "./utils.js"; // TODO: move to utils.ts const diffEndRegexp = /-((expected)|(diff)|(actual))\.png$/; @@ -91,6 +101,8 @@ export class AllureReporter implements ReporterV2 { private readonly startedTestCasesTitlesCache: string[] = []; private readonly allureResultsUuids: Map = new Map(); private readonly attachmentSteps: Map = new Map(); + private beforeHooksStepsStack: Map = new Map(); + private afterHooksStepsStack: Map = new Map(); constructor(config: AllurePlaywrightReporterConfig) { this.options = { suiteTitle: true, detail: true, ...config }; @@ -281,6 +293,11 @@ export class AllureReporter implements ReporterV2 { return true; } + // playwright doesn't report this step + if (step.title === "Worker Cleanup" || isDescendantOfStepWithTitle(step, "Worker Cleanup")) { + return true; + } + return false; } @@ -297,10 +314,46 @@ export class AllureReporter implements ReporterV2 { return; } - this.allureRuntime!.startStep(testUuid, undefined, { + const baseStep: StepResult = { + ...createStepResult(), name: step.title, start: step.startTime.getTime(), - }); + stage: Stage.RUNNING, + }; + const isBeforeHookDescendant = isBeforeHookStep(step); + const isAfterHookDescendant = isAfterHookStep(step); + + if (isBeforeHookDescendant) { + const stack = this.beforeHooksStepsStack.get(test.id)!; + + stack.startStep(baseStep); + return; + } + + if (isAfterHookDescendant) { + const stack = this.afterHooksStepsStack.get(test.id)!; + + stack.startStep(baseStep); + return; + } + + if (step.title === BEFORE_HOOKS_ROOT_STEP_TITLE) { + const stack = new ShallowStepsStack(); + + stack.startStep(baseStep); + this.beforeHooksStepsStack.set(test.id, stack); + return; + } + + if (step.title === AFTER_HOOKS_ROOT_STEP_TITLE) { + const stack = new ShallowStepsStack(); + + stack.startStep(baseStep); + this.afterHooksStepsStack.set(test.id, stack); + return; + } + + this.allureRuntime!.startStep(testUuid, undefined, baseStep)!; } onStepEnd(test: TestCase, _result: PlaywrightTestResult, step: TestStep): void { @@ -314,6 +367,47 @@ export class AllureReporter implements ReporterV2 { } const testUuid = this.allureResultsUuids.get(test.id)!; + const isBeforeHookDescendant = isBeforeHookStep(step); + const isAfterHookDescendant = isAfterHookStep(step); + + if (isBeforeHookDescendant) { + const stack = this.beforeHooksStepsStack.get(test.id)!; + + stack.stopStep({ + stage: Stage.FINISHED, + }); + return; + } + + if (isAfterHookDescendant) { + const stack = this.afterHooksStepsStack.get(test.id)!; + + stack.stopStep({ + stage: Stage.FINISHED, + }); + return; + } + + if (step.title === BEFORE_HOOKS_ROOT_STEP_TITLE) { + const stack = this.beforeHooksStepsStack.get(test.id)!; + + this.allureRuntime?.updateTest(testUuid, (testResult) => { + testResult.steps.unshift(...stack.steps); + }); + this.beforeHooksStepsStack.delete(test.id); + return; + } + + if (step.title === AFTER_HOOKS_ROOT_STEP_TITLE) { + const stack = this.afterHooksStepsStack.get(test.id)!; + + this.allureRuntime?.updateTest(testUuid, (testResult) => { + testResult.steps.push(...stack.steps); + }); + this.afterHooksStepsStack.delete(test.id); + return; + } + const currentStep = this.allureRuntime!.currentStep(testUuid); if (!currentStep) { diff --git a/packages/allure-playwright/src/utils.ts b/packages/allure-playwright/src/utils.ts index 757271640..395617290 100644 --- a/packages/allure-playwright/src/utils.ts +++ b/packages/allure-playwright/src/utils.ts @@ -1,6 +1,11 @@ import type { TestStatus } from "@playwright/test"; +import type { TestStep } from "@playwright/test/reporter"; import { Status } from "allure-js-commons"; +export const AFTER_HOOKS_ROOT_STEP_TITLE = "After Hooks"; + +export const BEFORE_HOOKS_ROOT_STEP_TITLE = "Before Hooks"; + export const statusToAllureStats = (status: TestStatus, expectedStatus: TestStatus): Status => { if (status === "skipped") { return Status.SKIPPED; @@ -16,3 +21,21 @@ export const statusToAllureStats = (status: TestStatus, expectedStatus: TestStat return Status.FAILED; }; + +export const isDescendantOfStepWithTitle = (step: TestStep, title: string): boolean => { + let parent = step.parent; + + while (parent) { + if (parent.title === title) { + return true; + } + + parent = parent.parent; + } + + return false; +}; + +export const isAfterHookStep = (step: TestStep) => isDescendantOfStepWithTitle(step, AFTER_HOOKS_ROOT_STEP_TITLE); + +export const isBeforeHookStep = (step: TestStep) => isDescendantOfStepWithTitle(step, BEFORE_HOOKS_ROOT_STEP_TITLE); diff --git a/packages/allure-playwright/test/spec/hooks.spec.ts b/packages/allure-playwright/test/spec/hooks.spec.ts index eb8ff9dbe..efe582792 100644 --- a/packages/allure-playwright/test/spec/hooks.spec.ts +++ b/packages/allure-playwright/test/spec/hooks.spec.ts @@ -104,21 +104,77 @@ it("should mark step as failed when any child step is failed", async () => { name: "Before Hooks", status: Status.PASSED, }), + expect.objectContaining({ name: "page.waitForEvent", status: Status.FAILED, - steps: [ - expect.objectContaining({ - name: "After Hooks", - status: Status.FAILED, - }), - ], }), expect.objectContaining({ name: "Worker Cleanup", status: Status.PASSED, }), + expect.objectContaining({ + name: "After Hooks", + status: Status.FAILED, + }), ], }), ); }); + +it("keeps correct hooks structure when something failed", async () => { + const results = await runPlaywrightInlineTest({ + "sample.test.js": ` + import test from '@playwright/test'; + + test.beforeAll(async () => {}); + + test.beforeEach(async () => {}); + + test.afterAll(async () => {}); + + test.afterEach(async () => {}); + + test("should contain hooks", async ({ page }) => { + await test.step("step 1", async () => { + await page.waitForEvent("en_event"); + }); + }); + `, + "playwright.config.js": ` + import { defineConfig } from "@playwright/test"; + + export default { + reporter: [ + [ + "allure-playwright", + { + resultsDir: "./allure-results", + }, + ], + ["dot"], + ], + projects: [ + { + name: "project", + }, + ], + timeout: 1000, + use: { + screenshot: "on", + }, + }; + `, + }); + + expect(results.tests[0].steps).toHaveLength(3); + expect(results.tests[0].steps[0]).toMatchObject({ + name: "Before Hooks", + }); + expect(results.tests[0].steps[1]).toMatchObject({ + name: "step 1", + }); + expect(results.tests[0].steps[2]).toMatchObject({ + name: "After Hooks", + }); +}); From b5504aba709491d32cf8a0a7e01087d5281662c7 Mon Sep 17 00:00:00 2001 From: epszaw Date: Fri, 7 Feb 2025 16:43:04 +0100 Subject: [PATCH 3/3] add method to update shallow stack's steps change stop method signature complete new hooks reporting logic for pw fix failed pw tests --- .../src/sdk/reporter/ReporterRuntime.ts | 21 +++-- .../test/sdk/reporter/ReporterRuntime.spec.ts | 8 +- packages/allure-playwright/src/index.ts | 86 ++++++++++--------- .../allure-playwright/test/spec/hooks.spec.ts | 44 ++++------ 4 files changed, 82 insertions(+), 77 deletions(-) diff --git a/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts b/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts index d800dd71c..9cd001a16 100644 --- a/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts +++ b/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts @@ -86,22 +86,27 @@ export class ShallowStepsStack { this.#runningSteps.push(stepResult); } - updateStep(step?: Partial) { + updateStep(updateFunc: (result: StepResult) => void) { if (!this.#currentStep) { - throw new Error("there is no step to stop"); + // eslint-disable-next-line no-console + console.error("There is no running step in the stack to update!"); + return; } - Object.assign(this.#currentStep, step); + updateFunc(this.#currentStep); } - stopStep(step?: Partial) { + stopStep(opts?: { stop?: number; duration?: number }) { if (!this.#currentStep) { - throw new Error("there is no step to stop"); + // eslint-disable-next-line no-console + console.error("There is no running step in the stack to stop!"); + return; } - Object.assign(this.#currentStep, { - stage: Stage.FINISHED, - ...step, + const { stop, duration = 0 } = opts ?? {}; + + this.updateStep((result) => { + result.stop = stop ?? result.start ? result.start! + duration : undefined; }); this.#runningSteps.pop(); diff --git a/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts b/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts index 19b5fe6b5..249dba89f 100644 --- a/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts +++ b/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts @@ -794,13 +794,15 @@ describe("ReporterRuntime", () => { const stack = new ShallowStepsStack(); stack.startStep({ name: "step1" }); - stack.stopStep({ status: Status.PASSED }); + stack.updateStep((result) => { + result.status = Status.PASSED; + result.stage = Stage.FINISHED; + }); + stack.stopStep(); expect(stack.steps).toEqual([ expect.objectContaining({ name: "step1", - status: Status.PASSED, - stage: Stage.FINISHED, steps: [], }), ]); diff --git a/packages/allure-playwright/src/index.ts b/packages/allure-playwright/src/index.ts index 0d6fbcb07..f4c6d99b3 100644 --- a/packages/allure-playwright/src/index.ts +++ b/packages/allure-playwright/src/index.ts @@ -320,36 +320,31 @@ export class AllureReporter implements ReporterV2 { start: step.startTime.getTime(), stage: Stage.RUNNING, }; + const isRootBeforeHook = step.title === BEFORE_HOOKS_ROOT_STEP_TITLE; + const isRootAfterHook = step.title === AFTER_HOOKS_ROOT_STEP_TITLE; + const isRootHook = isRootBeforeHook || isRootAfterHook; const isBeforeHookDescendant = isBeforeHookStep(step); const isAfterHookDescendant = isAfterHookStep(step); - if (isBeforeHookDescendant) { - const stack = this.beforeHooksStepsStack.get(test.id)!; + if (isBeforeHookDescendant || isAfterHookDescendant) { + const stack = isBeforeHookDescendant + ? this.beforeHooksStepsStack.get(test.id)! + : this.afterHooksStepsStack.get(test.id)!; stack.startStep(baseStep); return; } - if (isAfterHookDescendant) { - const stack = this.afterHooksStepsStack.get(test.id)!; - - stack.startStep(baseStep); - return; - } - - if (step.title === BEFORE_HOOKS_ROOT_STEP_TITLE) { + if (isRootHook) { const stack = new ShallowStepsStack(); stack.startStep(baseStep); - this.beforeHooksStepsStack.set(test.id, stack); - return; - } - if (step.title === AFTER_HOOKS_ROOT_STEP_TITLE) { - const stack = new ShallowStepsStack(); - - stack.startStep(baseStep); - this.afterHooksStepsStack.set(test.id, stack); + if (isRootBeforeHook) { + this.beforeHooksStepsStack.set(test.id, stack); + } else { + this.afterHooksStepsStack.set(test.id, stack); + } return; } @@ -367,44 +362,53 @@ export class AllureReporter implements ReporterV2 { } const testUuid = this.allureResultsUuids.get(test.id)!; + const isRootBeforeHook = step.title === BEFORE_HOOKS_ROOT_STEP_TITLE; + const isRootAfterHook = step.title === AFTER_HOOKS_ROOT_STEP_TITLE; + const isRootHook = isRootBeforeHook || isRootAfterHook; const isBeforeHookDescendant = isBeforeHookStep(step); const isAfterHookDescendant = isAfterHookStep(step); + const isAfterHook = isRootAfterHook || isAfterHookDescendant; + const isHook = isRootBeforeHook || isRootAfterHook || isBeforeHookDescendant || isAfterHookDescendant; - if (isBeforeHookDescendant) { - const stack = this.beforeHooksStepsStack.get(test.id)!; + if (isHook) { + const stack = isAfterHook ? this.afterHooksStepsStack.get(test.id)! : this.beforeHooksStepsStack.get(test.id)!; - stack.stopStep({ - stage: Stage.FINISHED, - }); - return; - } + stack.updateStep((stepResult) => { + const { status = Status.PASSED } = getWorstTestStepResult(stepResult.steps) ?? {}; - if (isAfterHookDescendant) { - const stack = this.afterHooksStepsStack.get(test.id)!; + stepResult.status = step.error ? Status.FAILED : status; + stepResult.stage = Stage.FINISHED; + if (step.error) { + stepResult.statusDetails = { ...getMessageAndTraceFromError(step.error) }; + } + }); stack.stopStep({ - stage: Stage.FINISHED, + duration: step.duration, }); - return; } - if (step.title === BEFORE_HOOKS_ROOT_STEP_TITLE) { - const stack = this.beforeHooksStepsStack.get(test.id)!; + if (isRootHook) { + const stack = isRootAfterHook + ? this.afterHooksStepsStack.get(test.id)! + : this.beforeHooksStepsStack.get(test.id)!; this.allureRuntime?.updateTest(testUuid, (testResult) => { - testResult.steps.unshift(...stack.steps); + if (isRootAfterHook) { + testResult.steps.push(...stack.steps); + } else { + testResult.steps.unshift(...stack.steps); + } }); - this.beforeHooksStepsStack.delete(test.id); - return; - } - if (step.title === AFTER_HOOKS_ROOT_STEP_TITLE) { - const stack = this.afterHooksStepsStack.get(test.id)!; + if (isRootAfterHook) { + this.afterHooksStepsStack.delete(test.id); + } else { + this.beforeHooksStepsStack.delete(test.id); + } + } - this.allureRuntime?.updateTest(testUuid, (testResult) => { - testResult.steps.push(...stack.steps); - }); - this.afterHooksStepsStack.delete(test.id); + if (isHook) { return; } diff --git a/packages/allure-playwright/test/spec/hooks.spec.ts b/packages/allure-playwright/test/spec/hooks.spec.ts index efe582792..190f3dba9 100644 --- a/packages/allure-playwright/test/spec/hooks.spec.ts +++ b/packages/allure-playwright/test/spec/hooks.spec.ts @@ -17,6 +17,7 @@ it("handles before hooks", async () => { const [beforeHooks] = tests[0].steps; expect(beforeHooks).toMatchObject({ + name: "Before Hooks", steps: expect.arrayContaining([ expect.objectContaining({ name: "beforeAll hook", @@ -95,31 +96,24 @@ it("should mark step as failed when any child step is failed", async () => { `, }); - expect(results.tests[0]).toEqual( - expect.objectContaining({ - name: "should contain hooks", - status: Status.BROKEN, - steps: [ - expect.objectContaining({ - name: "Before Hooks", - status: Status.PASSED, - }), - - expect.objectContaining({ - name: "page.waitForEvent", - status: Status.FAILED, - }), - expect.objectContaining({ - name: "Worker Cleanup", - status: Status.PASSED, - }), - expect.objectContaining({ - name: "After Hooks", - status: Status.FAILED, - }), - ], - }), - ); + expect(results.tests[0]).toMatchObject({ + name: "should contain hooks", + status: Status.BROKEN, + steps: [ + expect.objectContaining({ + name: "Before Hooks", + status: Status.PASSED, + }), + expect.objectContaining({ + name: "page.waitForEvent", + status: Status.FAILED, + }), + expect.objectContaining({ + name: "After Hooks", + status: Status.PASSED, + }), + ], + }); }); it("keeps correct hooks structure when something failed", async () => {