From 72f85e1277cac7d0eb5d03938a6a4f657429829b Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 1 Nov 2024 17:17:28 +0700 Subject: [PATCH] feat(codeceptjs): add support for mocha reporters (fixes #1167, via #1184) --- packages/allure-codeceptjs/src/index.ts | 11 +- .../test/spec/labels.test.ts | 9 +- .../test/spec/reporters.test.ts | 149 ++++++++++++++++++ .../test/spec/testplan.test.ts | 2 +- packages/allure-codeceptjs/test/utils.ts | 34 +++- packages/allure-codeceptjs/vitest.config.ts | 16 +- 6 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 packages/allure-codeceptjs/test/spec/reporters.test.ts diff --git a/packages/allure-codeceptjs/src/index.ts b/packages/allure-codeceptjs/src/index.ts index 1d8ac1bb2..83daf251f 100644 --- a/packages/allure-codeceptjs/src/index.ts +++ b/packages/allure-codeceptjs/src/index.ts @@ -6,7 +6,16 @@ import { AllureCodeceptJsReporter } from "./reporter.js"; const allurePlugin = (config: ReporterConfig) => { const mocha = container.mocha(); - mocha.reporter(AllureCodeceptJsReporter.prototype.constructor, { ...config }); + + // At this point the configured reporter's constructor has been initialized and is available via the _reporter field. + // See https://github.com/mochajs/mocha/blob/05097db4f2e0118f033978b8503aec36b1867c55/lib/mocha.js#L352 + // The field is not public but there is no other option to get the constructor; this is covered by tests in reporters.test.ts. + // eslint-disable-next-line no-underscore-dangle + const currentReporter = mocha._reporter; + mocha.reporter(AllureCodeceptJsReporter.prototype.constructor, { + ...config, + ...(currentReporter ? { extraReporters: [currentReporter, mocha.options.reporterOptions] } : {}), + }); return { ...allureCodeceptJsLegacyApi, diff --git a/packages/allure-codeceptjs/test/spec/labels.test.ts b/packages/allure-codeceptjs/test/spec/labels.test.ts index 2bd0047c4..236cb10dd 100644 --- a/packages/allure-codeceptjs/test/spec/labels.test.ts +++ b/packages/allure-codeceptjs/test/spec/labels.test.ts @@ -102,8 +102,7 @@ it("should not depend on CWD", async () => { }); `, }, - {}, - "nested", + { cwd: "nested" }, ); expect(tests).toEqual( @@ -141,8 +140,10 @@ it("should add labels from env variables", async () => { `, }, { - ALLURE_LABEL_A: "a", - ALLURE_LABEL_B: "b", + env: { + ALLURE_LABEL_A: "a", + ALLURE_LABEL_B: "b", + }, }, ); diff --git a/packages/allure-codeceptjs/test/spec/reporters.test.ts b/packages/allure-codeceptjs/test/spec/reporters.test.ts new file mode 100644 index 000000000..40f8ea218 --- /dev/null +++ b/packages/allure-codeceptjs/test/spec/reporters.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from "vitest"; +import { issue } from "allure-js-commons"; +import { runCodeceptJsInlineTest } from "../utils.js"; + +describe("mocha reporters", () => { + it("cli should be enabled by default", async () => { + const { tests, stdout } = await runCodeceptJsInlineTest({ + "foo.test.js": ` + Feature("foo") + Scenario('bar', () => {}); + `, + }); + + expect(tests).toEqual([expect.objectContaining({ name: "bar" })]); + expect(stdout.join("").split("\n")).toEqual(expect.arrayContaining([expect.stringMatching(/^foo --/)])); + }); + + it("cli --steps should work out-of-box", async () => { + await issue("1167"); + + const { tests, stdout } = await runCodeceptJsInlineTest( + { + "foo.test.js": ` + Feature("foo") + Scenario('bar', () => {}); + `, + }, + { args: ["--steps"] }, + ); + + expect(tests).toEqual([expect.objectContaining({ name: "bar" })]); + expect(stdout.join("").split("\n")).toEqual( + expect.arrayContaining([expect.stringMatching(/^foo --/), expect.stringMatching(/^ {2}bar/)]), + ); + }); + + it("should support Mocha's built-in reporters", async () => { + const { tests, stdout } = await runCodeceptJsInlineTest({ + "foo.test.js": ` + Feature("foo") + Scenario('bar', () => {}); + `, + "codecept.conf.js": ` + const path = require("node:path"); + exports.config = { + tests: "./**/*.test.js", + output: path.resolve(__dirname, "./output"), + plugins: { + allure: { + require: require.resolve("allure-codeceptjs"), + enabled: true, + }, + }, + mocha: { + reporter: "json", + }, + }; + `, + }); + + const stdoutLines = stdout.join("").split("\n"); + + expect(tests).toEqual([expect.objectContaining({ name: "bar" })]); + const jsonString = stdoutLines.slice(stdoutLines.indexOf("{")).join(""); + expect(JSON.parse(jsonString)).toMatchObject({ + stats: expect.objectContaining({ + suites: 1, + tests: 1, + }), + }); + expect(stdoutLines).not.toEqual( + // no default cli reporter + expect.arrayContaining([expect.stringMatching(/^foo --/)]), + ); + }); + + it("should support local reporters", async () => { + const { tests, stdout } = await runCodeceptJsInlineTest({ + "foo.test.js": ` + Feature("foo") + Scenario('bar', () => {}); + `, + "reporter.cjs": ` + module.exports = function (r, o) { + r.on("start", () => { + console.log(JSON.stringify(o.reporterOptions)); + }); + }; + `, + "codecept.conf.js": ` + const path = require("node:path"); + exports.config = { + tests: "./**/*.test.js", + output: path.resolve(__dirname, "./output"), + plugins: { + allure: { + require: require.resolve("allure-codeceptjs"), + enabled: true, + }, + }, + mocha: { + reporter: "reporter.cjs", + reporterOptions: { foo: "bar" }, + }, + }; + `, + }); + + const stdoutLines = stdout.join("").split("\n"); + + expect(tests).toEqual([expect.objectContaining({ name: "bar" })]); + expect(stdoutLines).toEqual(expect.arrayContaining([String.raw`{"foo":"bar"}`])); + }); + + it("should support reporter constructors", async () => { + const { tests, stdout } = await runCodeceptJsInlineTest({ + "foo.test.js": ` + Feature("foo") + Scenario('bar', () => {}); + `, + "codecept.conf.js": ` + const path = require("node:path"); + exports.config = { + tests: "./**/*.test.js", + output: path.resolve(__dirname, "./output"), + plugins: { + allure: { + require: require.resolve("allure-codeceptjs"), + enabled: true, + }, + }, + mocha: { + reporter: function (r, o) { + r.on("start", () => { + console.log(JSON.stringify(o.reporterOptions)); + }); + }, + reporterOptions: { foo: { bar: "baz" } }, + }, + }; + `, + }); + + const stdoutLines = stdout.join("").split("\n"); + + expect(tests).toEqual([expect.objectContaining({ name: "bar" })]); + expect(stdoutLines).toEqual(expect.arrayContaining([String.raw`{"foo":{"bar":"baz"}}`])); + }); +}); diff --git a/packages/allure-codeceptjs/test/spec/testplan.test.ts b/packages/allure-codeceptjs/test/spec/testplan.test.ts index 11bf1df96..5263e92d0 100644 --- a/packages/allure-codeceptjs/test/spec/testplan.test.ts +++ b/packages/allure-codeceptjs/test/spec/testplan.test.ts @@ -37,7 +37,7 @@ it("should support test plan", async () => { [testPlanFilename]: `${JSON.stringify(exampleTestPlan)}`, }, { - ALLURE_TESTPLAN_PATH: `./${testPlanFilename}`, + env: { ALLURE_TESTPLAN_PATH: `./${testPlanFilename}` }, }, ); diff --git a/packages/allure-codeceptjs/test/utils.ts b/packages/allure-codeceptjs/test/utils.ts index 0460baf89..eb5a17c49 100644 --- a/packages/allure-codeceptjs/test/utils.ts +++ b/packages/allure-codeceptjs/test/utils.ts @@ -7,11 +7,21 @@ import { attachment, step } from "allure-js-commons"; import type { AllureResults } from "allure-js-commons/sdk"; import { MessageReader } from "allure-js-commons/sdk/reporter"; +type RunOptions = { + env?: Record; + cwd?: string; + args?: string[]; +}; + +type RunResult = AllureResults & { + stdout: string[]; + stderr: string[]; +}; + export const runCodeceptJsInlineTest = async ( files: Record, - env?: Record, - cwd?: string, -): Promise => { + { env, cwd, args: extraCliArgs = [] }: RunOptions = {}, +): Promise => { const testFiles = { // package.json is used to find project root in case of absolute file paths are used // eslint-disable-next-line @stylistic/quotes @@ -40,7 +50,7 @@ export const runCodeceptJsInlineTest = async ( const modulePath = await step("resolve codeceptjs", () => { return require.resolve("codeceptjs/bin/codecept.js"); }); - const args = ["run", "-c", testDir]; + const args = ["run", "-c", testDir, ...extraCliArgs]; const testProcess = await step(`${modulePath} ${args.join(" ")}`, () => { return fork(modulePath, args, { env: { @@ -53,23 +63,31 @@ export const runCodeceptJsInlineTest = async ( }); }); + const stdout: string[] = []; + const stderr: string[] = []; + testProcess.stdout?.setEncoding("utf8").on("data", (chunk) => { - process.stdout.write(String(chunk)); + stdout.push(String(chunk)); }); testProcess.stderr?.setEncoding("utf8").on("data", (chunk) => { - process.stderr.write(String(chunk)); + stderr.push(String(chunk)); }); const messageReader = new MessageReader(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument testProcess.on("message", messageReader.handleMessage); return new Promise((resolve) => { testProcess.on("exit", async () => { await messageReader.attachResults(); + if (stdout.length) { + await attachment("stdout", stdout.join("\n"), { contentType: "text/plain" }); + } + if (stderr.length) { + await attachment("stderr", stderr.join("\n"), { contentType: "text/plain" }); + } await rm(testDir, { recursive: true }); - return resolve(messageReader.results); + return resolve({ ...messageReader.results, stdout, stderr }); }); }); }; diff --git a/packages/allure-codeceptjs/vitest.config.ts b/packages/allure-codeceptjs/vitest.config.ts index 4c2f78527..f836907e2 100644 --- a/packages/allure-codeceptjs/vitest.config.ts +++ b/packages/allure-codeceptjs/vitest.config.ts @@ -6,7 +6,21 @@ export default defineConfig({ fileParallelism: false, testTimeout: 5000, setupFiles: ["./vitest-setup.ts"], - reporters: ["default", ["allure-vitest/reporter", { resultsDir: "./out/allure-results" }]], + reporters: [ + "default", + [ + "allure-vitest/reporter", + { + resultsDir: "./out/allure-results", + links: { + issue: { + urlTemplate: "https://github.com/allure-framework/allure-js/issues/%s", + nameTemplate: "Issue %s", + }, + }, + }, + ], + ], typecheck: { enabled: true, tsconfig: "./test/tsconfig.json",