Skip to content

Commit 4c50833

Browse files
committed
Use @cucumber/compatibility-kit to verify impl.
This uses `@cucumber/compatibility-kit` to verify the implementation of messages. In addition, some minor changes has been made to make the tests pass.
1 parent 8155928 commit 4c50833

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+917
-129
lines changed

.mocharc.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ All notable changes to this project will be documented in this file.
1616

1717
- Implement `link`, as seen in cucumber-js.
1818

19+
- Minor changes to the messages report, to ensure compatibility with `cucumber-js`.
20+
1921
## v23.0.0
2022

2123
Breaking changes:

compatibility/cck_spec.ts

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import fs from "node:fs/promises";
2+
3+
import path from "node:path";
4+
5+
import assert from "node:assert/strict";
6+
7+
import childProcess from "node:child_process";
8+
9+
import * as messages from "@cucumber/messages";
10+
11+
import * as glob from "glob";
12+
13+
import { stringToNdJson } from "../features/support/helpers";
14+
15+
/**
16+
* This file is heavily inspired by the cucumber-js' counterpart.
17+
*
18+
* @see https://github.com/cucumber/cucumber-js/blob/v10.8.0/compatibility/cck_spec.ts
19+
*/
20+
21+
const IS_WIN = process.platform === "win32";
22+
const PROJECT_PATH = path.join(__dirname, "..");
23+
const CCK_FEATURES_PATH = "node_modules/@cucumber/compatibility-kit/features";
24+
const CCK_IMPLEMENTATIONS_PATH = "compatibility/step_definitions";
25+
26+
// Shamelessly copied form https://github.com/cucumber/cucumber-js/blob/v10.8.0/features/support/formatter_output_helpers.ts#L100-L122
27+
const ignorableKeys = [
28+
"meta",
29+
// sources
30+
"uri",
31+
"line",
32+
"column",
33+
// ids
34+
"astNodeId",
35+
"astNodeIds",
36+
"hookId",
37+
"id",
38+
"pickleId",
39+
"pickleStepId",
40+
"stepDefinitionIds",
41+
"testCaseId",
42+
"testCaseStartedId",
43+
"testStepId",
44+
"testRunStartedId",
45+
// time
46+
"nanos",
47+
"seconds",
48+
// errors
49+
"message",
50+
"stackTrace",
51+
];
52+
53+
function isObject(object: any): object is object {
54+
return typeof object === "object" && object != null;
55+
}
56+
57+
function hasOwnProperty<X extends object, Y extends PropertyKey>(
58+
obj: X,
59+
prop: Y,
60+
): obj is X & Record<Y, unknown> {
61+
return Object.prototype.hasOwnProperty.call(obj, prop);
62+
}
63+
64+
export function* traverseTree(object: any): Generator<object, void, any> {
65+
if (!isObject(object)) {
66+
throw new Error(`Expected object, got ${typeof object}`);
67+
}
68+
69+
yield object;
70+
71+
for (const property of Object.values(object)) {
72+
if (isObject(property)) {
73+
yield* traverseTree(property);
74+
}
75+
}
76+
}
77+
78+
function normalizeMessage(message: messages.Envelope): messages.Envelope {
79+
for (const node of traverseTree(message as any)) {
80+
for (const ignorableKey of ignorableKeys) {
81+
if (hasOwnProperty(node, ignorableKey)) {
82+
delete node[ignorableKey];
83+
}
84+
}
85+
}
86+
87+
return message;
88+
}
89+
90+
describe("Cucumber Compatibility Kit", () => {
91+
const ndjsonFiles = glob.sync(`${CCK_FEATURES_PATH}/**/*.ndjson`);
92+
93+
for (const ndjsonFile of ndjsonFiles) {
94+
const suiteName = path.basename(path.dirname(ndjsonFile));
95+
96+
/**
97+
* Unknown parameter type will generate an exception outside of a Cypress test and halt all
98+
* execution. Thus, cucumber-js' behavior is tricky to mirror.
99+
*
100+
* Markdown is unsupported.
101+
*/
102+
switch (suiteName) {
103+
case "unknown-parameter-type":
104+
case "markdown":
105+
case "hooks-conditional":
106+
it.skip(`passes the cck suite for '${suiteName}'`);
107+
continue;
108+
}
109+
110+
if (process.env.CCK_ONLY != null && process.env.CCK_ONLY !== suiteName) {
111+
it.skip(`passes the cck suite for '${suiteName}'`);
112+
continue;
113+
}
114+
115+
it(`passes the cck suite for '${suiteName}'`, async () => {
116+
const tmpDir = path.join(PROJECT_PATH, "tmp", "compatibility", suiteName);
117+
118+
await fs.rm(tmpDir, { recursive: true, force: true });
119+
120+
await fs.mkdir(tmpDir, { recursive: true });
121+
122+
await fs.writeFile(
123+
path.join(tmpDir, "cypress.config.js"),
124+
`
125+
const { defineConfig } = require("cypress");
126+
const setupNodeEvents = require("./setupNodeEvents.js");
127+
128+
module.exports = defineConfig({
129+
e2e: {
130+
specPattern: "cypress/e2e/**/*.feature",
131+
video: false,
132+
supportFile: false,
133+
screenshotOnRunFailure: false,
134+
setupNodeEvents
135+
}
136+
});
137+
`,
138+
);
139+
140+
await fs.writeFile(
141+
path.join(tmpDir, ".cypress-cucumber-preprocessorrc"),
142+
`
143+
{
144+
"messages": {
145+
"enabled": true
146+
}
147+
}
148+
`,
149+
);
150+
151+
await fs.writeFile(
152+
path.join(tmpDir, "setupNodeEvents.js"),
153+
`
154+
const { addCucumberPreprocessorPlugin } = require("@badeball/cypress-cucumber-preprocessor");
155+
const { createEsbuildPlugin } = require("@badeball/cypress-cucumber-preprocessor/esbuild");
156+
const createBundler = require("@bahmutov/cypress-esbuild-preprocessor");
157+
158+
module.exports = async function setupNodeEvents(on, config) {
159+
await addCucumberPreprocessorPlugin(on, config);
160+
161+
on(
162+
"file:preprocessor",
163+
createBundler({
164+
plugins: [createEsbuildPlugin(config)],
165+
})
166+
);
167+
168+
return config;
169+
};
170+
`,
171+
);
172+
173+
await fs.mkdir(path.join(tmpDir, "node_modules", "@badeball"), {
174+
recursive: true,
175+
});
176+
177+
await fs.symlink(
178+
PROJECT_PATH,
179+
path.join(
180+
tmpDir,
181+
"node_modules",
182+
"@badeball",
183+
"cypress-cucumber-preprocessor",
184+
),
185+
"dir",
186+
);
187+
188+
await fs.mkdir(path.join(tmpDir, "cypress", "e2e"), { recursive: true });
189+
190+
await fs.copyFile(
191+
path.join(CCK_FEATURES_PATH, suiteName, `${suiteName}.feature`),
192+
path.join(tmpDir, "cypress", "e2e", `${suiteName}.feature`),
193+
);
194+
195+
if (suiteName === "hooks-attachment") {
196+
await fs.copyFile(
197+
path.join(CCK_FEATURES_PATH, suiteName, "cucumber.svg"),
198+
path.join(tmpDir, "cucumber.svg"),
199+
);
200+
} else if (suiteName === "examples-tables-attachment") {
201+
const files = ["cucumber.jpeg", "cucumber.png"];
202+
203+
for (const file of files) {
204+
await fs.copyFile(
205+
path.join(CCK_FEATURES_PATH, suiteName, file),
206+
path.join(tmpDir, file),
207+
);
208+
}
209+
} else if (suiteName === "attachments") {
210+
const files = ["cucumber.jpeg", "cucumber.png", "document.pdf"];
211+
212+
for (const file of files) {
213+
await fs.copyFile(
214+
path.join(CCK_FEATURES_PATH, suiteName, file),
215+
path.join(tmpDir, file),
216+
);
217+
}
218+
}
219+
220+
await fs.mkdir(
221+
path.join(tmpDir, "cypress", "support", "step_definitions"),
222+
{ recursive: true },
223+
);
224+
225+
await fs.copyFile(
226+
path.join(PROJECT_PATH, CCK_IMPLEMENTATIONS_PATH, `${suiteName}.ts`),
227+
path.join(
228+
tmpDir,
229+
"cypress",
230+
"support",
231+
"step_definitions",
232+
`${suiteName}.ts`,
233+
),
234+
);
235+
236+
const args = ["run"];
237+
238+
if (suiteName === "retry") {
239+
args.push("-c", "retries=2");
240+
}
241+
242+
const child = childProcess.spawn(
243+
path.join(
244+
PROJECT_PATH,
245+
"node_modules",
246+
".bin",
247+
IS_WIN ? "cypress.cmd" : "cypress",
248+
),
249+
args,
250+
{
251+
stdio: ["ignore", "pipe", "pipe"],
252+
cwd: tmpDir,
253+
// https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
254+
shell: IS_WIN,
255+
},
256+
);
257+
258+
if (process.env.DEBUG) {
259+
child.stdout.pipe(process.stdout);
260+
child.stderr.pipe(process.stderr);
261+
}
262+
263+
await new Promise((resolve) => {
264+
child.on("close", resolve);
265+
});
266+
267+
const actualMessages = stringToNdJson(
268+
(
269+
await fs.readFile(path.join(tmpDir, "cucumber-messages.ndjson"))
270+
).toString(),
271+
).map(normalizeMessage);
272+
273+
const expectedMessages = stringToNdJson(
274+
(await fs.readFile(ndjsonFile)).toString(),
275+
).map(normalizeMessage);
276+
277+
if (suiteName === "pending") {
278+
/**
279+
* We can't control Cypress exit code without failing a test, thus is cucumber-js behavior
280+
* difficult to mimic.
281+
*/
282+
actualMessages.forEach((message) => {
283+
if (message.testRunFinished) {
284+
message.testRunFinished.success = false;
285+
}
286+
});
287+
} else if (suiteName === "hooks") {
288+
/**
289+
* Lack of try-catch in Cypress makes it difficult to mirror cucumber-js behavior in terms
290+
* of hooks, for which exceptions doesn't halt execution.
291+
*/
292+
actualMessages.forEach((message) => {
293+
if (
294+
message.testStepFinished?.testStepResult.status ===
295+
messages.TestStepResultStatus.SKIPPED
296+
) {
297+
message.testStepFinished.testStepResult.status =
298+
messages.TestStepResultStatus.PASSED;
299+
}
300+
});
301+
}
302+
303+
assert.deepEqual(actualMessages, expectedMessages);
304+
});
305+
}
306+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
When,
3+
attach,
4+
log,
5+
link,
6+
} from "@badeball/cypress-cucumber-preprocessor";
7+
8+
When(
9+
"the string {string} is attached as {string}",
10+
function (text: string, mediaType: string) {
11+
attach(text, mediaType);
12+
},
13+
);
14+
15+
When("the string {string} is logged", function (text: string) {
16+
log(text);
17+
});
18+
19+
When("text with ANSI escapes is logged", function () {
20+
log(
21+
"This displays a \x1b[31mr\x1b[0m\x1b[91ma\x1b[0m\x1b[33mi\x1b[0m\x1b[32mn\x1b[0m\x1b[34mb\x1b[0m\x1b[95mo\x1b[0m\x1b[35mw\x1b[0m",
22+
);
23+
});
24+
25+
When(
26+
"the following string is attached as {string}:",
27+
function (mediaType: string, text: string) {
28+
attach(text, mediaType);
29+
},
30+
);
31+
32+
When(
33+
"an array with {int} bytes is attached as {string}",
34+
function (size: number, mediaType: string) {
35+
const data = [...Array(size).keys()];
36+
const buffer = new Uint8Array(data).buffer;
37+
attach(buffer, mediaType);
38+
},
39+
);
40+
41+
When("a JPEG image is attached", function () {
42+
cy.readFile("cucumber.jpeg", "base64").then((file) =>
43+
attach(file, "base64:image/jpeg"),
44+
);
45+
});
46+
47+
When("a PNG image is attached", function () {
48+
cy.readFile("cucumber.png", "base64").then((file) =>
49+
attach(file, "base64:image/png"),
50+
);
51+
});
52+
53+
When("a PDF document is attached and renamed", function () {
54+
cy.readFile("document.pdf", "base64").then((file) =>
55+
attach(file, {
56+
mediaType: "base64:application/pdf",
57+
fileName: "renamed.pdf",
58+
}),
59+
);
60+
});
61+
62+
When("a link to {string} is attached", function (uri: string) {
63+
link(uri);
64+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Given } from "@badeball/cypress-cucumber-preprocessor";
2+
3+
Given(
4+
"I have {int} <![CDATA[cukes]]> in my belly",
5+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6+
function (cukeCount: number) {
7+
// no-op
8+
},
9+
);

0 commit comments

Comments
 (0)