diff --git a/packages/allure-mocha/package.json b/packages/allure-mocha/package.json index 1bf5cb236..b1b44e15f 100644 --- a/packages/allure-mocha/package.json +++ b/packages/allure-mocha/package.json @@ -46,7 +46,7 @@ "compile:fixup": "node ./scripts/fixup.mjs", "lint": "eslint ./src ./test --ext .ts", "lint:fix": "eslint ./src ./test --ext .ts --fix", - "test": "run-s 'test:*'", + "test": "run-s --print-name 'test:*'", "test:serial": "vitest run", "test:parallel": "ALLURE_MOCHA_TEST_PARALLEL=true vitest run", "test:runner": "ALLURE_MOCHA_TEST_RUNNER=cjs ALLURE_MOCHA_TEST_SPEC_FORMAT=cjs vitest run", diff --git a/packages/allure-mocha/src/AllureMochaReporter.ts b/packages/allure-mocha/src/AllureMochaReporter.ts index 45a21de87..2b47ac4d0 100644 --- a/packages/allure-mocha/src/AllureMochaReporter.ts +++ b/packages/allure-mocha/src/AllureMochaReporter.ts @@ -14,7 +14,19 @@ import { import { setGlobalTestRuntime } from "allure-js-commons/sdk/runtime"; import { MochaTestRuntime } from "./MochaTestRuntime.js"; import { setLegacyApiRuntime } from "./legacyUtils.js"; -import { getInitialLabels, getSuitesOfMochaTest, resolveParallelModeSetupFile } from "./utils.js"; +import { + applyTestPlan, + createTestPlanIndices, + getAllureDisplayName, + getAllureFullName, + getAllureMetaLabels, + getInitialLabels, + getSuitesOfMochaTest, + getTestCaseId, + isIncludedInTestRun, + resolveParallelModeSetupFile, +} from "./utils.js"; +import type { TestPlanIndices } from "./utils.js"; const { EVENT_SUITE_BEGIN, @@ -30,6 +42,7 @@ const { export class AllureMochaReporter extends Mocha.reporters.Base { private readonly runtime: ReporterRuntime; + private readonly testplan?: TestPlanIndices; constructor(runner: Mocha.Runner, opts: Mocha.MochaOptions) { super(runner, opts); @@ -40,6 +53,8 @@ export class AllureMochaReporter extends Mocha.reporters.Base { writer: writer || new FileSystemWriter({ resultsDir }), ...restOptions, }); + this.testplan = createTestPlanIndices(); + const testRuntime = new MochaTestRuntime(this.runtime); setGlobalTestRuntime(testRuntime); @@ -52,6 +67,12 @@ export class AllureMochaReporter extends Mocha.reporters.Base { } } + override done(failures: number, fn?: ((failures: number) => void) | undefined): void { + this.runtime.writeEnvironmentInfo(); + this.runtime.writeCategoriesDefinitions(); + return fn?.(failures); + } + private applyListeners = () => { this.runner .on(EVENT_SUITE_BEGIN, this.onSuite) @@ -65,7 +86,10 @@ export class AllureMochaReporter extends Mocha.reporters.Base { .on(EVENT_HOOK_END, this.onHookEnd); }; - private onSuite = () => { + private onSuite = (suite: Mocha.Suite) => { + if (!suite.parent && this.testplan) { + applyTestPlan(this.testplan.idIndex, this.testplan.fullNameIndex, suite); + } this.runtime.startScope(); }; @@ -74,26 +98,24 @@ export class AllureMochaReporter extends Mocha.reporters.Base { }; private onTest = (test: Mocha.Test) => { - let fullName = ""; const globalLabels = getEnvironmentLabels().filter((label) => !!label.value); const initialLabels: Label[] = getInitialLabels(); - const labels = globalLabels.concat(initialLabels); + const metaLabels = getAllureMetaLabels(test); + const labels = globalLabels.concat(initialLabels, metaLabels); if (test.file) { const testPath = getRelativePath(test.file); - fullName = `${testPath!}: `; const packageLabelFromPath: Label = getPackageLabelFromPath(testPath); labels.push(packageLabelFromPath); } - fullName += test.titlePath().join(" > "); - this.runtime.startTest( { - name: test.title, + name: getAllureDisplayName(test), stage: Stage.RUNNING, - fullName, + fullName: getAllureFullName(test), labels, + testCaseId: getTestCaseId(test), }, { dedicatedScope: true }, ); @@ -116,23 +138,27 @@ export class AllureMochaReporter extends Mocha.reporters.Base { }; private onPending = (test: Mocha.Test) => { - this.onTest(test); - this.runtime.updateTest((r) => { - r.status = Status.SKIPPED; - r.statusDetails = { - message: "Test skipped", - }; - }); + if (isIncludedInTestRun(test)) { + this.onTest(test); + this.runtime.updateTest((r) => { + r.status = Status.SKIPPED; + r.statusDetails = { + message: "Test skipped", + }; + }); + } }; private onTestEnd = (test: Mocha.Test) => { - const defaultSuites = getSuitesOfMochaTest(test); - this.runtime.updateTest((t) => { - ensureSuiteLabels(t, defaultSuites); - t.stage = Stage.FINISHED; - }); - this.runtime.stopTest(); - this.runtime.writeTest(); + if (isIncludedInTestRun(test)) { + const defaultSuites = getSuitesOfMochaTest(test); + this.runtime.updateTest((t) => { + ensureSuiteLabels(t, defaultSuites); + t.stage = Stage.FINISHED; + }); + this.runtime.stopTest(); + this.runtime.writeTest(); + } }; private onHookStart = (hook: Mocha.Hook) => { diff --git a/packages/allure-mocha/src/utils.ts b/packages/allure-mocha/src/utils.ts index 6b480e04f..9082b5812 100644 --- a/packages/allure-mocha/src/utils.ts +++ b/packages/allure-mocha/src/utils.ts @@ -1,36 +1,110 @@ import type * as Mocha from "mocha"; -import { hostname } from "node:os"; import { dirname, extname, join } from "node:path"; -import { env, pid } from "node:process"; +import { env } from "node:process"; import { fileURLToPath } from "node:url"; -import { isMainThread, threadId } from "node:worker_threads"; import type { Label } from "allure-js-commons"; import { LabelName } from "allure-js-commons"; +import type { TestPlanV1, TestPlanV1Test } from "allure-js-commons/sdk"; +import { extractMetadataFromString } from "allure-js-commons/sdk"; +import { getHostLabel, getRelativePath, getThreadLabel, md5, parseTestPlan } from "allure-js-commons/sdk/reporter"; const filename = fileURLToPath(import.meta.url); -export const getSuitesOfMochaTest = (test: Mocha.Test) => test.titlePath().slice(0, -1); +const allureMochaDataKey = Symbol("Used to access Allure extra data in Mocha objects"); -export const resolveParallelModeSetupFile = () => - join(dirname(filename), `setupAllureMochaParallel${extname(filename)}`); +type AllureMochaTestData = { + isIncludedInTestRun: boolean; + fullName: string; + labels: readonly Label[]; + displayName: string; +}; + +const getAllureData = (item: Mocha.Test): AllureMochaTestData => { + const data = (item as any)[allureMochaDataKey]; + if (!data) { + const meta = extractMetadataFromString(item.title); + const defaultData: AllureMochaTestData = { + isIncludedInTestRun: true, + fullName: createAllureFullName(item), + labels: meta.labels, + displayName: meta.cleanTitle, + }; + (item as any)[allureMochaDataKey] = defaultData; + return defaultData; + } + return data; +}; + +const createAllureFullName = (test: Mocha.Test) => { + const titlePath = test.titlePath().join(" > "); + return test.file ? `${getRelativePath(test.file)}: ${titlePath}` : titlePath; +}; + +const createTestPlanSelectorIndex = (testplan: TestPlanV1) => createTestPlanIndex((e) => e.selector, testplan); + +const createTestPlanIdIndex = (testplan: TestPlanV1) => createTestPlanIndex((e) => e.id?.toString(), testplan); + +const createTestPlanIndex = (keySelector: (entry: TestPlanV1Test) => T, testplan: TestPlanV1) => + new Set(testplan.tests.map((e) => keySelector(e)).filter((v) => v) as readonly T[]); + +export type TestPlanIndices = { + fullNameIndex: ReadonlySet; + idIndex: ReadonlySet; +}; -export const resolveMochaWorkerId = () => env.MOCHA_WORKER_ID ?? (isMainThread ? pid : threadId).toString(); +export const createTestPlanIndices = (): TestPlanIndices | undefined => { + const testplan = parseTestPlan(); + if (testplan) { + return { + fullNameIndex: createTestPlanSelectorIndex(testplan), + idIndex: createTestPlanIdIndex(testplan), + }; + } +}; -const allureHostName = env.ALLURE_HOST_NAME || hostname(); +export const getAllureFullName = (test: Mocha.Test) => getAllureData(test).fullName; -export const getHostLabel = (): Label => ({ - name: LabelName.HOST, - value: allureHostName, -}); +export const isIncludedInTestRun = (test: Mocha.Test) => getAllureData(test).isIncludedInTestRun; -export const getWorkerIdLabel = (): Label => ({ - name: LabelName.THREAD, - value: resolveMochaWorkerId(), -}); +export const getAllureMetaLabels = (test: Mocha.Test) => getAllureData(test).labels; + +export const getAllureId = (data: AllureMochaTestData) => { + const values = data.labels.filter((l) => l.name === LabelName.ALLURE_ID).map((l) => l.value); + if (values.length) { + return values[0]; + } +}; + +export const getAllureDisplayName = (test: Mocha.Test) => getAllureData(test).displayName; + +export const getSuitesOfMochaTest = (test: Mocha.Test) => test.titlePath().slice(0, -1); + +export const resolveParallelModeSetupFile = () => + join(dirname(filename), `setupAllureMochaParallel${extname(filename)}`); export const getInitialLabels = (): Label[] => [ { name: LabelName.LANGUAGE, value: "javascript" }, { name: LabelName.FRAMEWORK, value: "mocha" }, getHostLabel(), - getWorkerIdLabel(), + getThreadLabel(env.MOCHA_WORKER_ID), ]; + +export const getTestCaseId = (test: Mocha.Test) => { + const suiteTitles = test.titlePath().slice(0, -1); + return md5(JSON.stringify([...suiteTitles, getAllureDisplayName(test)])); +}; + +export const applyTestPlan = (ids: ReadonlySet, selectors: ReadonlySet, rootSuite: Mocha.Suite) => { + const suiteQueue = []; + for (let s: Mocha.Suite | undefined = rootSuite; s; s = suiteQueue.shift()) { + for (const test of s.tests) { + const allureData = getAllureData(test); + const allureId = getAllureId(allureData); + if (!selectors.has(allureData.fullName) && (!allureId || !ids.has(allureId))) { + allureData.isIncludedInTestRun = false; + test.pending = true; + } + } + suiteQueue.push(...s.suites); + } +}; diff --git a/packages/allure-mocha/test/fixtures/reporter.cjs b/packages/allure-mocha/test/fixtures/reporter.cjs index 3dd8d6aa3..e3a5d1975 100644 --- a/packages/allure-mocha/test/fixtures/reporter.cjs +++ b/packages/allure-mocha/test/fixtures/reporter.cjs @@ -1,13 +1,15 @@ /* eslint @typescript-eslint/no-unsafe-argument: 0 */ const AllureMochaReporter = require("allure-mocha"); -const path = require("path"); class ProcessMessageAllureReporter extends AllureMochaReporter { constructor(runner, opts) { if (opts.reporterOptions?.emitFiles !== "true") { - opts.reporterOptions = { - writer: opts.parallel ? path.join(__dirname, "./AllureMochaParallelWriter.cjs") : "MessageWriter", - }; + (opts.reporterOptions ??= {}).writer = "MessageWriter"; + } + for (const key of ["environmentInfo", "categories"]) { + if (typeof opts.reporterOptions?.[key] === "string") { + opts.reporterOptions[key] = JSON.parse(Buffer.from(opts.reporterOptions[key], "base64Url").toString()); + } } super(runner, opts); } diff --git a/packages/allure-mocha/test/fixtures/runner.js b/packages/allure-mocha/test/fixtures/runner.js index f2d3cae88..38c9510b5 100644 --- a/packages/allure-mocha/test/fixtures/runner.js +++ b/packages/allure-mocha/test/fixtures/runner.js @@ -14,11 +14,12 @@ let emitFiles = false; let parallel = false; const requires = []; +const reporterOptions = {}; const sepIndex = process.argv.indexOf("--"); const args = sepIndex === -1 ? [] : process.argv.splice(sepIndex); -for (const arg of args) { - switch (arg) { +for (let i = 0; i < args.length; i++) { + switch (args[i]) { case "--emit-files": emitFiles = true; break; @@ -28,14 +29,22 @@ for (const arg of args) { // esm: await import("./setupParallel.cjs"); requires.push(path.join(dirname, "setupParallel.cjs")); break; + case "--environment-info": + reporterOptions.environmentInfo = JSON.parse(Buffer.from(args[++i], "base64url").toString()); + break; + case "--categories": + reporterOptions.categories = JSON.parse(Buffer.from(args[++i], "base64url").toString()); + break; } } +if (!emitFiles) { + reporterOptions.writer = "MessageWriter"; +} + const mocha = new Mocha({ reporter: AllureReporter, - reporterOptions: { - writer: emitFiles ? undefined : "MessageWriter", - }, + reporterOptions, parallel, require: requires, color: false, diff --git a/packages/allure-mocha/test/fixtures/samples/labels/meta/change.spec.js b/packages/allure-mocha/test/fixtures/samples/labels/meta/change.spec.js new file mode 100644 index 000000000..59943f711 --- /dev/null +++ b/packages/allure-mocha/test/fixtures/samples/labels/meta/change.spec.js @@ -0,0 +1,6 @@ +// cjs: const { it } = require("mocha"); +// esm: import { it } from "mocha"; + +it("a test a changing meta @allure.label.foo:bar", async () => {}); + +it("a test a changing meta @allure.label.foo:baz", async () => {}); diff --git a/packages/allure-mocha/test/fixtures/samples/labels/meta/id/1004.spec.js b/packages/allure-mocha/test/fixtures/samples/labels/meta/id/1004.spec.js new file mode 100644 index 000000000..dbcd7ca38 --- /dev/null +++ b/packages/allure-mocha/test/fixtures/samples/labels/meta/id/1004.spec.js @@ -0,0 +1,4 @@ +// cjs: const { it } = require("mocha"); +// esm: import { it } from "mocha"; + +it("a test with ID 1004 @allure.id:1004", async () => {}); diff --git a/packages/allure-mocha/test/fixtures/samples/labels/meta/id/1005.spec.js b/packages/allure-mocha/test/fixtures/samples/labels/meta/id/1005.spec.js new file mode 100644 index 000000000..0d38bf36f --- /dev/null +++ b/packages/allure-mocha/test/fixtures/samples/labels/meta/id/1005.spec.js @@ -0,0 +1,4 @@ +// cjs: const { it } = require("mocha"); +// esm: import { it } from "mocha"; + +it("a test with ID 1005 @allure.id:1005", async () => {}); diff --git a/packages/allure-mocha/test/fixtures/samples/labels/meta/multiple.spec.js b/packages/allure-mocha/test/fixtures/samples/labels/meta/multiple.spec.js new file mode 100644 index 000000000..976fe35c0 --- /dev/null +++ b/packages/allure-mocha/test/fixtures/samples/labels/meta/multiple.spec.js @@ -0,0 +1,4 @@ +// cjs: const { it } = require("mocha"); +// esm: import { it } from "mocha"; + +it("a test two embedded custom label @allure.label.foo:bar @allure.label.baz:qux", async () => {}); diff --git a/packages/allure-mocha/test/fixtures/samples/labels/meta/single.spec.js b/packages/allure-mocha/test/fixtures/samples/labels/meta/single.spec.js new file mode 100644 index 000000000..6a8879938 --- /dev/null +++ b/packages/allure-mocha/test/fixtures/samples/labels/meta/single.spec.js @@ -0,0 +1,4 @@ +// cjs: const { it } = require("mocha"); +// esm: import { it } from "mocha"; + +it("a test with an embedded custom label @allure.label.foo:bar", async () => {}); diff --git a/packages/allure-mocha/test/spec/api/metadata.test.ts b/packages/allure-mocha/test/spec/api/metadata.test.ts new file mode 100644 index 000000000..c131419af --- /dev/null +++ b/packages/allure-mocha/test/spec/api/metadata.test.ts @@ -0,0 +1,71 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import type { TestResult } from "allure-js-commons"; +import { runMochaInlineTest } from "../../utils.js"; + +describe("embedded metadata", () => { + let tests: TestResult[]; + beforeAll(async () => { + ({ tests } = await runMochaInlineTest( + ["labels", "meta", "id", "1004"], + ["labels", "meta", "single"], + ["labels", "meta", "multiple"], + ["labels", "meta", "change"], + )); + }); + + it("may apply an Allure ID", () => { + expect(tests).toContainEqual( + expect.objectContaining({ + name: "a test with ID 1004", + labels: expect.arrayContaining([ + { + name: "ALLURE_ID", + value: "1004", + }, + ]), + }), + ); + }); + + it("may apply a custom label", () => { + expect(tests).toContainEqual( + expect.objectContaining({ + name: "a test with an embedded custom label", + labels: expect.arrayContaining([ + { + name: "foo", + value: "bar", + }, + ]), + }), + ); + }); + + it("may apply multiple labels", () => { + expect(tests).toContainEqual( + expect.objectContaining({ + name: "a test two embedded custom label", + labels: expect.arrayContaining([ + { + name: "foo", + value: "bar", + }, + { + name: "baz", + value: "qux", + }, + ]), + }), + ); + }); + + it("meta doesn't affect testCaseId and historyId", () => { + const [ + { testCaseId: testCaseIdBefore, historyId: historyIdBefore }, + { testCaseId: testCaseIdAfter, historyId: historyIdAfter }, + ] = tests.filter((t) => t.name === "a test a changing meta"); + + expect(testCaseIdBefore).toBe(testCaseIdAfter); + expect(historyIdBefore).toBe(historyIdAfter); + }); +}); diff --git a/packages/allure-mocha/test/spec/api/testplan.test.ts b/packages/allure-mocha/test/spec/api/testplan.test.ts new file mode 100644 index 000000000..a0877078d --- /dev/null +++ b/packages/allure-mocha/test/spec/api/testplan.test.ts @@ -0,0 +1,37 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import type { TestResult } from "allure-js-commons"; +import { runMochaInlineTest } from "../../utils.js"; + +describe("testplan", () => { + describe("by selector", () => { + let tests: TestResult[]; + beforeAll(async () => { + ({ tests } = await runMochaInlineTest( + { + testplan: [{ selector: [["plain-mocha", "testInFileScope"], "a test in a file scope"] }], + }, + ["plain-mocha", "testInFileScope"], + ["plain-mocha", "testInSuite"], + )); + }); + + it("selects only one test by fullName", () => { + expect(tests).toEqual([expect.objectContaining({ name: "a test in a file scope" })]); + }); + }); + + describe("by id", () => { + let tests: TestResult[]; + beforeAll(async () => { + ({ tests } = await runMochaInlineTest( + { testplan: [{ id: 1004 }] }, + ["labels", "meta", "id", "1004"], + ["labels", "meta", "id", "1005"], + )); + }); + + it("selects only one test by ID", () => { + expect(tests).toEqual([expect.objectContaining({ name: "a test with ID 1004" })]); + }); + }); +}); diff --git a/packages/allure-mocha/test/spec/config.test.ts b/packages/allure-mocha/test/spec/config.test.ts new file mode 100644 index 000000000..462f4b2d3 --- /dev/null +++ b/packages/allure-mocha/test/spec/config.test.ts @@ -0,0 +1,53 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { Status } from "allure-js-commons"; +import { runMochaInlineTest } from "../utils.js"; + +describe("configuration", () => { + let envInfo: unknown, categories: unknown; + beforeAll(async () => { + ({ envInfo, categories } = await runMochaInlineTest( + { + environmentInfo: { foo: "bar", baz: "qux" }, + categories: [ + { + name: "foo", + description: "bar", + messageRegex: "broken", + matchedStatuses: [Status.BROKEN], + }, + { + name: "baz", + description: "qux", + messageRegex: "failure", + matchedStatuses: [Status.FAILED], + }, + ], + }, + ["plain-mocha", "testInFileScope"], + )); + }); + + it("applies environmentInfo", () => { + expect(envInfo).toMatchObject({ + foo: "bar", + baz: "qux", + }); + }); + + it("applies categories", () => { + expect(categories).toEqual([ + { + name: "foo", + description: "bar", + messageRegex: "broken", + matchedStatuses: [Status.BROKEN], + }, + { + name: "baz", + description: "qux", + messageRegex: "failure", + matchedStatuses: [Status.FAILED], + }, + ]); + }); +}); diff --git a/packages/allure-mocha/test/spec/framework/result.test.ts b/packages/allure-mocha/test/spec/framework/result.test.ts index 5b2f86e45..c03f44c0f 100644 --- a/packages/allure-mocha/test/spec/framework/result.test.ts +++ b/packages/allure-mocha/test/spec/framework/result.test.ts @@ -14,7 +14,9 @@ describe("defaults", () => { }); it("has fullName", () => { - const fnPattern = new RegExp(`plain-mocha\\/testInFileScope\\.spec\\${SPEC_EXT}: a test in a file scope$`); + const fnPattern = new RegExp( + `^test/fixtures/run-results/[a-f0-9-]+/plain-mocha/testInFileScope\\.spec\\${SPEC_EXT}: a test in a file scope$`, + ); expect(test.fullName).toMatch(fnPattern); }); @@ -35,7 +37,9 @@ describe("defaults", () => { it("has default labels", () => { const labels = test.labels; - const packagePattern = new RegExp(`plain-mocha\\.testInFileScope\\.spec\\${SPEC_EXT}$`); + const packagePattern = new RegExp( + `^test\\.fixtures\\.run-results\\.[a-f0-9-]+\\.plain-mocha\\.testInFileScope\\.spec\\${SPEC_EXT}$`, + ); expect(labels).toContainEqual({ name: "language", value: "javascript" }); expect(labels).toContainEqual({ name: "framework", value: "mocha" }); expect(labels).toContainEqual({ name: "host", value: expect.anything() }); diff --git a/packages/allure-mocha/test/utils.ts b/packages/allure-mocha/test/utils.ts index de269a7ad..f2e498721 100644 --- a/packages/allure-mocha/test/utils.ts +++ b/packages/allure-mocha/test/utils.ts @@ -3,12 +3,23 @@ import { randomUUID } from "node:crypto"; import { appendFileSync } from "node:fs"; import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; import * as path from "node:path"; -import type { AllureResults } from "allure-js-commons/sdk"; +import type { AllureResults, Category } from "allure-js-commons/sdk"; import { MessageReader } from "allure-js-commons/sdk/reporter"; type MochaRunOptions = { env?: { [keys: string]: string }; + testplan?: readonly TestPlanEntryFixture[]; + environmentInfo?: { [keys: string]: string }; + categories?: readonly Category[]; }; + +type TestPlanEntryFixture = { + id?: string | number; + selector?: TestPlanSelectorEntryFixture; +}; + +type TestPlanSelectorEntryFixture = [file: readonly string[], name: string]; + type ModuleFormat = "cjs" | "esm"; const getFormatExt = (format: ModuleFormat) => (format === "cjs" ? ".cjs" : ".mjs"); @@ -32,7 +43,7 @@ abstract class AllureMochaTestRunner { readonly fixturesPath: string; readonly samplesPath: string; readonly runResultsDir: string; - constructor(private readonly config: MochaRunOptions) { + constructor(protected readonly config: MochaRunOptions) { this.fixturesPath = path.join(__dirname, "fixtures"); this.samplesPath = path.join(this.fixturesPath, "samples"); this.runResultsDir = path.join(this.fixturesPath, "run-results"); @@ -72,13 +83,31 @@ abstract class AllureMochaTestRunner { const dstDir = path.dirname(dst); const content = await readFile(src, { encoding: "utf-8" }); await mkdir(dstDir, { recursive: true }); - const uncommentedSample = content.replaceAll(uncomment, "$1"); + const uncommentedSample = content.replace(uncomment, "$1"); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument writeFile(dst, uncommentedSample, { encoding: "utf-8" }); }), writeFile(cmdPath, cmdContent, "utf-8"), ]); + if (this.config.testplan) { + const testplanPath = path.join(testDir, "testplan.json"); + this.config.env ??= {}; + this.config.env.ALLURE_TESTPLAN_PATH = testplanPath; + const selectorPrefix = path.relative(path.join(__dirname, ".."), testDir); + await writeFile( + testplanPath, + JSON.stringify({ + version: "1.0", + tests: this.config.testplan.map((test) => ({ + id: test.id, + selector: test.selector ? this.resolveTestplanSelector(selectorPrefix, test.selector) : undefined, + })), + }), + { encoding: "utf-8" }, + ); + } + const testProcess = fork(scriptPath, scriptArgs, { env: { ...process.env, @@ -134,6 +163,12 @@ abstract class AllureMochaTestRunner { ]; }; + protected encodeEnvironmentInfo = () => this.toBase64Url(this.config.environmentInfo); + + protected encodeCategories = () => this.toBase64Url(this.config.categories); + + protected toBase64Url = (value: any) => Buffer.from(JSON.stringify(value)).toString("base64url"); + private getSampleEntry = (name: string | readonly string[], samplesDir: string, testDir: string) => { if (name instanceof Array) { name = path.join(...name); @@ -141,6 +176,10 @@ abstract class AllureMochaTestRunner { return this.getTransformEntry(`${name}.spec`, SPEC_FORMAT, testDir, samplesDir); }; + private resolveTestplanSelector = (prefix: string, [file, name]: TestPlanSelectorEntryFixture) => { + return `${path.join(prefix, ...file)}.spec${SPEC_EXT}: ${name}`; + }; + getFilesToCopy: (testDir: string) => readonly [string, string][] = () => []; getFilesToTransform: (testDir: string) => readonly [string, string, RegExp][] = () => []; abstract getScriptPath: (testDir: string) => string; @@ -149,9 +188,17 @@ abstract class AllureMochaTestRunner { class AllureMochaCliTestRunner extends AllureMochaTestRunner { getFilesToCopy = (testDir: string) => [this.getCopyEntry("reporter.cjs", testDir)]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument getScriptPath = () => path.resolve(require.resolve("mocha"), "../bin/mocha.js"); - getScriptArgs = () => ["--no-color", "--reporter", "./reporter.cjs", "**/*.spec.*"]; + getScriptArgs = () => { + const args = ["--no-color", "--reporter", "./reporter.cjs", "**/*.spec.*"]; + if (this.config.environmentInfo) { + args.push("--reporter-option", `environmentInfo=${this.encodeEnvironmentInfo()}`); + } + if (this.config.categories) { + args.push("--reporter-option", `categories=${this.encodeCategories()}`); + } + return args; + }; } class AllureMochaCodeTestRunner extends AllureMochaTestRunner { @@ -163,7 +210,16 @@ class AllureMochaCodeTestRunner extends AllureMochaTestRunner { } getFilesToTransform = (testDir: string) => [this.getTransformEntry("runner", this.runnerFormat, testDir)]; getScriptPath = (testDir: string) => path.join(testDir, `runner${getFormatExt(this.runnerFormat)}`); - getScriptArgs = () => ["--"]; + getScriptArgs = () => { + const args = ["--"]; + if (this.config.environmentInfo) { + args.push("--environment-info", this.encodeEnvironmentInfo()); + } + if (this.config.categories) { + args.push("--categories", this.encodeCategories()); + } + return args; + }; } export const runMochaInlineTest = async ( diff --git a/packages/allure-mocha/vitest.config.mts b/packages/allure-mocha/vitest.config.mts index f09b562af..f05ce21c5 100644 --- a/packages/allure-mocha/vitest.config.mts +++ b/packages/allure-mocha/vitest.config.mts @@ -5,6 +5,7 @@ export default defineConfig({ dir: "./test/spec", fileParallelism: false, testTimeout: 25000, + hookTimeout: 25000, reporters: ["default"], globalSetup: ["./test/setup.ts"], typecheck: {