Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { expect, it } from "vitest";
import { runCodeceptJsInlineTest } from "../../../utils.js";

it("writes globals payload from scenario body", async () => {
const { globals, attachments } = await runCodeceptJsInlineTest({
"login.test.js": `
const { globalAttachment, globalError } = require("allure-js-commons");

Feature("sample-feature");
Scenario("sample-scenario", async () => {
await globalAttachment("global-log", "hello", { contentType: "text/plain" });
await globalError({ message: "global setup failed", trace: "stack" });
});
`,
});

const globalsEntries = Object.entries(globals ?? {});
expect(globalsEntries).toHaveLength(1);

const [, globalInfo] = globalsEntries[0];
expect(globalInfo.errors).toEqual(
expect.arrayContaining([
{
message: "global setup failed",
trace: "stack",
},
]),
);

const scenarioAttachment = globalInfo.attachments.find((a) => a.name === "global-log");
expect(scenarioAttachment?.type).toBe("text/plain");
const encodedScenarioAttachment = attachments[scenarioAttachment!.source] as string;
expect(Buffer.from(encodedScenarioAttachment, "base64").toString("utf-8")).toBe("hello");
});

it("writes globals payload from suite hooks", async () => {
const { globals, attachments } = await runCodeceptJsInlineTest({
"login.test.js": `
const { globalAttachment, globalError } = require("allure-js-commons");

Feature("sample-feature");
BeforeSuite(async () => {
await globalAttachment("before-suite-log", "before", { contentType: "text/plain" });
});

AfterSuite(async () => {
await globalError({ message: "after suite error" });
});

Scenario("sample-scenario", async () => {});
`,
});

const globalsEntries = Object.entries(globals ?? {});
expect(globalsEntries).toHaveLength(1);

const [, globalInfo] = globalsEntries[0];
expect(globalInfo.errors).toEqual(expect.arrayContaining([{ message: "after suite error" }]));
expect(globalInfo.attachments).toHaveLength(1);

const beforeSuiteAttachment = globalInfo.attachments.find((a) => a.name === "before-suite-log");
expect(beforeSuiteAttachment?.type).toBe("text/plain");
const encodedBeforeSuiteAttachment = attachments[beforeSuiteAttachment!.source] as string;
expect(Buffer.from(encodedBeforeSuiteAttachment, "base64").toString("utf-8")).toBe("before");
});

