From a0cf483959672ad6df7eaffa1be0840865d6dfbd Mon Sep 17 00:00:00 2001 From: epszaw Date: Fri, 24 Jan 2025 14:12:12 +0100 Subject: [PATCH 1/2] Support spaces in the title metadata format (#1227) --- packages/allure-js-commons/src/sdk/utils.ts | 92 +++++++++++++++---- .../allure-js-commons/test/sdk/utils.spec.ts | 43 +++++++++ 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/packages/allure-js-commons/src/sdk/utils.ts b/packages/allure-js-commons/src/sdk/utils.ts index dfdf3a068..15754f741 100644 --- a/packages/allure-js-commons/src/sdk/utils.ts +++ b/packages/allure-js-commons/src/sdk/utils.ts @@ -44,7 +44,14 @@ export const stripAnsi = (str: string): string => { return str.replace(regex, ""); }; -export const getMessageAndTraceFromError = (error: Error | { message?: string; stack?: string }): StatusDetails => { +export const getMessageAndTraceFromError = ( + error: + | Error + | { + message?: string; + stack?: string; + }, +): StatusDetails => { const { message, stack } = error; const actual = "actual" in error && error.actual !== undefined ? { actual: serialize(error.actual) } : {}; const expected = "expected" in error && error.expected !== undefined ? { expected: serialize(error.expected) } : {}; @@ -56,13 +63,40 @@ export const getMessageAndTraceFromError = (error: Error | { message?: string; s }; }; -export const allureIdRegexp = /(?:^|\s)@?allure\.id[:=](?[^\s]+)/; -export const allureIdRegexpGlobal = new RegExp(allureIdRegexp, "g"); +type AllureTitleMetadataMatch = RegExpMatchArray & { + groups: { + type?: string; + v1?: string; + v2?: string; + v3?: string; + v4?: string; + }; +}; + +export const allureTitleMetadataRegexp = /(?:^|\s)@?allure\.(?\S+)[:=]("[^"]+"|'[^']+'|`[^`]+`|\S+)/; +export const allureTitleMetadataRegexpGlobal = new RegExp(allureTitleMetadataRegexp, "g"); +export const allureIdRegexp = /(?:^|\s)@?allure\.id[:=](?\S+)/; export const allureLabelRegexp = /(?:^|\s)@?allure\.label\.(?[^:=\s]+)[:=](?[^\s]+)/; -export const allureLabelRegexpGlobal = new RegExp(allureLabelRegexp, "g"); + +export const getTypeFromAllureTitleMetadataMatch = (match: AllureTitleMetadataMatch) => { + return match?.[1]; +}; + +export const getValueFromAllureTitleMetadataMatch = (match: AllureTitleMetadataMatch) => { + const quotesRegexp = /['"`]/; + const quoteOpenRegexp = new RegExp(`^${quotesRegexp.source}`); + const quoteCloseRegexp = new RegExp(`${quotesRegexp.source}$`); + const matchedValue = match?.[2] ?? ""; + + if (quoteOpenRegexp.test(matchedValue) && quoteCloseRegexp.test(matchedValue)) { + return matchedValue.slice(1, -1); + } + + return matchedValue; +}; export const isMetadataTag = (tag: string) => { - return allureIdRegexp.test(tag) || allureLabelRegexp.test(tag); + return allureTitleMetadataRegexp.test(tag); }; export const extractMetadataFromString = ( @@ -72,25 +106,45 @@ export const extractMetadataFromString = ( cleanTitle: string; } => { const labels = [] as Label[]; - - title.split(" ").forEach((val) => { - const idValue = val.match(allureIdRegexp)?.groups?.id; - - if (idValue) { - labels.push({ name: LabelName.ALLURE_ID, value: idValue }); + const metadata = title.matchAll(allureTitleMetadataRegexpGlobal); + const cleanTitle = title + .replaceAll(allureTitleMetadataRegexpGlobal, "") + .split(" ") + .filter(Boolean) + .reduce((acc, word) => { + if (/^[\n\r]/.test(word)) { + return acc + word; + } + + return `${acc} ${word}`; + }, "") + .trim(); + + for (const m of metadata) { + const match = m as AllureTitleMetadataMatch; + const type = getTypeFromAllureTitleMetadataMatch(match); + const value = getValueFromAllureTitleMetadataMatch(match); + + if (!type || !value) { + continue; } - const labelMatch = val.match(allureLabelRegexp); - const { name, value } = labelMatch?.groups || {}; + const [subtype, name] = type.split("."); - if (name && value) { - labels?.push({ name, value }); + switch (subtype) { + case "id": + labels.push({ name: LabelName.ALLURE_ID, value }); + break; + case "label": + labels.push({ name, value }); + break; } - }); - - const cleanTitle = title.replace(allureLabelRegexpGlobal, "").replace(allureIdRegexpGlobal, "").trim(); + } - return { labels, cleanTitle }; + return { + labels, + cleanTitle, + }; }; export const isAnyStepFailed = (item: StepResult | TestResult | FixtureResult): boolean => { diff --git a/packages/allure-js-commons/test/sdk/utils.spec.ts b/packages/allure-js-commons/test/sdk/utils.spec.ts index b19d1715c..569d2d567 100644 --- a/packages/allure-js-commons/test/sdk/utils.spec.ts +++ b/packages/allure-js-commons/test/sdk/utils.spec.ts @@ -296,6 +296,49 @@ describe("extractMetadataFromString", () => { ], }); }); + + it("should support values in single quotes", () => { + expect( + extractMetadataFromString("foo @allure.label.l1='foo bar baz' and bar @allure.id=beep @allure.label.l1=boop"), + ).toEqual({ + cleanTitle: "foo and bar", + labels: [ + { name: "l1", value: "foo bar baz" }, + { name: LabelName.ALLURE_ID, value: "beep" }, + { name: "l1", value: "boop" }, + ], + }); + }); + + it("should support values in double quotes", () => { + expect(extractMetadataFromString('foo @allure.label.l1="foo bar baz"')).toEqual({ + cleanTitle: "foo", + labels: [{ name: "l1", value: "foo bar baz" }], + }); + }); + + it("should support values in backticks", () => { + expect(extractMetadataFromString("foo @allure.label.l1=`foo bar baz`")).toEqual({ + cleanTitle: "foo", + labels: [{ name: "l1", value: "foo bar baz" }], + }); + }); + + it("should support mixed values at the same time", () => { + expect( + extractMetadataFromString( + "foo @allure.label.l1=foo @allure.label.l1=`foo 1` bar @allure.label.l1='foo 2' baz @allure.label.l1=\"foo 3\"", + ), + ).toEqual({ + cleanTitle: "foo bar baz", + labels: [ + { name: "l1", value: "foo" }, + { name: "l1", value: "foo 1" }, + { name: "l1", value: "foo 2" }, + { name: "l1", value: "foo 3" }, + ], + }); + }); }); describe("isMetadataTag", () => { From ecad2779179d1f6b7a41756f28e21c6e097a7944 Mon Sep 17 00:00:00 2001 From: epszaw Date: Tue, 28 Jan 2025 15:11:26 +0100 Subject: [PATCH 2/2] Fix cypress issues (#1228) Co-authored-by: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> --- .../allure-cypress/src/browser/commandLog.ts | 18 +++++++++++++--- .../src/browser/events/cypress.ts | 1 + packages/allure-cypress/src/browser/state.ts | 3 +++ packages/allure-cypress/src/browser/utils.ts | 5 ++++- .../allure-cypress/test/spec/security.test.ts | 21 +++++++++++++++++++ 5 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 packages/allure-cypress/test/spec/security.test.ts diff --git a/packages/allure-cypress/src/browser/commandLog.ts b/packages/allure-cypress/src/browser/commandLog.ts index e27d5fa30..fb587e74f 100644 --- a/packages/allure-cypress/src/browser/commandLog.ts +++ b/packages/allure-cypress/src/browser/commandLog.ts @@ -51,6 +51,7 @@ export const setupScreenshotAttachmentStep = (originalName: string | undefined, export const startCommandLogStep = (entry: CypressLogEntry) => { const currentLogEntry = getCurrentLogEntry(); + if (typeof currentLogEntry !== "undefined" && shouldStopCurrentLogStep(currentLogEntry.log, entry)) { stopCommandLogStep(currentLogEntry.log.attributes.id); } @@ -65,12 +66,14 @@ export const stopCommandLogStep = (entryId: string) => findAndStopStepWithSubste const pushLogEntry = (entry: CypressLogEntry) => { const id = entry.attributes.id; const stepDescriptor: LogStepDescriptor = { id, type: "log", log: entry }; + pushStep(stepDescriptor); // Some properties of some Command Log entries are undefined at the time the entry is stopped. An example is the // Yielded property of some queries. We defer converting them to Allure step parameters until the test/hook ends. setupStepFinalization(stepDescriptor, (data) => { data.parameters = getCommandLogStepParameters(entry); + if (stepDescriptor.attachmentName) { // Rename the step to match the attachment name. Once the names are the same, Allure will render the // attachment in the place of the step. @@ -146,18 +149,27 @@ const getLogProps = (entry: CypressLogEntry) => { attributes: { consoleProps }, } = entry; const isAssertionWithMessage = !!maybeGetAssertionLogMessage(entry); + const { props, name } = consoleProps(); + + // accessing LocalStorage after the page reload can stick the test runner + // to avoid the issue, we just need to log the command manually + // the problem potentially can happen with other storage related commands, like `clearAllLocalStorage`, `clearAllSessionStorage`, `getAllLocalStorage`, `getAllSessionStorage`, `setLocalStorage`, `setSessionStorage` + // but probably, we don't need to silent them all at this moment + // the context: https://github.com/allure-framework/allure-js/issues/1222 + if (["clearLocalStorage"].includes(name)) { + return [] as [string, unknown][]; + } // For assertion logs, we interpolate the 'Message' property, which contains unformatted assertion description, // directly into the step's name. // No need to keep the exact same information in the step's parameters. - return Object.entries(consoleProps().props).filter( - ([k, v]) => isDefined(v) && !(isAssertionWithMessage && k === "Message"), - ); + return Object.entries(props).filter(([k, v]) => isDefined(v) && !(isAssertionWithMessage && k === "Message")); }; const maybeGetAssertionLogMessage = (entry: CypressLogEntry) => { if (isAssertLog(entry)) { const message = entry.attributes.consoleProps().props.Message; + if (message && typeof message === "string") { return message; } diff --git a/packages/allure-cypress/src/browser/events/cypress.ts b/packages/allure-cypress/src/browser/events/cypress.ts index fd0514d00..969330e83 100644 --- a/packages/allure-cypress/src/browser/events/cypress.ts +++ b/packages/allure-cypress/src/browser/events/cypress.ts @@ -16,6 +16,7 @@ const onAfterScreenshot = ( ...[, { name: originalName, path }]: Parameters ) => { const name = originalName ?? getFileNameFromPath(path); + reportScreenshot(path, name); setupScreenshotAttachmentStep(originalName, name); }; diff --git a/packages/allure-cypress/src/browser/state.ts b/packages/allure-cypress/src/browser/state.ts index fb6a2154d..aa99d987e 100644 --- a/packages/allure-cypress/src/browser/state.ts +++ b/packages/allure-cypress/src/browser/state.ts @@ -3,6 +3,7 @@ import { DEFAULT_RUNTIME_CONFIG, last, toReversed } from "../utils.js"; export const getAllureState = () => { let state = Cypress.env("allure") as AllureSpecState; + if (!state) { state = { config: DEFAULT_RUNTIME_CONFIG, @@ -15,8 +16,10 @@ export const getAllureState = () => { stepsToFinalize: [], nextApiStepId: 0, }; + Cypress.env("allure", state); } + return state; }; diff --git a/packages/allure-cypress/src/browser/utils.ts b/packages/allure-cypress/src/browser/utils.ts index 573a11e30..2f9615589 100644 --- a/packages/allure-cypress/src/browser/utils.ts +++ b/packages/allure-cypress/src/browser/utils.ts @@ -30,7 +30,10 @@ export const uint8ArrayToBase64 = (data: unknown) => { export const getTestStartData = (test: CypressTest) => ({ ...getNamesAndLabels(Cypress.spec, test), - start: test.wallClockStartedAt?.getTime() || Date.now(), + start: + typeof test.wallClockStartedAt === "string" + ? Date.parse(test.wallClockStartedAt) + : test.wallClockStartedAt?.getTime?.() || Date.now(), }); export const getTestStopData = (test: CypressTest) => ({ diff --git a/packages/allure-cypress/test/spec/security.test.ts b/packages/allure-cypress/test/spec/security.test.ts new file mode 100644 index 000000000..608343d2e --- /dev/null +++ b/packages/allure-cypress/test/spec/security.test.ts @@ -0,0 +1,21 @@ +import { expect, it } from "vitest"; +import { Stage, Status } from "allure-js-commons"; +import { runCypressInlineTest } from "../utils.js"; + +it("shouldn't break the flow when access storage after the page reload", async () => { + const { tests } = await runCypressInlineTest({ + "cypress/e2e/sample.cy.js": () => ` + it("passed", () => { + cy.visit("https://allurereport.org"); + cy.clearLocalStorage(); + cy.wait(200); + cy.reload(); + cy.wait(200); + }); + `, + }); + + expect(tests).toHaveLength(1); + expect(tests[0].status).toBe(Status.PASSED); + expect(tests[0].stage).toBe(Stage.FINISHED); +});