diff --git a/.pnp.cjs b/.pnp.cjs index ba391071b..2f5f715ce 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -8875,7 +8875,7 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-n", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:17.10.1"],\ ["eslint-plugin-no-null", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:1.0.2"],\ ["eslint-plugin-prefer-arrow", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:1.2.3"],\ - ["glob", "npm:11.0.0"],\ + ["glob", "npm:11.0.1"],\ ["mocha", "npm:11.0.1"],\ ["npm-run-all2", "npm:7.0.0"],\ ["rimraf", "npm:6.0.0"],\ @@ -8912,7 +8912,7 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-n", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:17.10.1"],\ ["eslint-plugin-no-null", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:1.0.2"],\ ["eslint-plugin-prefer-arrow", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:1.2.3"],\ - ["glob", "npm:11.0.0"],\ + ["glob", "npm:11.0.1"],\ ["mocha", "npm:11.0.1"],\ ["npm-run-all2", "npm:7.0.0"],\ ["rimraf", "npm:6.0.0"],\ @@ -8950,6 +8950,7 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-n", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:17.10.1"],\ ["eslint-plugin-no-null", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:1.0.2"],\ ["eslint-plugin-prefer-arrow", "virtual:a59b12f7fe7bf3b80fc61d73eaaa33af60483f6ce31789d384fbe8ef169791f667d2559ec5f2fbae1a273a658ce021f1f5f1ea0718c56f81b30ad4e95a5668dd#npm:1.2.3"],\ + ["glob", "npm:11.0.1"],\ ["npm-run-all2", "npm:7.0.0"],\ ["rimraf", "npm:6.0.0"],\ ["typescript", "patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7"],\ @@ -13791,6 +13792,19 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ + ["npm:11.0.1", {\ + "packageLocation": "./.yarn/cache/glob-npm-11.0.1-2249503635-57b12a05cc.zip/node_modules/glob/",\ + "packageDependencies": [\ + ["glob", "npm:11.0.1"],\ + ["foreground-child", "npm:3.1.1"],\ + ["jackspeak", "npm:4.0.1"],\ + ["minimatch", "npm:10.0.1"],\ + ["minipass", "npm:7.1.2"],\ + ["package-json-from-dist", "npm:1.0.0"],\ + ["path-scurry", "npm:2.0.0"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:6.0.1", {\ "packageLocation": "./.yarn/cache/glob-npm-6.0.1-8e9c8956b1-a0670bc51f.zip/node_modules/glob/",\ "packageDependencies": [\ diff --git a/.yarn/cache/glob-npm-11.0.1-2249503635-57b12a05cc.zip b/.yarn/cache/glob-npm-11.0.1-2249503635-57b12a05cc.zip new file mode 100644 index 000000000..b38f7b609 Binary files /dev/null and b/.yarn/cache/glob-npm-11.0.1-2249503635-57b12a05cc.zip differ diff --git a/packages/allure-mocha/package.json b/packages/allure-mocha/package.json index f9f3bc691..aded2786c 100644 --- a/packages/allure-mocha/package.json +++ b/packages/allure-mocha/package.json @@ -86,7 +86,7 @@ "eslint-plugin-n": "^17.10.1", "eslint-plugin-no-null": "^1.0.2", "eslint-plugin-prefer-arrow": "^1.2.3", - "glob": "^11.0.0", + "glob": "^11.0.1", "mocha": "^11.0.0", "npm-run-all2": "^7.0.0", "rimraf": "^6.0.0", diff --git a/packages/allure-playwright/package.json b/packages/allure-playwright/package.json index 007ec45ac..d0f54b179 100644 --- a/packages/allure-playwright/package.json +++ b/packages/allure-playwright/package.json @@ -86,6 +86,7 @@ "eslint-plugin-n": "^17.10.1", "eslint-plugin-no-null": "^1.0.2", "eslint-plugin-prefer-arrow": "^1.2.3", + "glob": "^11.0.1", "npm-run-all2": "^7.0.0", "rimraf": "^6.0.0", "typescript": "^5.2.2", diff --git a/packages/allure-playwright/src/index.ts b/packages/allure-playwright/src/index.ts index 30fc211d2..3b44cc941 100644 --- a/packages/allure-playwright/src/index.ts +++ b/packages/allure-playwright/src/index.ts @@ -21,7 +21,7 @@ import { type StepResult, type TestResult, } from "allure-js-commons"; -import type { RuntimeMessage, TestPlanV1Test } from "allure-js-commons/sdk"; +import type { RuntimeMessage, RuntimeStepMetadataMessage, TestPlanV1Test } from "allure-js-commons/sdk"; import { extractMetadataFromString, getMessageAndTraceFromError, @@ -306,6 +306,7 @@ export class AllureReporter implements ReporterV2 { if (step.category === "attach") { const currentStep = this.allureRuntime?.currentStep(testUuid); + this.attachmentSteps.set(testUuid, [...(this.attachmentSteps.get(testUuid) ?? []), currentStep]); return; } @@ -562,6 +563,18 @@ export class AllureReporter implements ReporterV2 { return false; } + private processStepMetadataMessage(attachmentStepUuid: string, message: RuntimeStepMetadataMessage) { + const { name, parameters = [] } = message.data; + + this.allureRuntime!.updateStep(attachmentStepUuid, (step) => { + if (name) { + step.name = name; + } + + step.parameters.push(...parameters); + }); + } + private async processAttachment( testUuid: string, attachmentStepUuid: string | undefined, @@ -585,18 +598,24 @@ export class AllureReporter implements ReporterV2 { if (allureRuntimeMessage) { const message = JSON.parse(attachment.body!.toString()) as RuntimeMessage; - // TODO fix step metadata messages + if (message.type === "step_metadata") { + this.processStepMetadataMessage(attachmentStepUuid!, message); + return; + } + this.allureRuntime!.applyRuntimeMessages(testUuid, [message]); return; } const parentUuid = this.allureRuntime!.startStep(testUuid, attachmentStepUuid, { name: attachment.name }); + // only stop if step is created. Step may not be created only if test with specified uuid doesn't exists. // usually, missing test by uuid means we should completely skip result processing; // the later operations are safe and will only produce console warnings if (parentUuid) { this.allureRuntime!.stopStep(parentUuid, undefined); } + if (attachment.body) { this.allureRuntime!.writeAttachment(testUuid, parentUuid, attachment.name, attachment.body, { contentType: attachment.contentType, diff --git a/packages/allure-playwright/src/runtime.ts b/packages/allure-playwright/src/runtime.ts index 7612581ef..fd9452339 100644 --- a/packages/allure-playwright/src/runtime.ts +++ b/packages/allure-playwright/src/runtime.ts @@ -1,5 +1,5 @@ import { test } from "@playwright/test"; -import type { AttachmentOptions } from "allure-js-commons"; +import type { AttachmentOptions, ParameterMode } from "allure-js-commons"; import type { RuntimeMessage } from "allure-js-commons/sdk"; import { ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE } from "allure-js-commons/sdk/reporter"; import { MessageTestRuntime } from "allure-js-commons/sdk/runtime"; @@ -9,6 +9,40 @@ export class AllurePlaywrightTestRuntime extends MessageTestRuntime { super(); } + async step(stepName: string, body: () => any) { + return await test.step(stepName, async () => await body()); + } + + async stepDisplayName(name: string) { + await test.info().attach("Allure Step Metadata", { + contentType: ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE, + body: Buffer.from( + JSON.stringify({ + type: "step_metadata", + data: { + name, + }, + }), + "utf8", + ), + }); + } + + async stepParameter(name: string, value: string, mode?: ParameterMode) { + await test.info().attach("Allure Step Metadata", { + contentType: ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE, + body: Buffer.from( + JSON.stringify({ + type: "step_metadata", + data: { + parameters: [{ name, value, mode }], + }, + }), + "utf8", + ), + }); + } + async attachment(name: string, content: Buffer | string, options: AttachmentOptions) { await test.info().attach(name, { body: content, contentType: options.contentType }); } diff --git a/packages/allure-playwright/test/spec/runtime/legacy/steps.spec.ts b/packages/allure-playwright/test/spec/runtime/legacy/steps.spec.ts index 0f9b8128a..80be28391 100644 --- a/packages/allure-playwright/test/spec/runtime/legacy/steps.spec.ts +++ b/packages/allure-playwright/test/spec/runtime/legacy/steps.spec.ts @@ -88,16 +88,24 @@ it("handles nested lambda steps", async () => { it("should allow to set step metadata through its context", async () => { const { tests } = await runPlaywrightInlineTest({ "sample.test.ts": ` - import { test, allure } from "allure-playwright"; + import { allure, test } from "allure-playwright"; test("steps", async () => { - await allure.step("step 1", async () => { - await allure.step("step 2", async () => { - await allure.step("step 3", async (stepContext) => { - await stepContext.displayName("custom name"); - await stepContext.parameter("param", "value"); + await allure.step("step 1", async (ctx1) => { + await ctx1.displayName("custom name 1"); + + await allure.step("step 2", async (ctx2) => { + await ctx2.displayName("custom name 2"); + + await allure.step("step 3", async (ctx3) => { + await ctx3.displayName("custom name 3"); + await ctx3.parameter("param", "value 3"); }); + + await ctx2.parameter("param", "value 2"); }); + + await ctx1.parameter("param", "value 1"); }); }); `, @@ -109,24 +117,78 @@ it("should allow to set step metadata through its context", async () => { name: "Before Hooks", }); expect(tests[0].steps[1]).toMatchObject({ - name: "step 1", + name: "custom name 1", status: Status.PASSED, stage: Stage.FINISHED, + parameters: [expect.objectContaining({ name: "param", value: "value 1" })], }); expect(tests[0].steps[1].steps).toHaveLength(1); expect(tests[0].steps[1].steps[0]).toMatchObject({ - name: "step 2", + name: "custom name 2", status: Status.PASSED, stage: Stage.FINISHED, + parameters: [expect.objectContaining({ name: "param", value: "value 2" })], }); expect(tests[0].steps[1].steps[0].steps).toHaveLength(1); expect(tests[0].steps[1].steps[0].steps[0]).toMatchObject({ - name: "custom name", - parameters: [expect.objectContaining({ name: "param", value: "value" })], + name: "custom name 3", status: Status.PASSED, stage: Stage.FINISHED, + parameters: [expect.objectContaining({ name: "param", value: "value 3" })], }); expect(tests[0].steps[2]).toMatchObject({ name: "After Hooks", }); }); + +it("should use native playwright steps under the hood", async () => { + const { tests, restFiles } = await runPlaywrightInlineTest({ + "playwright.config.js": ` + module.exports = { + reporter: [ + [ + "allure-playwright", + { + resultsDir: "./allure-results", + }, + ], + ["dot"], + ["json", { outputFile: "./test-results.json" }], + ], + projects: [ + { + name: "project", + }, + ], + }; + `, + "sample.test.ts": ` + import { allure, test } from "allure-playwright"; + + test("steps", async () => { + await allure.step("step 1", async () => {}); + }); + `, + }); + + expect(tests).toHaveLength(1); + expect(tests[0].steps).toHaveLength(3); + expect(tests[0].steps[0]).toMatchObject({ + name: "Before Hooks", + }); + expect(tests[0].steps[1]).toMatchObject({ + name: "step 1", + status: Status.PASSED, + stage: Stage.FINISHED, + }); + expect(tests[0].steps[2]).toMatchObject({ + name: "After Hooks", + }); + expect(restFiles["test-results.json"]).toBeDefined(); + + const pwTestResults = JSON.parse(restFiles["test-results.json"]); + + expect(pwTestResults.suites[0].specs[0].tests[0].results[0].steps[0]).toMatchObject({ + title: "step 1", + }); +}); diff --git a/packages/allure-playwright/test/spec/runtime/modern/steps.spec.ts b/packages/allure-playwright/test/spec/runtime/modern/steps.spec.ts index 559e41db8..bce6bf1bf 100644 --- a/packages/allure-playwright/test/spec/runtime/modern/steps.spec.ts +++ b/packages/allure-playwright/test/spec/runtime/modern/steps.spec.ts @@ -28,7 +28,7 @@ it("handles single lambda step with attachment", async () => { const { tests, attachments } = await runPlaywrightInlineTest({ "sample.test.ts": ` import { test } from '@playwright/test'; - import { step, attachment } from "allure-js-commons"; + import { attachment, step } from "allure-js-commons"; test("steps", async () => { await step("step", async () => { @@ -115,16 +115,24 @@ it("should allow to set step metadata through its context", async () => { const { tests } = await runPlaywrightInlineTest({ "sample.test.ts": ` import { test } from "allure-playwright"; - import { step } from "allure-js-commons"; + import { step, attachment } from "allure-js-commons"; test("steps", async () => { - await step("step 1", async () => { - await step("step 2", async () => { - await step("step 3", async (stepContext) => { - await stepContext.displayName("custom name"); - await stepContext.parameter("param", "value"); + await step("step 1", async (ctx1) => { + await ctx1.displayName("custom name 1"); + + await step("step 2", async (ctx2) => { + await ctx2.displayName("custom name 2"); + + await step("step 3", async (ctx3) => { + await ctx3.displayName("custom name 3"); + await ctx3.parameter("param", "value 3"); }); + + await ctx2.parameter("param", "value 2"); }); + + await ctx1.parameter("param", "value 1"); }); }); `, @@ -136,24 +144,79 @@ it("should allow to set step metadata through its context", async () => { name: "Before Hooks", }); expect(tests[0].steps[1]).toMatchObject({ - name: "step 1", + name: "custom name 1", status: Status.PASSED, stage: Stage.FINISHED, + parameters: [expect.objectContaining({ name: "param", value: "value 1" })], }); expect(tests[0].steps[1].steps).toHaveLength(1); expect(tests[0].steps[1].steps[0]).toMatchObject({ - name: "step 2", + name: "custom name 2", status: Status.PASSED, stage: Stage.FINISHED, + parameters: [expect.objectContaining({ name: "param", value: "value 2" })], }); expect(tests[0].steps[1].steps[0].steps).toHaveLength(1); expect(tests[0].steps[1].steps[0].steps[0]).toMatchObject({ - name: "custom name", - parameters: [expect.objectContaining({ name: "param", value: "value" })], + name: "custom name 3", status: Status.PASSED, stage: Stage.FINISHED, + parameters: [expect.objectContaining({ name: "param", value: "value 3" })], }); expect(tests[0].steps[2]).toMatchObject({ name: "After Hooks", }); }); + +it("should use native playwright steps under the hood", async () => { + const { tests, restFiles } = await runPlaywrightInlineTest({ + "playwright.config.js": ` + module.exports = { + reporter: [ + [ + "allure-playwright", + { + resultsDir: "./allure-results", + }, + ], + ["dot"], + ["json", { outputFile: "./test-results.json" }], + ], + projects: [ + { + name: "project", + }, + ], + }; + `, + "sample.test.ts": ` + import { test } from "allure-playwright"; + import { step } from "allure-js-commons"; + + test("steps", async () => { + await step("step 1", async () => {}); + }); + `, + }); + + expect(tests).toHaveLength(1); + expect(tests[0].steps).toHaveLength(3); + expect(tests[0].steps[0]).toMatchObject({ + name: "Before Hooks", + }); + expect(tests[0].steps[1]).toMatchObject({ + name: "step 1", + status: Status.PASSED, + stage: Stage.FINISHED, + }); + expect(tests[0].steps[2]).toMatchObject({ + name: "After Hooks", + }); + expect(restFiles["test-results.json"]).toBeDefined(); + + const pwTestResults = JSON.parse(restFiles["test-results.json"]); + + expect(pwTestResults.suites[0].specs[0].tests[0].results[0].steps[0]).toMatchObject({ + title: "step 1", + }); +}); diff --git a/packages/allure-playwright/test/utils.ts b/packages/allure-playwright/test/utils.ts index 8981bd3dc..785bcf558 100644 --- a/packages/allure-playwright/test/utils.ts +++ b/packages/allure-playwright/test/utils.ts @@ -1,16 +1,19 @@ import { fork } from "child_process"; +import { glob } from "glob"; import { randomUUID } from "node:crypto"; -import { mkdir, rm, writeFile } from "node:fs/promises"; -import { dirname, extname, join } from "node:path"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, extname, join, relative } from "node:path"; import { attachment, logStep, step } from "allure-js-commons"; import type { AllureResults } from "allure-js-commons/sdk"; import { MessageReader } from "allure-js-commons/sdk/reporter"; +type AllurePlaywrightTestResults = AllureResults & { restFiles: Record }; + export const runPlaywrightInlineTest = async ( files: Record, cliArgs: string[] = [], env?: Record, -): Promise => { +): Promise => { const testFiles = { "playwright.config.js": ` module.exports = { @@ -32,19 +35,24 @@ export const runPlaywrightInlineTest = async ( `, ...files, }; + const testFilesNames = Object.keys(testFiles); const testDir = join(__dirname, "fixtures", randomUUID()); await step(`create test dir ${testDir}`, async () => { await mkdir(testDir, { recursive: true }); }); - for (const file of Object.keys(testFiles)) { + for (const file of testFilesNames) { await step(file, async () => { const filePath = join(testDir, file); await mkdir(dirname(filePath), { recursive: true }); const content = testFiles[file as keyof typeof testFiles]; await writeFile(filePath, content, "utf8"); - await attachment(file, content, { contentType: "text/plain", fileExtension: extname(file), encoding: "utf-8" }); + await attachment(file, content, { + contentType: "text/plain", + fileExtension: extname(file), + encoding: "utf-8", + }); }); } @@ -79,7 +87,14 @@ export const runPlaywrightInlineTest = async ( return new Promise((resolve) => { testProcess.on("exit", async (code, signal) => { - await rm(testDir, { recursive: true }); + const resultsFiles = ( + await glob(join(testDir, "**/*"), { + nodir: true, + windowsPathsNoEscape: true, + }) + ) + .map((filename) => relative(testDir, filename)) + .filter((filename) => !testFilesNames.includes(filename)); if (signal) { await logStep(`Interrupted with ${signal}`); @@ -90,9 +105,20 @@ export const runPlaywrightInlineTest = async ( await attachment("stdout", stdout.join("\n"), "text/plain"); await attachment("stderr", stderr.join("\n"), "text/plain"); - await messageReader.attachResults(); - return resolve(messageReader.results); + + const result: AllurePlaywrightTestResults = { + ...messageReader.results, + restFiles: {}, + }; + + for (const file of resultsFiles) { + result.restFiles[file] = await readFile(join(testDir, file), "utf-8"); + } + + await rm(testDir, { recursive: true }); + + return resolve(result); }); }); }; diff --git a/yarn.lock b/yarn.lock index b2bebf47a..c57209835 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5799,7 +5799,7 @@ __metadata: eslint-plugin-n: "npm:^17.10.1" eslint-plugin-no-null: "npm:^1.0.2" eslint-plugin-prefer-arrow: "npm:^1.2.3" - glob: "npm:^11.0.0" + glob: "npm:^11.0.1" mocha: "npm:^11.0.0" npm-run-all2: "npm:^7.0.0" rimraf: "npm:^6.0.0" @@ -5837,6 +5837,7 @@ __metadata: eslint-plugin-n: "npm:^17.10.1" eslint-plugin-no-null: "npm:^1.0.2" eslint-plugin-prefer-arrow: "npm:^1.2.3" + glob: "npm:^11.0.1" npm-run-all2: "npm:^7.0.0" rimraf: "npm:^6.0.0" typescript: "npm:^5.2.2" @@ -9939,6 +9940,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:^11.0.1": + version: 11.0.1 + resolution: "glob@npm:11.0.1" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^4.0.1" + minimatch: "npm:^10.0.0" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^2.0.0" + bin: + glob: dist/esm/bin.mjs + checksum: 10/57b12a05cc25f1c38f3b24cf6ea7a8bacef11e782c4b9a8c5b0bef3e6c5bcb8c4548cb31eb4115592e0490a024c1bde7359c470565608dd061d3b21179740457 + languageName: node + linkType: hard + "glob@npm:^6.0.1": version: 6.0.4 resolution: "glob@npm:6.0.4"