it("does not collect globals from module scope", async () => {
const { globals, stdout } = await runCodeceptJsInlineTest({
"login.test.js": `
const { globalError } = require("allure-js-commons");

void globalError({ message: "module scope error" });

Feature("sample-feature");
Scenario("sample-scenario", async () => {});
`,
});

const globalsEntries = Object.entries(globals ?? {});
expect(globalsEntries).toHaveLength(0);
expect(stdout.join("\n")).toContain("no test runtime is found");
});
59 changes: 46 additions & 13 deletions packages/allure-cucumberjs/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
import { extname } from "node:path";
import type { Label, Link, TestResult } from "allure-js-commons";
import { ContentType, LabelName, Stage, Status } from "allure-js-commons";
import { getMessageAndTraceFromError, getStatusFromError } from "allure-js-commons/sdk";
import {
type RuntimeMessage,
getMessageAndTraceFromError,
getStatusFromError,
isGlobalRuntimeMessage,
} from "allure-js-commons/sdk";
import {
ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE,
ReporterRuntime,
Expand Down Expand Up @@ -500,16 +505,9 @@ export default class AllureCucumberReporter extends Formatter {
}

private onAttachment(message: messages.Attachment): void {
if (!message.testCaseStartedId) {
return;
}

const fixtureUuid = this.fixtureUuids.get(message.testCaseStartedId);
const testUuid = this.testResultUuids.get(message.testCaseStartedId);
const fixtureUuid = message.testCaseStartedId ? this.fixtureUuids.get(message.testCaseStartedId) : undefined;
const testUuid = message.testCaseStartedId ? this.testResultUuids.get(message.testCaseStartedId) : undefined;
const rootUuid = fixtureUuid ?? testUuid;
if (!rootUuid) {
return;
}

if (message.mediaType === "application/vnd.allure.skipcucumber+json") {
if (testUuid) {
Expand All @@ -521,10 +519,11 @@ export default class AllureCucumberReporter extends Formatter {
}

if (message.mediaType === ALLURE_RUNTIME_MESSAGE_CONTENT_TYPE) {
const parsedMessage = JSON.parse(message.body);
this.applyRuntimeAttachmentMessages(rootUuid, message.body);
return;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.allureRuntime.applyRuntimeMessages(rootUuid, Array.isArray(parsedMessage) ? parsedMessage : [parsedMessage]);
if (!rootUuid) {
return;
}

Expand All @@ -545,9 +544,43 @@ export default class AllureCucumberReporter extends Formatter {
]);
}

private parseRuntimeMessages(messageBody: string): RuntimeMessage[] {
const parsedMessage = JSON.parse(messageBody) as RuntimeMessage | RuntimeMessage[];
return Array.isArray(parsedMessage) ? parsedMessage : [parsedMessage];
}

private applyRuntimeAttachmentMessages(rootUuid: string | undefined, messageBody: string): void {
const runtimeMessages = this.parseRuntimeMessages(messageBody);
this.applyScopedRuntimeAttachmentMessages(rootUuid, runtimeMessages);
this.applyGlobalRuntimeAttachmentMessages(runtimeMessages);
}

private applyScopedRuntimeAttachmentMessages(rootUuid: string | undefined, runtimeMessages: RuntimeMessage[]): void {
if (!rootUuid) {
return;
}

const scopedMessages = runtimeMessages.filter((m) => !isGlobalRuntimeMessage(m));
if (!scopedMessages.length) {
return;
}

this.allureRuntime.applyRuntimeMessages(rootUuid, scopedMessages);
}

private applyGlobalRuntimeAttachmentMessages(runtimeMessages: RuntimeMessage[]): void {
const globalMessages = runtimeMessages.filter(isGlobalRuntimeMessage);
if (!globalMessages.length) {
return;
}

this.allureRuntime.applyGlobalRuntimeMessages(globalMessages);
}

private onTestRunFinished() {
this.allureRuntime.writeCategoriesDefinitions();
this.allureRuntime.writeEnvironmentInfo();
this.allureRuntime.writeGlobals();
}

private exceptionToError(message?: string, exception?: messages.Exception): Error | undefined {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { Given } = require("@cucumber/cucumber");
const { globalAttachment, globalError } = require("allure-js-commons");

Given("a passed step", async () => {
await globalAttachment("global-log", "hello", { contentType: "text/plain" });
await globalError({ message: "global setup failed", trace: "stack" });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const { Before, After } = require("@cucumber/cucumber");
const { globalAttachment, globalError } = require("allure-js-commons");

void globalError({ message: "module scope error" });

Before(async () => {
await globalAttachment("before-log", "before", { contentType: "text/plain" });
});

After(async () => {
await globalError({ message: "after hook error" });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, it } from "vitest";
import { runCucumberInlineTest } from "../../../utils.js";

it("writes globals payload from runtime API calls", async () => {
const { globals, attachments } = await runCucumberInlineTest(["hooks"], ["runtime/modern/globals"], {
parallel: false,
});

const globalsEntries = Object.entries(globals ?? {});
expect(globalsEntries).toHaveLength(1);

const [globalsFileName, globalInfo] = globalsEntries[0];
expect(globalsFileName).toMatch(/.+-globals\.json/);
expect(globalInfo.errors).toEqual([
{
message: "global setup failed",
trace: "stack",
},
]);
expect(globalInfo.attachments).toHaveLength(1);

const [globalAttachmentRef] = globalInfo.attachments;
expect(globalAttachmentRef.name).toBe("global-log");
expect(globalAttachmentRef.type).toBe("text/plain");

const encodedAttachment = attachments[globalAttachmentRef.source] as string;
expect(Buffer.from(encodedAttachment, "base64").toString("utf-8")).toBe("hello");
});

it("supports globals from hooks", async () => {
const { globals, attachments } = await runCucumberInlineTest(["hooks"], ["hooks", "runtime/modern/globalsContexts"], {
parallel: false,
});

const globalsEntries = Object.entries(globals ?? {});
expect(globalsEntries).toHaveLength(1);

const [, globalInfo] = globalsEntries[0];
expect(globalInfo.errors).toEqual(expect.arrayContaining([{ message: "after hook error" }]));

const beforeAttachmentRef = globalInfo.attachments.find((a) => a.name === "before-log");
expect(beforeAttachmentRef?.type).toBe("text/plain");
const encodedAttachment = attachments[beforeAttachmentRef!.source] as string;
expect(Buffer.from(encodedAttachment, "base64").toString("utf-8")).toBe("before");
});

it("does not collect globals from support module scope", async () => {
const { globals } = await runCucumberInlineTest(["hooks"], ["hooks", "runtime/modern/globalsContexts"], {
parallel: false,
});

const globalsEntries = Object.entries(globals ?? {});
expect(globalsEntries).toHaveLength(1);

const [, globalInfo] = globalsEntries[0];
expect(globalInfo.errors).not.toEqual(expect.arrayContaining([{ message: "module scope error" }]));
});
81 changes: 74 additions & 7 deletions packages/allure-cypress/src/browser/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AttachmentOptions, Label, Link, ParameterMode, ParameterOptions } from "allure-js-commons";
import type { AttachmentOptions, Label, Link, ParameterMode, ParameterOptions, StatusDetails } from "allure-js-commons";
import { Status } from "allure-js-commons";
import { getMessageAndTraceFromError } from "allure-js-commons/sdk";
import type { TestRuntime } from "allure-js-commons/sdk/runtime";
Expand All @@ -12,6 +12,11 @@ export const initTestRuntime = () => setGlobalTestRuntime(new AllureCypressTestR

export const getTestRuntime = () => getGlobalTestRuntime() as AllureCypressTestRuntime;

type SerializedBuffer = {
type: "Buffer";
data: number[];
};

class AllureCypressTestRuntime implements TestRuntime {
constructor() {
this.#resetMessages();
Expand Down Expand Up @@ -95,12 +100,10 @@ class AllureCypressTestRuntime implements TestRuntime {
});
}

// @ts-ignore
attachment(name: string, content: string, options: AttachmentOptions) {
// @ts-ignore
const attachmentRawContent: string | Uint8Array = content?.type === "Buffer" ? content.data : content;
const actualEncoding = typeof attachmentRawContent === "string" ? "utf8" : "base64";
const attachmentContent = uint8ArrayToBase64(attachmentRawContent);
attachment(name: string, content: Buffer | string, options: AttachmentOptions) {
const [attachmentContent, actualEncoding] = this.#buildAttachmentContent(
content as Buffer | string | SerializedBuffer,
);

return this.#enqueueMessageAsync({
type: "attachment_content",
Expand All @@ -126,6 +129,42 @@ class AllureCypressTestRuntime implements TestRuntime {
});
}

globalAttachment(name: string, content: Buffer | string, options: AttachmentOptions) {
const [attachmentContent, actualEncoding] = this.#buildAttachmentContent(
content as Buffer | string | SerializedBuffer,
);

return this.#enqueueMessageAsync({
type: "global_attachment_content",
data: {
name,
content: attachmentContent,
encoding: actualEncoding,
contentType: options.contentType,
fileExtension: options.fileExtension,
},
});
}

globalAttachmentFromPath(name: string, path: string, options: Omit<AttachmentOptions, "encoding">) {
return this.#enqueueMessageAsync({
type: "global_attachment_path",
data: {
name,
path,
contentType: options.contentType,
fileExtension: options.fileExtension,
},
});
}

globalError(details: StatusDetails) {
return this.#enqueueMessageAsync({
type: "global_error",
data: details,
});
}

