From 5ac84d4ee94e24bd8398467ed2265d1fd111a416 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:15:37 +0700 Subject: [PATCH] feat(allure-cypress): impose some limits on step parameter values created from Cypress command arguments (#1127) --- packages/allure-cypress/README.md | 69 ++++++- packages/allure-cypress/src/model.ts | 7 + packages/allure-cypress/src/reporter.ts | 29 ++- packages/allure-cypress/src/runtime.ts | 9 +- packages/allure-cypress/src/state.ts | 4 + packages/allure-cypress/src/utils.ts | 7 + .../allure-cypress/test/spec/commands.test.ts | 150 ++++++++++++++- packages/allure-js-commons/src/sdk/index.ts | 1 + .../src/sdk/reporter/utils.ts | 12 -- packages/allure-js-commons/src/sdk/types.ts | 5 + packages/allure-js-commons/src/sdk/utils.ts | 46 ++++- .../test/sdk/reporter/utils.spec.ts | 57 +----- .../allure-js-commons/test/sdk/utils.spec.ts | 172 ++++++++++++++++++ packages/allure-mocha/src/legacy.ts | 2 +- 14 files changed, 478 insertions(+), 92 deletions(-) diff --git a/packages/allure-cypress/README.md b/packages/allure-cypress/README.md index 5ecc3677a..d2afaaacd 100644 --- a/packages/allure-cypress/README.md +++ b/packages/allure-cypress/README.md @@ -71,13 +71,30 @@ Learn more about Allure Cypress from the official documentation at ## Allure Cypress options -| Option | Description | Default | -|-----------------|----------------------------------------------------------------------------------------------------------------------|--------------------| -| resultsDir | The path of the results folder. | `./allure-results` | -| videoOnFailOnly | When video capturing is enabled, set this option to `true` to attach the video to failed specs only. | `undefined` | -| links | Allure Runtime API link templates. | `undefined` | -| environmentInfo | A set of key-value pairs to display in the Environment section of the report | `undefined` | -| categories | An array of category definitions, each describing a [category of defects](https://allurereport.org/docs/categories/) | `undefined` | + +Customize Allure Cypress by providing a configuration object as the third argument +of `allureCypress`. + +The following options are supported: + +| Option | Description | Default | +|-------------------|----------------------------------------------------------------------------------------------------------------------|---------------------------------| +| resultsDir | The path of the results folder. | `./allure-results` | +| videoOnFailOnly | When video capturing is enabled, set this option to `true` to attach the video to failed specs only. | `false` | +| links | Allure Runtime API link templates. | `undefined` | +| stepsFromCommands | Options that affect how Allure creates steps from Cypress commands | See [below](#stepsfromcommands) | +| environmentInfo | A set of key-value pairs to display in the Environment section of the report | `undefined` | +| categories | An array of category definitions, each describing a [category of defects](https://allurereport.org/docs/categories/) | `undefined` | + +### stepsFromCommands + +| Property | Description | Default | +|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------|---------| +| maxArgumentLength | The maximum length of the parameter value created from Cypress command argument. The rest of the characters are replaces with `...`. | 128 | +| maxArgumentDepth | The maximum depth of the Cypress command argument (an array or an object) that will be converted to the corresponding step parameter value. | 3 | + + +### Example Here is an example of the Allure Cypress configuration: @@ -100,6 +117,10 @@ export default defineConfig({ nameTemplate: "ISSUE-%s", }, }, + stepsFromCommands: { + maxArgumentLength: 64, + maxArgumentDepth: 5, + }, environmentInfo: { OS: os.platform(), Architecture: os.arch(), @@ -157,7 +178,39 @@ export default defineConfig({ }); ``` -## Known limitations +## Common issues + +### The test plan feature doesn't work + +Make sure you pass the Cypress config as the second argument of `allureCypress`. + +Correct: + +```javascript +allureCypress(on, config); +``` + +Also correct: + +```javascript +allureCypress(on, config, { + resultsDir: "output", +}); +``` + +Incorrect (the test plan won't work): + +```javascript +allureCypress(on); +``` + +Also incorrect (the legacy style; the test plan won't work either): + +```javascript +allureCypress(on, { + resultsDir: "output", +}); +``` ### `setupNodeEvents` limitations diff --git a/packages/allure-cypress/src/model.ts b/packages/allure-cypress/src/model.ts index d8152ebe6..50ce4ab18 100644 --- a/packages/allure-cypress/src/model.ts +++ b/packages/allure-cypress/src/model.ts @@ -8,6 +8,7 @@ export const ALLURE_REPORT_STEP_COMMAND = "__allure_report_step_command__"; export type AllureCypressConfig = ReporterConfig & { videoOnFailOnly?: boolean; + stepsFromCommands?: Partial; }; export type CypressSuite = Mocha.Suite & { @@ -174,6 +175,12 @@ export type SpecContext = { }; export type AllureSpecState = { + config: { + stepsFromCommands: { + maxArgumentLength: number; + maxArgumentDepth: number; + }; + }; initialized: boolean; testPlan: TestPlanV1 | null | undefined; messages: CypressMessage[]; diff --git a/packages/allure-cypress/src/reporter.ts b/packages/allure-cypress/src/reporter.ts index 702114514..135f20995 100644 --- a/packages/allure-cypress/src/reporter.ts +++ b/packages/allure-cypress/src/reporter.ts @@ -32,7 +32,7 @@ import type { CypressTestStartMessage, SpecContext, } from "./model.js"; -import { last } from "./utils.js"; +import { DEFAULT_RUNTIME_CONFIG, last } from "./utils.js"; export class AllureCypress { allureRuntime: ReporterRuntime; @@ -489,19 +489,28 @@ export class AllureCypress { }; } -const getInitialSpecState = (): AllureSpecState => ({ +const createRuntimeState = (allureConfig?: AllureCypressConfig): AllureSpecState => ({ + config: getRuntimeConfigDefaults(allureConfig), initialized: false, messages: [], testPlan: parseTestPlan(), }); -/** - * Explicitly enables the selective run feature. - * @param config The Cypress configuration. - */ -export const enableTestPlan = (config: Cypress.PluginConfigOptions) => { - config.env.allure = getInitialSpecState(); - return config; +const getRuntimeConfigDefaults = ({ + stepsFromCommands: { + maxArgumentLength = DEFAULT_RUNTIME_CONFIG.stepsFromCommands.maxArgumentLength, + maxArgumentDepth = DEFAULT_RUNTIME_CONFIG.stepsFromCommands.maxArgumentDepth, + } = DEFAULT_RUNTIME_CONFIG.stepsFromCommands, +}: AllureCypressConfig = DEFAULT_RUNTIME_CONFIG): AllureSpecState["config"] => ({ + stepsFromCommands: { + maxArgumentDepth, + maxArgumentLength, + }, +}); + +const initializeRuntimeState = (cypressConfig: Cypress.PluginConfigOptions, allureConfig?: AllureCypressConfig) => { + cypressConfig.env.allure = createRuntimeState(allureConfig); + return cypressConfig; }; /** @@ -539,7 +548,7 @@ export const allureCypress = ( allureCypressReporter.attachToCypress(on); if (cypressConfig && "env" in cypressConfig) { - enableTestPlan(cypressConfig); + initializeRuntimeState(cypressConfig, allureConfig); } return allureCypressReporter; diff --git a/packages/allure-cypress/src/runtime.ts b/packages/allure-cypress/src/runtime.ts index d4a0cae5d..65ac9ffe2 100644 --- a/packages/allure-cypress/src/runtime.ts +++ b/packages/allure-cypress/src/runtime.ts @@ -5,6 +5,7 @@ import { getStatusFromError, getUnfinishedStepsMessages, isPromise, + serialize, } from "allure-js-commons/sdk"; import type { RuntimeMessage } from "allure-js-commons/sdk"; import { getGlobalTestRuntime, setGlobalTestRuntime } from "allure-js-commons/sdk/runtime"; @@ -26,6 +27,7 @@ import { ALLURE_REPORT_STEP_COMMAND } from "./model.js"; import { dropCurrentTest, enqueueRuntimeMessage, + getConfig, getCurrentTest, getRuntimeMessages, setCurrentTest, @@ -361,11 +363,16 @@ export const reportTestSkip = (test: CypressTest) => { }; export const reportCommandStart = (command: CypressCommand) => { + const { + stepsFromCommands: { maxArgumentDepth, maxArgumentLength }, + } = getConfig(); enqueueRuntimeMessage({ type: "cypress_command_start", data: { name: `Command "${command.attributes.name}"`, - args: command.attributes.args.map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg, null, 2))), + args: command.attributes.args.map((arg) => + serialize(arg, { maxDepth: maxArgumentDepth, maxLength: maxArgumentLength }), + ), start: Date.now(), }, }); diff --git a/packages/allure-cypress/src/state.ts b/packages/allure-cypress/src/state.ts index bc4cd9367..03b525847 100644 --- a/packages/allure-cypress/src/state.ts +++ b/packages/allure-cypress/src/state.ts @@ -1,9 +1,11 @@ import type { AllureSpecState, CypressMessage, CypressTest } from "./model.js"; +import { DEFAULT_RUNTIME_CONFIG } from "./utils.js"; export const getAllureState = () => { let state = Cypress.env("allure") as AllureSpecState; if (!state) { state = { + config: DEFAULT_RUNTIME_CONFIG, initialized: false, messages: [], testPlan: undefined, @@ -41,3 +43,5 @@ export const setCurrentTest = (test: CypressTest) => { export const dropCurrentTest = () => { getAllureState().currentTest = undefined; }; + +export const getConfig = () => getAllureState().config; diff --git a/packages/allure-cypress/src/utils.ts b/packages/allure-cypress/src/utils.ts index 84d6e73bc..6bf131cf3 100644 --- a/packages/allure-cypress/src/utils.ts +++ b/packages/allure-cypress/src/utils.ts @@ -5,6 +5,13 @@ import { ALLURE_REPORT_STEP_COMMAND, ALLURE_REPORT_SYSTEM_HOOK } from "./model.j import type { CypressCommand, CypressHook, CypressSuite, CypressTest } from "./model.js"; import { getAllureTestPlan } from "./state.js"; +export const DEFAULT_RUNTIME_CONFIG = { + stepsFromCommands: { + maxArgumentLength: 128, + maxArgumentDepth: 3, + }, +}; + export const uint8ArrayToBase64 = (data: unknown) => { // @ts-ignore const u8arrayLike = Array.isArray(data) || data.buffer; diff --git a/packages/allure-cypress/test/spec/commands.test.ts b/packages/allure-cypress/test/spec/commands.test.ts index 0d8836579..19b910c5b 100644 --- a/packages/allure-cypress/test/spec/commands.test.ts +++ b/packages/allure-cypress/test/spec/commands.test.ts @@ -1,5 +1,6 @@ import { expect, it } from "vitest"; import { Stage, Status } from "allure-js-commons"; +import { issue } from "allure-js-commons"; import { runCypressInlineTest } from "../utils.js"; it("reports test with cypress command", async () => { @@ -25,7 +26,7 @@ it("reports test with cypress command", async () => { parameters: expect.arrayContaining([ expect.objectContaining({ name: String.raw`Argument [0]`, - value: JSON.stringify(1, null, 2), + value: JSON.stringify(1), }), ]), }), @@ -43,7 +44,7 @@ it("reports test with cypress command", async () => { parameters: expect.arrayContaining([ expect.objectContaining({ name: String.raw`Argument [0]`, - value: JSON.stringify([1, 2, 3], null, 2), + value: JSON.stringify([1, 2, 3]), }), ]), }), @@ -52,7 +53,7 @@ it("reports test with cypress command", async () => { parameters: expect.arrayContaining([ expect.objectContaining({ name: String.raw`Argument [0]`, - value: JSON.stringify({ foo: 1, bar: 2, baz: 3 }, null, 2), + value: JSON.stringify({ foo: 1, bar: 2, baz: 3 }), }), ]), }), @@ -77,3 +78,146 @@ it("doesn't report cypress command when they shouldn't be reported", async () => expect(tests[0].stage).toBe(Stage.FINISHED); expect(tests[0].steps).toHaveLength(0); }); + +it("should impose limits on command arguments", async () => { + issue("1070"); + const { tests } = await runCypressInlineTest({ + "cypress/e2e/sample.cy.js": () => ` + it("foo", () => { + const obj1 = {}; + obj1.ref = obj1; // should remove a direct circular reference 'ref' + cy.wrap(obj1); + + const sibling = {}; + cy.wrap({ ref: { foo: sibling, bar: sibling } }); // it's okay to have the same object on different paths + + const obj2 = { ref: {} }; + obj2.ref.ref = obj2; + cy.wrap(obj2); // should remove an indirect circular reference 'ref.ref' + + cy.wrap("A".repeat(1000)); // should truncate string values + cy.wrap(Array(1000).fill("A")); // should truncate objects + cy.wrap({ foo: { bar: { baz: {}, qux: "qut" } } }) // should remove 'baz' because it creates nesting level 4 + }); + `, + }); + + expect(tests).toEqual([ + expect.objectContaining({ + steps: [ + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: JSON.stringify({}), + }, + ], + }), + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: JSON.stringify({ ref: { foo: {}, bar: {} } }), + }, + ], + }), + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: JSON.stringify({ ref: {} }), + }, + ], + }), + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: `${"A".repeat(128)}...`, + }, + ], + }), + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: `[${String.raw`"A",`.repeat(31)}"A"...`, + }, + ], + }), + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: JSON.stringify({ foo: { bar: { qux: "qut" } } }), + }, + ], + }), + ], + }), + ]); +}); + +it("should take the limits from the config", async () => { + issue("1070"); + const { tests } = await runCypressInlineTest({ + "cypress/e2e/sample.cy.js": () => ` + it("foo", () => { + cy.wrap("A".repeat(100)); // should truncate string values + cy.wrap(Array(100).fill("A")); // should truncate objects + cy.wrap({ foo: { bar: { }, baz: "qux" } }) // should remove 'bar' that creates nesting level 3 but keep 'baz' + }); + `, + "cypress.config.js": ({ allureCypressModuleBasePath }) => ` + const { allureCypress } = require("${allureCypressModuleBasePath}/reporter.js"); + + module.exports = { + e2e: { + baseUrl: "https://allurereport.org", + viewportWidth: 1240, + setupNodeEvents: (on, config) => { + allureCypress(on, config, { + stepsFromCommands: { + maxArgumentLength: 25, + maxArgumentDepth: 2, + } + }); + + return config; + }, + }, + }; + `, + }); + + expect(tests).toEqual([ + expect.objectContaining({ + steps: [ + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: `${"A".repeat(25)}...`, + }, + ], + }), + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: `[${String.raw`"A",`.repeat(6)}...`, + }, + ], + }), + expect.objectContaining({ + parameters: [ + { + name: "Argument [0]", + value: JSON.stringify({ foo: { baz: "qux" } }), + }, + ], + }), + ], + }), + ]); +}); diff --git a/packages/allure-js-commons/src/sdk/index.ts b/packages/allure-js-commons/src/sdk/index.ts index 6139f8dfa..991a68fea 100644 --- a/packages/allure-js-commons/src/sdk/index.ts +++ b/packages/allure-js-commons/src/sdk/index.ts @@ -25,4 +25,5 @@ export { isPromise, hasLabel, stripAnsi, + serialize, } from "./utils.js"; diff --git a/packages/allure-js-commons/src/sdk/reporter/utils.ts b/packages/allure-js-commons/src/sdk/reporter/utils.ts index 38bd739c6..870905964 100644 --- a/packages/allure-js-commons/src/sdk/reporter/utils.ts +++ b/packages/allure-js-commons/src/sdk/reporter/utils.ts @@ -137,18 +137,6 @@ export const getRelativePath = (filepath: string) => { export const deepClone = (obj: T): T => JSON.parse(JSON.stringify(obj)); -export const serialize = (val: unknown): string => { - if (typeof val === "object" && !(val instanceof Map || val instanceof Set)) { - return JSON.stringify(val); - } - - if (val === undefined) { - return "undefined"; - } - - return (val as any).toString(); -}; - export const getSuiteLabels = (suites: readonly string[]): Label[] => { if (suites.length === 0) { return []; diff --git a/packages/allure-js-commons/src/sdk/types.ts b/packages/allure-js-commons/src/sdk/types.ts index c1a865b8a..d5d8521ee 100644 --- a/packages/allure-js-commons/src/sdk/types.ts +++ b/packages/allure-js-commons/src/sdk/types.ts @@ -110,3 +110,8 @@ export interface AllureResults { envInfo?: EnvironmentInfo; categories?: Category[]; } + +export type SerializeOptions = { + maxDepth?: number; + maxLength?: number; +}; diff --git a/packages/allure-js-commons/src/sdk/utils.ts b/packages/allure-js-commons/src/sdk/utils.ts index 595087c87..86a04d333 100644 --- a/packages/allure-js-commons/src/sdk/utils.ts +++ b/packages/allure-js-commons/src/sdk/utils.ts @@ -1,6 +1,6 @@ import type { FixtureResult, Label, StatusDetails, StepResult, TestResult } from "../model.js"; import { LabelName, Status } from "../model.js"; -import type { RuntimeMessage } from "./types.js"; +import type { RuntimeMessage, SerializeOptions } from "./types.js"; export const getStatusFromError = (error: Error): Status => { switch (true) { @@ -141,3 +141,47 @@ export const getUnfinishedStepsMessages = (messages: RuntimeMessage[]) => { export const isPromise = (obj: any): obj is PromiseLike => !!obj && (typeof obj === "object" || typeof obj === "function") && typeof obj.then === "function"; + +export const serialize = (value: any, { maxDepth = 0, maxLength = 0 }: SerializeOptions = {}): string => + limitString( + typeof value === "object" ? JSON.stringify(value, createSerializeReplacer(maxDepth)) : String(value), + maxLength, + ); + +const createSerializeReplacer = (maxDepth: number) => { + const parents: any[] = []; + return function (this: any, _: string, value: unknown) { + if (typeof value !== "object" || value === null) { + return value; + } + + while (parents.length > 0 && !Object.is(parents.at(-1), this)) { + parents.pop(); + } + + if ((maxDepth && parents.length >= maxDepth) || parents.includes(value)) { + return undefined; + } + + parents.push(value); + + return value instanceof Map + ? excludeCircularRefsFromMap(parents, value) + : value instanceof Set + ? excludeCircularRefsFromSet(parents, value) + : value; + }; +}; + +const excludeCircularRefsFromMap = (parents: any[], map: Map) => { + return Array.from(map) + .filter(([k]) => !parents.includes(k)) + .map(([k, v]) => [k, parents.includes(v) ? undefined : v]); +}; + +const excludeCircularRefsFromSet = (parents: any[], set: Set) => { + return Array.from(set).map((v) => (parents.includes(v) ? undefined : v)); +}; + +const limitString = (value: string, maxLength: number) => + maxLength && value.length > maxLength ? `${value.substring(0, maxLength)}...` : value; diff --git a/packages/allure-js-commons/test/sdk/reporter/utils.spec.ts b/packages/allure-js-commons/test/sdk/reporter/utils.spec.ts index c7a3893ec..0dc63b072 100644 --- a/packages/allure-js-commons/test/sdk/reporter/utils.spec.ts +++ b/packages/allure-js-commons/test/sdk/reporter/utils.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { LabelName } from "../../../src/model.js"; -import { getSuiteLabels, serialize } from "../../../src/sdk/reporter/utils.js"; +import { getSuiteLabels } from "../../../src/sdk/reporter/utils.js"; describe("getSuiteLabels", () => { describe("with empty suites", () => { @@ -54,58 +54,3 @@ describe("getSuiteLabels", () => { }); }); }); - -describe("serialize", () => { - describe("with object", () => { - it("returns JSON string", () => { - // eslint-disable-next-line @stylistic/quotes - expect(serialize({ foo: "bar" })).toBe('{"foo":"bar"}'); - }); - }); - - describe("with array", () => { - it("returns JSON string", () => { - // eslint-disable-next-line @stylistic/quotes - expect(serialize(["foo", "bar"])).toBe('["foo","bar"]'); - }); - }); - - describe("with map", () => { - it("returns JSON string", () => { - expect(serialize(new Map([["foo", "bar"]]))).toBe("[object Map]"); - }); - }); - - describe("with set", () => { - it("returns JSON string", () => { - expect(serialize(new Set(["foo", "bar"]))).toBe("[object Set]"); - }); - }); - - describe("with undefined", () => { - it("returns undefined string", () => { - expect(serialize(undefined)).toBe("undefined"); - }); - }); - - describe("with null", () => { - it("returns null string", () => { - expect(serialize(null)).toBe("null"); - }); - }); - - describe("with function", () => { - it("returns function string", () => { - expect(serialize(() => {})).toBe("() => {\n }"); - }); - }); - - describe("with primitives", () => { - it("returns stringified value", () => { - // eslint-disable-next-line @stylistic/quotes - expect(serialize("foo")).toBe("foo"); - expect(serialize(123)).toBe("123"); - expect(serialize(true)).toBe("true"); - }); - }); -}); diff --git a/packages/allure-js-commons/test/sdk/utils.spec.ts b/packages/allure-js-commons/test/sdk/utils.spec.ts index 04961565c..38e873620 100644 --- a/packages/allure-js-commons/test/sdk/utils.spec.ts +++ b/packages/allure-js-commons/test/sdk/utils.spec.ts @@ -8,6 +8,7 @@ import { getStatusFromError, isAnyStepFailed, isMetadataTag, + serialize, } from "../../src/sdk/utils.js"; type Executable = StepResult | TestResult | FixtureResult; @@ -266,3 +267,174 @@ describe("isMetadataTag", () => { expect(isMetadataTag("@allure.label.x=y")).toBeTruthy(); }); }); + +describe("serialize", () => { + describe("called with a primitive", () => { + describe("undefined", () => { + it("should return the 'undefined' constant", () => { + expect(serialize(undefined)).toBe("undefined"); + }); + }); + + describe("null", () => { + it("should return the 'null' constant", () => { + expect(serialize(null)).toBe("null"); + }); + }); + + describe("symbol", () => { + it("should return the same value as .toString()", () => { + const symbol = Symbol("foo"); + expect(serialize(symbol)).toBe(symbol.toString()); + }); + }); + + describe("number", () => { + it("should return the text of the number", () => { + expect(serialize(1)).toBe("1"); + expect(serialize(1.5)).toBe("1.5"); + }); + }); + + describe("bigint", () => { + it("should return the text of the bit int", () => { + expect(serialize(BigInt(1000000000000000) * BigInt(1000000000000000))).toBe("1000000000000000000000000000000"); + }); + }); + + describe("boolean", () => { + it("should return the 'true' or 'false' constant", () => { + expect(serialize(true)).toBe("true"); + expect(serialize(false)).toBe("false"); + }); + }); + + describe("string", () => { + it("should return the string itself", () => { + expect(serialize("")).toBe(""); + expect(serialize("foo")).toBe("foo"); + }); + + it("should limit the maximum length of the serialized string", () => { + expect(serialize("foobar".repeat(2), { maxLength: 5 })).toBe("fooba..."); + }); + }); + }); + + describe("called with an object", () => { + it("should return the same serialized object as JSON.stringify", () => { + expect(serialize({})).toBe(JSON.stringify({})); + expect(serialize({ foo: "bar" })).toBe(JSON.stringify({ foo: "bar" })); + expect(serialize({ foo: "bar", baz: "qux" })).toBe(JSON.stringify({ foo: "bar", baz: "qux" })); + expect(serialize({ foo: { bar: "qux" } })).toBe(JSON.stringify({ foo: { bar: "qux" } })); + }); + + it("should preclude circular references", () => { + const obj1: any = {}; + obj1.ref = obj1; // a reference to the direct parent + expect(serialize(obj1)).toBe(JSON.stringify({})); + + const obj2: any = {}; + obj2.ref = { ref: obj2 }; // a reference to the parent of the parent + expect(serialize(obj2)).toBe(JSON.stringify({ ref: {} })); + + const sharedObject = { baz: "qux" }; + const obj3 = { foo: sharedObject, bar: sharedObject }; // diamond-shaped refs; no cycles though + expect(serialize(obj3)).toBe(JSON.stringify({ foo: { baz: "qux" }, bar: { baz: "qux" } })); + + const obj4: any = { foo: "Lorem", bar: { baz: "Ipsum" } }; + obj4.bar.ref = obj4.bar; // A reference to the parent, but the node also has another property + expect(serialize(obj4)).toBe(JSON.stringify({ foo: "Lorem", bar: { baz: "Ipsum" } })); + + // Arrays adhere to the same rules (but 'null' is inserted for each excluded circular reference; + // otherwise the result may be confusing) + const obj5: any[] = []; + obj5.push(obj5); + expect(serialize(obj5)).toBe(JSON.stringify([null])); + + const obj6: any = [1, "Lorem", ["Ipsum"]]; + obj6[2].push(obj6); + expect(serialize(obj6)).toBe(JSON.stringify([1, "Lorem", ["Ipsum", null]])); + + const obj7: Map = new Map(); + obj7.set(1, obj7); + expect(serialize(obj7)).toBe(JSON.stringify([[1, null]])); + + const obj8: Set = new Set(); + obj8.add(obj8); + expect(serialize(obj8)).toBe(JSON.stringify([null])); + }); + + it("should limit the maximum size of the serialized object", () => { + expect(serialize(Array(1000).fill(1), { maxLength: 5 })).toBe("[1,1,..."); + }); + + it("should limit the maximum nesting level of the object", () => { + expect(serialize({ foo: 1, bar: { baz: {}, qux: 2 } }, { maxDepth: 2 })).toBe( + JSON.stringify({ + // this is nesting level 1 + foo: 1, + bar: { + // this is nesting level 2 + // only primitives are included + qux: 2, + }, + }), + ); + }); + + it("should replace nested maps and sets with arrays", () => { + expect( + serialize({ + foo: new Map([ + [1, "a"], + [2, "b"], + ]), + bar: new Set([1, 2]), + }), + ).toBe( + JSON.stringify({ + foo: [ + [1, "a"], + [2, "b"], + ], + bar: [1, 2], + }), + ); + }); + + describe("of type Array", () => { + it("should return the same serialized array as JSON.stringify", () => { + expect(serialize([])).toBe(JSON.stringify([])); + expect(serialize([1])).toBe(JSON.stringify([1])); + expect(serialize([1, "foo"])).toBe(JSON.stringify([1, "foo"])); + expect(serialize([1, "foo", [2, { bar: "baz" }]])).toBe(JSON.stringify([1, "foo", [2, { bar: "baz" }]])); + }); + }); + + describe("of type Map", () => { + it("should return array of the key-value pairs", () => { + expect(serialize(new Map())).toBe("[]"); + expect(serialize(new Map([]))).toBe("[]"); + expect(serialize(new Map([["foo", "bar"]]))).toBe(String.raw`[["foo","bar"]]`); + expect( + serialize( + new Map([ + [1, "foo"], + [2, "bar"], + ]), + ), + ).toBe(String.raw`[[1,"foo"],[2,"bar"]]`); + }); + }); + + describe("of type Set", () => { + it("should return array of the set elements", () => { + expect(serialize(new Set())).toBe("[]"); + expect(serialize(new Set([]))).toBe("[]"); + expect(serialize(new Set([1]))).toBe("[1]"); + expect(serialize(new Set([1, "foo", 2]))).toBe(String.raw`[1,"foo",2]`); + }); + }); + }); +}); diff --git a/packages/allure-mocha/src/legacy.ts b/packages/allure-mocha/src/legacy.ts index 13a64f562..c4d6a8901 100644 --- a/packages/allure-mocha/src/legacy.ts +++ b/packages/allure-mocha/src/legacy.ts @@ -3,7 +3,7 @@ import type { ContentType, ParameterOptions } from "allure-js-commons"; import { Status } from "allure-js-commons"; import type { Category } from "allure-js-commons/sdk"; import { getStatusFromError, isPromise } from "allure-js-commons/sdk"; -import { serialize } from "allure-js-commons/sdk/reporter"; +import { serialize } from "allure-js-commons/sdk"; import { getLegacyApiRuntime } from "./legacyUtils.js"; interface StepInterface {