From 995c10ea17607db5ede916d1df14f28022429204 Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Thu, 29 Aug 2024 17:45:37 +0100 Subject: [PATCH] feat(commons): process metadata messages from before hooks (fixes #1075, via #1041) --- packages/allure-jest/test/spec/hooks.test.ts | 51 ++++++++++ .../src/sdk/reporter/LifecycleState.ts | 46 +++++---- .../src/sdk/reporter/ReporterRuntime.ts | 96 ++++++++++++++++--- .../src/sdk/reporter/types.ts | 26 ++++- .../test/sdk/reporter/ReporterRuntime.spec.ts | 61 ++++++++++++ 5 files changed, 243 insertions(+), 37 deletions(-) diff --git a/packages/allure-jest/test/spec/hooks.test.ts b/packages/allure-jest/test/spec/hooks.test.ts index 5237c7f67..1988913d5 100644 --- a/packages/allure-jest/test/spec/hooks.test.ts +++ b/packages/allure-jest/test/spec/hooks.test.ts @@ -401,6 +401,57 @@ it("should support steps in beforeEach hooks", async () => { ); }); +it("should support labels in beforeEach hooks", async () => { + const { tests } = await runJestInlineTest({ + "sample.test.js": ` + const { label } = require("allure-js-commons"); + + beforeEach(async () => { + await label("feature", "value 1"); + await label("story", "value 2"); + }); + + it("passed 1", () => {}); + + it("passed 2", () => {}); + `, + }); + + expect(tests).toHaveLength(2); + expect(tests).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + status: Status.PASSED, + name: "passed 1", + labels: expect.arrayContaining([ + { + name: "feature", + value: "value 1", + }, + { + name: "story", + value: "value 2", + }, + ]), + }), + expect.objectContaining({ + status: Status.PASSED, + name: "passed 2", + labels: expect.arrayContaining([ + { + name: "feature", + value: "value 1", + }, + { + name: "story", + value: "value 2", + }, + ]), + }), + ]), + ); +}); + it("should support steps in afterEach hooks", async () => { const { tests, groups } = await runJestInlineTest({ "sample.test.js": ` diff --git a/packages/allure-js-commons/src/sdk/reporter/LifecycleState.ts b/packages/allure-js-commons/src/sdk/reporter/LifecycleState.ts index 7d9600895..0a2690a8f 100644 --- a/packages/allure-js-commons/src/sdk/reporter/LifecycleState.ts +++ b/packages/allure-js-commons/src/sdk/reporter/LifecycleState.ts @@ -1,74 +1,80 @@ import type { FixtureResult, StepResult, TestResult } from "../../model.js"; -import type { FixtureType, FixtureWrapper, TestScope } from "./types.js"; +import type { FixtureResultWrapper, FixtureType, TestResultWrapper, TestScope } from "./types.js"; export class LifecycleState { - scopes = new Map(); + #scopes = new Map(); - testResults = new Map(); + #testResults = new Map(); - stepResults = new Map(); + #stepResults = new Map(); - fixturesResults = new Map(); + #fixturesResults = new Map(); - getScope = (uuid: string) => this.scopes.get(uuid); + getScope = (uuid: string) => this.#scopes.get(uuid); - getWrappedFixtureResult = (uuid: string) => this.fixturesResults.get(uuid); + getWrappedFixtureResult = (uuid: string) => this.#fixturesResults.get(uuid); getFixtureResult = (uuid: string) => this.getWrappedFixtureResult(uuid)?.value; - getTestResult = (uuid: string) => this.testResults.get(uuid); + getWrappedTestResult = (uuid: string) => this.#testResults.get(uuid); - getStepResult = (uuid: string) => this.stepResults.get(uuid); + getTestResult = (uuid: string) => this.getWrappedTestResult(uuid)?.value; + + getStepResult = (uuid: string) => this.#stepResults.get(uuid); getExecutionItem = (uuid: string): FixtureResult | TestResult | StepResult | undefined => this.getFixtureResult(uuid) ?? this.getTestResult(uuid) ?? this.getStepResult(uuid); // test results - setTestResult = (uuid: string, result: TestResult) => { - this.testResults.set(uuid, result); + setTestResult = (uuid: string, result: TestResult, scopeUuids: string[] = []) => { + this.#testResults.set(uuid, { value: result, scopeUuids }); }; deleteTestResult = (uuid: string) => { - this.testResults.delete(uuid); + this.#testResults.delete(uuid); }; // steps setStepResult = (uuid: string, result: StepResult) => { - this.stepResults.set(uuid, result); + this.#stepResults.set(uuid, result); }; deleteStepResult = (uuid: string) => { - this.stepResults.delete(uuid); + this.#stepResults.delete(uuid); }; // fixtures - setFixtureResult = (uuid: string, type: FixtureType, result: FixtureResult) => { - const wrappedResult: FixtureWrapper = { + setFixtureResult = (scopeUuid: string, uuid: string, type: FixtureType, result: FixtureResult) => { + const wrappedResult: FixtureResultWrapper = { uuid, type, value: result, + scopeUuid, }; - this.fixturesResults.set(uuid, wrappedResult); + this.#fixturesResults.set(uuid, wrappedResult); return wrappedResult; }; deleteFixtureResult = (uuid: string) => { - this.fixturesResults.delete(uuid); + this.#fixturesResults.delete(uuid); }; // test scopes setScope = (uuid: string, data: Partial = {}) => { const scope: TestScope = { + labels: [], + links: [], + parameters: [], fixtures: [], tests: [], ...data, uuid, }; - this.scopes.set(uuid, scope); + this.#scopes.set(uuid, scope); return scope; }; deleteScope = (uuid: string) => { - this.scopes.delete(uuid); + this.#scopes.delete(uuid); }; } diff --git a/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts b/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts index 478ff847a..f55b233d6 100644 --- a/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts +++ b/packages/allure-js-commons/src/sdk/reporter/ReporterRuntime.ts @@ -23,7 +23,14 @@ import { LifecycleState } from "./LifecycleState.js"; import { Notifier } from "./Notifier.js"; import { createFixtureResult, createStepResult, createTestResult } from "./factory.js"; import { hasSkipLabel } from "./testplan.js"; -import type { FixtureType, FixtureWrapper, LinkConfig, ReporterRuntimeConfig, TestScope, Writer } from "./types.js"; +import type { + FixtureResultWrapper, + FixtureType, + LinkConfig, + ReporterRuntimeConfig, + TestScope, + Writer, +} from "./types.js"; import { deepClone, formatLinks, getTestResultHistoryId, getTestResultTestCaseId, randomUuid } from "./utils.js"; import { buildAttachmentFileName } from "./utils/attachments.js"; import { resolveWriter } from "./writer/loader.js"; @@ -144,7 +151,7 @@ export class ReporterRuntime { } const uuid = randomUuid(); - const wrappedFixture = this.state.setFixtureResult(uuid, type, { + const wrappedFixture = this.state.setFixtureResult(scopeUuid, uuid, type, { ...createFixtureResult(), start: Date.now(), ...fixtureResult, @@ -201,7 +208,7 @@ export class ReporterRuntime { scope.tests.push(uuid); }); - this.state.setTestResult(uuid, testResult); + this.state.setTestResult(uuid, testResult, scopeUuids); this.notifier.afterTestResultStart(testResult); return uuid; }; @@ -221,13 +228,15 @@ export class ReporterRuntime { }; stopTest = (uuid: string, opts?: { stop?: number; duration?: number }) => { - const testResult = this.state.getTestResult(uuid); - if (!testResult) { + const wrapped = this.state.getWrappedTestResult(uuid); + if (!wrapped) { // eslint-disable-next-line no-console console.error(`could not stop test result: no test with uuid ${uuid}) is found`); return; } + const testResult = wrapped.value; + this.notifier.beforeTestResultStop(testResult); testResult.testCaseId ??= getTestResultTestCaseId(testResult); testResult.historyId ??= getTestResultHistoryId(testResult); @@ -236,11 +245,19 @@ export class ReporterRuntime { testResult.start = startStop.start; testResult.stop = startStop.stop; + const scopeUuids = wrapped.scopeUuids; + scopeUuids.forEach((scopeUuid) => { + const scope = this.state.getScope(scopeUuid); + if (scope?.labels) { + testResult.labels = [...testResult.labels, ...scope.labels]; + } + }); + this.notifier.afterTestResultStop(testResult); }; writeTest = (uuid: string) => { - const testResult = this.state.testResults.get(uuid); + const testResult = this.state.getTestResult(uuid); if (!testResult) { // eslint-disable-next-line no-console console.error(`could not write test result: no test with uuid ${uuid} is found`); @@ -430,17 +447,57 @@ export class ReporterRuntime { #handleMetadataMessage = (rootUuid: string, message: RuntimeMetadataMessage["data"]) => { // only display name could be set to fixture. - const fixtureResult = this.state.getFixtureResult(rootUuid); + const fixtureResult = this.state.getWrappedFixtureResult(rootUuid); + const { links, labels, parameters, displayName, testCaseId, historyId, description, descriptionHtml } = message; + if (fixtureResult) { - this.updateFixture(rootUuid, (result) => { - if (message.displayName) { - result.name = message.displayName; + if (displayName) { + this.updateFixture(rootUuid, (result) => { + result.name = displayName; + }); + } + + if (historyId) { + // eslint-disable-next-line no-console + console.error("historyId can't be changed within test fixtures"); + } + if (testCaseId) { + // eslint-disable-next-line no-console + console.error("testCaseId can't be changed within test fixtures"); + } + + if (links || labels || parameters || description || descriptionHtml) { + // in some frameworks, afterEach methods can be executed before test stop event, while + // in others after. To remove the possible undetermined behaviour we only allow + // using runtime metadata API in before hooks. + if (fixtureResult.type === "after") { + // eslint-disable-next-line no-console + console.error("metadata messages isn't supported for after test fixtures"); + return; } - }); + + this.updateScope(fixtureResult.scopeUuid, (scope) => { + if (links) { + scope.links = [...scope.links, ...(this.linkConfig ? formatLinks(this.linkConfig, links) : links)]; + } + if (labels) { + scope.labels = [...scope.labels, ...labels]; + } + if (parameters) { + scope.parameters = [...scope.parameters, ...parameters]; + } + if (description) { + scope.description = description; + } + if (descriptionHtml) { + scope.descriptionHtml = descriptionHtml; + } + }); + } + return; } - const { links, labels, parameters, displayName, ...rest } = message; this.updateTest(rootUuid, (result) => { if (links) { result.links = [...result.links, ...(this.linkConfig ? formatLinks(this.linkConfig, links) : links)]; @@ -454,7 +511,18 @@ export class ReporterRuntime { if (displayName) { result.name = displayName; } - Object.assign(result, rest); + if (testCaseId) { + result.testCaseId = testCaseId; + } + if (historyId) { + result.historyId = historyId; + } + if (description) { + result.description = description; + } + if (descriptionHtml) { + result.descriptionHtml = descriptionHtml; + } }); }; @@ -549,7 +617,7 @@ export class ReporterRuntime { } }; - #writeContainer = (tests: string[], wrappedFixture: FixtureWrapper) => { + #writeContainer = (tests: string[], wrappedFixture: FixtureResultWrapper) => { const fixture = wrappedFixture.value; const befores = wrappedFixture.type === "before" ? [wrappedFixture.value] : []; const afters = wrappedFixture.type === "after" ? [wrappedFixture.value] : []; diff --git a/packages/allure-js-commons/src/sdk/reporter/types.ts b/packages/allure-js-commons/src/sdk/reporter/types.ts index 370f813b5..f2e38cbb1 100644 --- a/packages/allure-js-commons/src/sdk/reporter/types.ts +++ b/packages/allure-js-commons/src/sdk/reporter/types.ts @@ -1,4 +1,13 @@ -import type { FixtureResult, LinkType, StepResult, TestResult, TestResultContainer } from "../../model.js"; +import type { + FixtureResult, + Label, + Link, + LinkType, + Parameter, + StepResult, + TestResult, + TestResultContainer, +} from "../../model.js"; import type { Category, EnvironmentInfo } from "../types.js"; export const ALLURE_METADATA_CONTENT_TYPE = "application/vnd.allure.metadata+json"; @@ -67,13 +76,24 @@ export interface Writer { export type TestScope = { uuid: string; tests: string[]; - fixtures: FixtureWrapper[]; + fixtures: FixtureResultWrapper[]; + labels: Label[]; + links: Link[]; + parameters: Parameter[]; + description?: string; + descriptionHtml?: string; }; export type FixtureType = "before" | "after"; -export type FixtureWrapper = { +export type FixtureResultWrapper = { uuid: string; value: FixtureResult; type: FixtureType; + scopeUuid: string; +}; + +export type TestResultWrapper = { + value: TestResult; + scopeUuids: string[]; }; 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 2031f67d8..c924a7c1a 100644 --- a/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts +++ b/packages/allure-js-commons/test/sdk/reporter/ReporterRuntime.spec.ts @@ -437,6 +437,67 @@ describe("ReporterRuntime", () => { expect(writer.writeResult.mock.calls.length).toBe(0); }); + + it("should support metadata messages from before fixtures", () => { + const writer = mockWriter(); + const runtime = new ReporterRuntime({ writer }); + + const scopeUuid = runtime.startScope(); + const fixtureUuid = runtime.startFixture(scopeUuid, "before", {})!; + runtime.applyRuntimeMessages(fixtureUuid, [ + { + type: "metadata", + data: { + labels: [ + { + name: "label 1", + value: "value 1", + }, + { + name: "label 2", + value: "value 2", + }, + ], + }, + }, + ]); + runtime.stopFixture(fixtureUuid); + const testUuid = runtime.startTest({}, [scopeUuid]); + runtime.applyRuntimeMessages(testUuid, [ + { + type: "metadata", + data: { + labels: [ + { + name: "label 3", + value: "value 3", + }, + ], + }, + }, + ]); + runtime.stopTest(testUuid); + runtime.writeTest(testUuid); + runtime.writeScope(scopeUuid); + + const [testResult] = writer.writeResult.mock.calls[0]; + expect(testResult.labels).toEqual( + expect.arrayContaining([ + { + name: "label 1", + value: "value 1", + }, + { + name: "label 2", + value: "value 2", + }, + { + name: "label 3", + value: "value 3", + }, + ]), + ); + }); }); describe("load well-known writers", () => {