logStep(name: string, status: Status = Status.PASSED, error?: Error): PromiseLike<void> {
if (this.#isInOriginContext()) {
startAllureApiStep(name);
Expand Down Expand Up @@ -210,6 +249,34 @@ class AllureCypressTestRuntime implements TestRuntime {
return messages;
}

#buildAttachmentContent(content: Buffer | string | SerializedBuffer): [string, BufferEncoding] {
const rawContent = this.#normalizeAttachmentContent(content);
const encoding: BufferEncoding = typeof rawContent === "string" ? "utf8" : "base64";

return [uint8ArrayToBase64(rawContent), encoding];
}

#normalizeAttachmentContent(content: Buffer | string | SerializedBuffer): string | Uint8Array {
if (typeof content === "string") {
return content;
}

if (this.#isSerializedBuffer(content)) {
return new Uint8Array(content.data);
}

return content;
}

#isSerializedBuffer(content: unknown): content is SerializedBuffer {
if (!content || typeof content !== "object") {
return false;
}

const candidate = content as Partial<SerializedBuffer>;
return candidate.type === "Buffer" && Array.isArray(candidate.data);
}

#isInOriginContext(): boolean {
try {
const hasOriginContext = !!(window as any).cypressOriginContext;
Expand Down
11 changes: 9 additions & 2 deletions packages/allure-cypress/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type Cypress from "cypress";
import { ContentType, Stage, Status } from "allure-js-commons";
import type { FixtureResult, TestResult } from "allure-js-commons";
import type { RuntimeMessage } from "allure-js-commons/sdk";
import { isGlobalRuntimeMessage } from "allure-js-commons/sdk";
import {
ReporterRuntime,
createDefaultWriter,
Expand Down Expand Up @@ -138,6 +139,7 @@ export class AllureCypress {
this.#endAllSpecs();
this.allureRuntime.writeEnvironmentInfo();
this.allureRuntime.writeCategoriesDefinitions();
this.allureRuntime.writeGlobals();
};

endSpec = (specAbsolutePath: string, cypressVideoPath?: string) => {
Expand Down Expand Up @@ -459,9 +461,14 @@ export class AllureCypress {

#applyRuntimeApiMessages = (context: SpecContext, message: RuntimeMessage) => {
const rootUuid = this.#resolveRootUuid(context);
if (rootUuid) {
this.allureRuntime.applyRuntimeMessages(rootUuid, [message]);
if (isGlobalRuntimeMessage(message)) {
this.allureRuntime.applyGlobalRuntimeMessages([message]);
return;
}
if (!rootUuid) {
return;
}
this.allureRuntime.applyRuntimeMessages(rootUuid, [message]);
};

/**
Expand Down
Loading
Loading