playwright-cucumber-ts-steps 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -39
- package/{dist → lib}/actions/clickSteps.js +10 -3
- package/{dist → lib}/actions/debugSteps.js +2 -5
- package/{dist → lib}/actions/elementFindSteps.js +15 -18
- package/lib/actions/fillFormSteps.js +130 -0
- package/{dist → lib}/actions/inputSteps.js +9 -76
- package/{dist → lib}/actions/interceptionSteps.js +8 -0
- package/{dist → lib}/actions/miscSteps.js +60 -12
- package/{dist → lib}/actions/storageSteps.js +26 -4
- package/{dist → lib}/assertions/buttonAndTextVisibilitySteps.js +3 -23
- package/{dist → lib}/assertions/elementSteps.js +7 -1
- package/{dist → lib}/assertions/locationSteps.js +16 -0
- package/{dist → lib}/assertions/semanticSteps.js +16 -3
- package/{dist → lib}/assertions/visualSteps.js +9 -5
- package/lib/custom_setups/loginHooks.js +113 -0
- package/lib/helpers/hooks.js +210 -0
- package/{dist → lib}/helpers/utils/index.d.ts +1 -0
- package/{dist → lib}/helpers/utils/index.js +1 -0
- package/{dist → lib}/helpers/utils/optionsUtils.d.ts +5 -0
- package/{dist → lib}/helpers/utils/optionsUtils.js +12 -3
- package/{dist → lib}/helpers/utils/resolveUtils.d.ts +2 -0
- package/{dist → lib}/helpers/utils/resolveUtils.js +13 -3
- package/lib/helpers/utils/sessionUtils.d.ts +3 -0
- package/lib/helpers/utils/sessionUtils.js +40 -0
- package/{dist → lib}/helpers/world.d.ts +13 -0
- package/{dist → lib}/helpers/world.js +17 -10
- package/lib/iframes/frames.d.ts +1 -0
- package/{dist → lib}/index.d.ts +1 -1
- package/{dist → lib}/index.js +1 -1
- package/{dist → lib}/register.js +1 -5
- package/package.json +45 -23
- package/dist/custom_setups/globalLogin.d.ts +0 -2
- package/dist/custom_setups/globalLogin.js +0 -25
- package/dist/custom_setups/loginHooks.js +0 -141
- package/dist/helpers/hooks.js +0 -184
- package/{dist → lib}/actions/clickSteps.d.ts +0 -0
- package/{dist → lib}/actions/cookieSteps.d.ts +0 -0
- package/{dist → lib}/actions/cookieSteps.js +0 -0
- package/{dist → lib}/actions/debugSteps.d.ts +0 -0
- package/{dist → lib}/actions/elementFindSteps.d.ts +0 -0
- package/{dist/actions/inputSteps.d.ts → lib/actions/fillFormSteps.d.ts} +0 -0
- package/{dist/actions/interceptionSteps.d.ts → lib/actions/inputSteps.d.ts} +0 -0
- package/{dist/actions/miscSteps.d.ts → lib/actions/interceptionSteps.d.ts} +0 -0
- package/{dist/actions/mouseSteps.d.ts → lib/actions/miscSteps.d.ts} +0 -0
- package/{dist/actions/scrollSteps.d.ts → lib/actions/mouseSteps.d.ts} +0 -0
- package/{dist → lib}/actions/mouseSteps.js +0 -0
- package/{dist/actions/storageSteps.d.ts → lib/actions/scrollSteps.d.ts} +0 -0
- package/{dist → lib}/actions/scrollSteps.js +0 -0
- package/{dist/assertions → lib/actions}/storageSteps.d.ts +0 -0
- package/{dist → lib}/assertions/buttonAndTextVisibilitySteps.d.ts +0 -0
- package/{dist → lib}/assertions/cookieSteps.d.ts +0 -0
- package/{dist → lib}/assertions/cookieSteps.js +0 -0
- package/{dist → lib}/assertions/elementSteps.d.ts +0 -0
- package/{dist → lib}/assertions/formInputSteps.d.ts +0 -0
- package/{dist → lib}/assertions/formInputSteps.js +0 -0
- package/{dist → lib}/assertions/interceptionRequestsSteps.d.ts +0 -0
- package/{dist → lib}/assertions/interceptionRequestsSteps.js +0 -0
- package/{dist → lib}/assertions/locationSteps.d.ts +0 -0
- package/{dist → lib}/assertions/roleTestIdSteps.d.ts +0 -0
- package/{dist → lib}/assertions/roleTestIdSteps.js +0 -0
- package/{dist → lib}/assertions/semanticSteps.d.ts +0 -0
- package/{dist/assertions/visualSteps.d.ts → lib/assertions/storageSteps.d.ts} +0 -0
- package/{dist → lib}/assertions/storageSteps.js +1 -1
- /package/{dist/custom_setups/loginHooks.d.ts → lib/assertions/visualSteps.d.ts} +0 -0
- /package/{dist/helpers/hooks.d.ts → lib/custom_setups/loginHooks.d.ts} +0 -0
- /package/{dist → lib}/helpers/checkPeerDeps.d.ts +0 -0
- /package/{dist → lib}/helpers/checkPeerDeps.js +0 -0
- /package/{dist → lib}/helpers/compareSnapshots.d.ts +0 -0
- /package/{dist → lib}/helpers/compareSnapshots.js +0 -0
- /package/{dist/iframes/frames.d.ts → lib/helpers/hooks.d.ts} +0 -0
- /package/{dist → lib}/helpers/utils/fakerUtils.d.ts +0 -0
- /package/{dist → lib}/helpers/utils/fakerUtils.js +0 -0
- /package/{dist → lib}/iframes/frames.js +0 -0
- /package/{dist → lib}/register.d.ts +0 -0
|
@@ -4,8 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const cucumber_1 = require("@cucumber/cucumber");
|
|
7
|
+
const test_1 = require("@playwright/test");
|
|
7
8
|
const dayjs_1 = __importDefault(require("dayjs"));
|
|
8
9
|
const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
10
|
+
const resolveUtils_1 = require("../helpers/utils/resolveUtils");
|
|
9
11
|
//
|
|
10
12
|
// Timers
|
|
11
13
|
//
|
|
@@ -38,16 +40,18 @@ const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
|
38
40
|
(0, cucumber_1.When)(/^I wait (\d+) second[s]?$/, async function (seconds) {
|
|
39
41
|
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
40
42
|
});
|
|
41
|
-
(0, cucumber_1.When)(
|
|
43
|
+
(0, cucumber_1.When)(/^I wait (\d+) millisecond[s]?$/, async function (ms) {
|
|
42
44
|
await new Promise((res) => setTimeout(res, ms));
|
|
43
45
|
});
|
|
46
|
+
(0, cucumber_1.When)("I set step timeout to {int} ms", function (timeoutMs) {
|
|
47
|
+
(0, cucumber_1.setDefaultTimeout)(timeoutMs);
|
|
48
|
+
this.log?.(`⏱️ Timeout set to ${timeoutMs}ms`);
|
|
49
|
+
});
|
|
44
50
|
//
|
|
45
51
|
// Events
|
|
46
52
|
//
|
|
47
53
|
(0, cucumber_1.When)(/^I trigger "(.*)" event on "([^"]+)"$/, async function (eventType, selector) {
|
|
48
|
-
await this.page
|
|
49
|
-
.locator(selector)
|
|
50
|
-
.evaluate((el, type) => {
|
|
54
|
+
await this.page.locator(selector).evaluate((el, type) => {
|
|
51
55
|
const event = new Event(type, {
|
|
52
56
|
bubbles: true,
|
|
53
57
|
cancelable: true,
|
|
@@ -73,8 +77,8 @@ const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
|
73
77
|
//
|
|
74
78
|
// Debugging / Logging
|
|
75
79
|
//
|
|
76
|
-
(0, cucumber_1.When)(
|
|
77
|
-
|
|
80
|
+
(0, cucumber_1.When)("I log {string}", async function (message) {
|
|
81
|
+
this.log(message);
|
|
78
82
|
});
|
|
79
83
|
(0, cucumber_1.When)(/^I debug$/, async function () {
|
|
80
84
|
debugger;
|
|
@@ -163,9 +167,7 @@ const validUnits = [
|
|
|
163
167
|
//
|
|
164
168
|
(0, cucumber_1.When)("I switch to iframe with selector {string}", async function (selector) {
|
|
165
169
|
const frameLocator = this.page.frameLocator(selector);
|
|
166
|
-
await frameLocator
|
|
167
|
-
.locator("body")
|
|
168
|
-
.waitFor({ state: "visible", timeout: 10000 });
|
|
170
|
+
await frameLocator.locator("body").waitFor({ state: "visible", timeout: 10000 });
|
|
169
171
|
this.frame = frameLocator;
|
|
170
172
|
this.log?.(`🪟 Switched to iframe: ${selector}`);
|
|
171
173
|
});
|
|
@@ -197,9 +199,7 @@ function toOrdinal(n) {
|
|
|
197
199
|
async function getReadableLabel(el) {
|
|
198
200
|
try {
|
|
199
201
|
const tag = await el.evaluate((el) => el.tagName.toLowerCase());
|
|
200
|
-
return tag === "input"
|
|
201
|
-
? await el.inputValue()
|
|
202
|
-
: (await el.innerText()).trim();
|
|
202
|
+
return tag === "input" ? await el.inputValue() : (await el.innerText()).trim();
|
|
203
203
|
}
|
|
204
204
|
catch {
|
|
205
205
|
return "(unknown)";
|
|
@@ -270,3 +270,51 @@ const locatorActions = {
|
|
|
270
270
|
await this.element.press(key);
|
|
271
271
|
this.log?.(`🎹 Pressed {${key}}`);
|
|
272
272
|
});
|
|
273
|
+
(0, cucumber_1.When)(/^I set viewport to "([^"]+)"(?: and "([^"]+)")?$/, async function (deviceInput, orientation) {
|
|
274
|
+
const normalizedDevice = (0, resolveUtils_1.normalizeDeviceName)(deviceInput);
|
|
275
|
+
if (!normalizedDevice) {
|
|
276
|
+
throw new Error(`🚫 Unknown device: "${deviceInput}"`);
|
|
277
|
+
}
|
|
278
|
+
const baseDevice = test_1.devices[normalizedDevice];
|
|
279
|
+
if (!baseDevice) {
|
|
280
|
+
throw new Error(`🚫 Device not found: "${normalizedDevice}"`);
|
|
281
|
+
}
|
|
282
|
+
const isLandscape = orientation?.toLowerCase() === "landscape";
|
|
283
|
+
const deviceSettings = isLandscape
|
|
284
|
+
? baseDevice.landscape
|
|
285
|
+
? baseDevice.landscape
|
|
286
|
+
: {
|
|
287
|
+
...baseDevice,
|
|
288
|
+
isMobile: true,
|
|
289
|
+
viewport: { ...baseDevice.viewport, isLandscape: true },
|
|
290
|
+
}
|
|
291
|
+
: baseDevice;
|
|
292
|
+
// Close current context if needed
|
|
293
|
+
if (this.context) {
|
|
294
|
+
await this.context.close();
|
|
295
|
+
}
|
|
296
|
+
this.context = await this.browser.newContext(deviceSettings);
|
|
297
|
+
this.page = await this.context.newPage();
|
|
298
|
+
this.log?.(`📱 Set viewport to ${normalizedDevice}${isLandscape ? " in landscape" : ""}`);
|
|
299
|
+
});
|
|
300
|
+
(0, cucumber_1.When)("I set viewport to {int}px by {int}px", async function (width, height) {
|
|
301
|
+
// Close existing context
|
|
302
|
+
if (this.context) {
|
|
303
|
+
await this.context.close();
|
|
304
|
+
}
|
|
305
|
+
// Recreate new context with the desired viewport
|
|
306
|
+
this.context = await this.browser.newContext({
|
|
307
|
+
viewport: { width, height },
|
|
308
|
+
});
|
|
309
|
+
this.page = await this.context.newPage();
|
|
310
|
+
this.log?.(`🖥️ Set viewport to ${width}x${height}`);
|
|
311
|
+
});
|
|
312
|
+
// Dynamic Playwright Config Setters (for page-only config)
|
|
313
|
+
(0, cucumber_1.When)('I set Playwright config "{word}" to {string}', async function (key, value) {
|
|
314
|
+
this.page[key] = value;
|
|
315
|
+
});
|
|
316
|
+
(0, cucumber_1.When)("I set Playwright config", async function (table) {
|
|
317
|
+
for (const [key, value] of table.rows()) {
|
|
318
|
+
this.page[key] = value;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
// e2e/step_definitions/common/actions/storageSteps.ts
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
4
9
|
const cucumber_1 = require("@cucumber/cucumber");
|
|
5
10
|
(0, cucumber_1.When)("I clear all local storage", async function () {
|
|
6
11
|
await this.page.evaluate(() => localStorage.clear());
|
|
@@ -32,15 +37,15 @@ const cucumber_1 = require("@cucumber/cucumber");
|
|
|
32
37
|
(0, cucumber_1.When)("I set local storage item {string} to {string}", async function (key, value) {
|
|
33
38
|
await this.page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value]);
|
|
34
39
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
(0, cucumber_1.When)("I set session storage item {string} to {string}", async function (key, value) {
|
|
41
|
+
await this.page.evaluate(([k, v]) => sessionStorage.setItem(k, v), [key, value]);
|
|
42
|
+
});
|
|
38
43
|
(0, cucumber_1.When)("I clear local storage", async function () {
|
|
39
44
|
await this.page.evaluate(() => localStorage.clear());
|
|
40
45
|
});
|
|
41
46
|
(0, cucumber_1.When)("I store input text as {string}", async function (alias) {
|
|
42
47
|
const activeElementHandle = await this.page.evaluateHandle(() => document.activeElement);
|
|
43
|
-
const tagName = await activeElementHandle.evaluate((el) => el ? el.tagName.toLowerCase() : "");
|
|
48
|
+
const tagName = await activeElementHandle.evaluate((el) => (el ? el.tagName.toLowerCase() : ""));
|
|
44
49
|
if (tagName !== "input" && tagName !== "textarea") {
|
|
45
50
|
throw new Error(`Active element is not an input or textarea (found: ${tagName})`);
|
|
46
51
|
}
|
|
@@ -48,3 +53,20 @@ const cucumber_1 = require("@cucumber/cucumber");
|
|
|
48
53
|
this.data[alias] = value;
|
|
49
54
|
this.log?.(`📥 Stored value from input as "${alias}": ${value}`);
|
|
50
55
|
});
|
|
56
|
+
(0, cucumber_1.When)("I clear session {string}", async function (fileName) {
|
|
57
|
+
const baseDir = this.parameters?.artifactDir || process.env.TEST_ARTIFACT_DIR || "test-artifacts";
|
|
58
|
+
const fullPath = path_1.default.resolve(baseDir, "auth-cookies", fileName);
|
|
59
|
+
try {
|
|
60
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
61
|
+
fs_1.default.unlinkSync(fullPath);
|
|
62
|
+
this.log?.(`🗑️ Session file deleted: ${fullPath}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.log?.(`ℹ️ Session file not found, nothing to delete: ${fullPath}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
this.log?.(`❌ Failed to delete session file: ${err.message}`);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -14,9 +14,7 @@ const fakerUtils_1 = require("../helpers/utils/fakerUtils");
|
|
|
14
14
|
* THEN: I see button "Submit"
|
|
15
15
|
*/
|
|
16
16
|
(0, cucumber_1.Then)(/^I see button "(.*)"$/, async function (rawText) {
|
|
17
|
-
let buttonText = rawText.startsWith("@")
|
|
18
|
-
? this.data[rawText.slice(1)]
|
|
19
|
-
: rawText;
|
|
17
|
+
let buttonText = rawText.startsWith("@") ? this.data[rawText.slice(1)] : rawText;
|
|
20
18
|
if (!buttonText) {
|
|
21
19
|
throw new Error(`No value found for alias: "${rawText}"`);
|
|
22
20
|
}
|
|
@@ -31,9 +29,7 @@ const fakerUtils_1 = require("../helpers/utils/fakerUtils");
|
|
|
31
29
|
* THEN: I do not see button "Cancel"
|
|
32
30
|
*/
|
|
33
31
|
(0, cucumber_1.Then)(/^I do not see button "(.*)"$/, async function (rawText) {
|
|
34
|
-
let buttonText = rawText.startsWith("@")
|
|
35
|
-
? this.data[rawText.slice(1)]
|
|
36
|
-
: rawText;
|
|
32
|
+
let buttonText = rawText.startsWith("@") ? this.data[rawText.slice(1)] : rawText;
|
|
37
33
|
if (!buttonText) {
|
|
38
34
|
throw new Error(`No value found for alias: "${rawText}"`);
|
|
39
35
|
}
|
|
@@ -59,20 +55,6 @@ const fakerUtils_1 = require("../helpers/utils/fakerUtils");
|
|
|
59
55
|
await locator.waitFor({ state: "visible", timeout: 5000 });
|
|
60
56
|
this.log?.(`✅ Verified text visible: ${expected}`);
|
|
61
57
|
});
|
|
62
|
-
/**
|
|
63
|
-
* THEN: I do not see text "Error"
|
|
64
|
-
*/
|
|
65
|
-
(0, cucumber_1.Then)("I do not see text {string}", async function (text) {
|
|
66
|
-
await this.page.waitForLoadState("networkidle");
|
|
67
|
-
const locator = this.page.locator(`:has-text("${text}")`);
|
|
68
|
-
const count = await locator.count();
|
|
69
|
-
for (let i = 0; i < count; i++) {
|
|
70
|
-
const item = locator.nth(i);
|
|
71
|
-
if (await item.isVisible()) {
|
|
72
|
-
throw new Error(`Text "${text}" is visible but should not be`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
58
|
/**
|
|
77
59
|
* THEN: I see visible text "Dashboard"
|
|
78
60
|
*/
|
|
@@ -151,9 +133,7 @@ const fakerUtils_1 = require("../helpers/utils/fakerUtils");
|
|
|
151
133
|
});
|
|
152
134
|
(0, cucumber_1.Then)("I see button {string} is disabled", async function (rawText) {
|
|
153
135
|
// Resolve alias
|
|
154
|
-
let buttonText = rawText.startsWith("@")
|
|
155
|
-
? this.data[rawText.slice(1)]
|
|
156
|
-
: rawText;
|
|
136
|
+
let buttonText = rawText.startsWith("@") ? this.data[rawText.slice(1)] : rawText;
|
|
157
137
|
if (!buttonText) {
|
|
158
138
|
throw new Error(`No value found for alias: "${rawText}"`);
|
|
159
139
|
}
|
|
@@ -59,6 +59,12 @@ const test_1 = require("@playwright/test");
|
|
|
59
59
|
const el = this.page.locator(selector);
|
|
60
60
|
await (0, test_1.expect)(el).toHaveAttribute(attribute, expected);
|
|
61
61
|
});
|
|
62
|
+
(0, cucumber_1.Then)('I see element attribute "{word}" equals {string}', async function (attr, expected) {
|
|
63
|
+
if (!this.element) {
|
|
64
|
+
throw new Error("No element is currently selected. Use a 'find' step before asserting.");
|
|
65
|
+
}
|
|
66
|
+
await (0, test_1.expect)(this.element).toHaveAttribute(attr, expected);
|
|
67
|
+
});
|
|
62
68
|
(0, cucumber_1.Then)("I see element has attribute {string}", async function (attr) {
|
|
63
69
|
if (!this.element)
|
|
64
70
|
throw new Error("No element in context");
|
|
@@ -66,7 +72,7 @@ const test_1 = require("@playwright/test");
|
|
|
66
72
|
if (value === null)
|
|
67
73
|
throw new Error(`Attribute "${attr}" not found`);
|
|
68
74
|
});
|
|
69
|
-
(0, cucumber_1.Then)(
|
|
75
|
+
(0, cucumber_1.Then)('I see element attribute "{word}" contains {string}', async function (attr, part) {
|
|
70
76
|
if (!this.element)
|
|
71
77
|
throw new Error("No element in context");
|
|
72
78
|
const value = await this.element.getAttribute(attr);
|
|
@@ -69,3 +69,19 @@ const test_1 = require("@playwright/test");
|
|
|
69
69
|
throw new Error(`Search does not contain "${part}". Got: "${search}"`);
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
|
+
(0, cucumber_1.Then)("I see location", async function (table) {
|
|
73
|
+
const location = await this.page.evaluate(() => ({
|
|
74
|
+
href: window.location.href,
|
|
75
|
+
origin: window.location.origin,
|
|
76
|
+
protocol: window.location.protocol,
|
|
77
|
+
host: window.location.host,
|
|
78
|
+
hostname: window.location.hostname,
|
|
79
|
+
port: window.location.port,
|
|
80
|
+
pathname: window.location.pathname,
|
|
81
|
+
search: window.location.search,
|
|
82
|
+
hash: window.location.hash,
|
|
83
|
+
}));
|
|
84
|
+
for (const [key, expected] of table.rows()) {
|
|
85
|
+
(0, test_1.expect)(location[key]).toBe(expected);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const cucumber_1 = require("@cucumber/cucumber");
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
4
6
|
//
|
|
5
7
|
// 🧠 HEADINGS
|
|
6
8
|
//
|
|
7
9
|
(0, cucumber_1.Then)("I see heading {string}", async function (text) {
|
|
8
|
-
const heading = await this.page
|
|
9
|
-
.locator("h1, h2, h3, h4, h5, h6", { hasText: text })
|
|
10
|
-
.first();
|
|
10
|
+
const heading = await this.page.locator("h1, h2, h3, h4, h5, h6", { hasText: text }).first();
|
|
11
11
|
if (!(await heading.isVisible())) {
|
|
12
12
|
throw new Error(`Heading "${text}" not found or not visible`);
|
|
13
13
|
}
|
|
@@ -52,3 +52,16 @@ const cucumber_1 = require("@cucumber/cucumber");
|
|
|
52
52
|
throw new Error(`Link "${text}" is visible but should not be`);
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
|
+
(0, cucumber_1.Then)("I count {int} element", async function (count) {
|
|
56
|
+
const locator = this.currentLocator ?? this.page.locator("*");
|
|
57
|
+
await (0, test_1.expect)(locator).toHaveCount(count);
|
|
58
|
+
});
|
|
59
|
+
//document title assertions
|
|
60
|
+
(0, cucumber_1.Then)("I see document title {string}", async function (expected, table) {
|
|
61
|
+
const options = (0, optionsUtils_1.parseExpectOptions)(table);
|
|
62
|
+
await (0, test_1.expect)(this.page).toHaveTitle(expected, options);
|
|
63
|
+
});
|
|
64
|
+
(0, cucumber_1.Then)("I see document title contains {string}", async function (substring, table) {
|
|
65
|
+
const options = (0, optionsUtils_1.parseExpectOptions)(table);
|
|
66
|
+
await (0, test_1.expect)(this.page).toHaveTitle(new RegExp(substring, "i"), options);
|
|
67
|
+
});
|
|
@@ -3,12 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const cucumber_1 = require("@cucumber/cucumber");
|
|
7
|
-
const pixelmatch_1 = __importDefault(require("pixelmatch"));
|
|
8
|
-
const pngjs_1 = require("pngjs");
|
|
9
6
|
const fs_1 = __importDefault(require("fs"));
|
|
10
7
|
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const cucumber_1 = require("@cucumber/cucumber");
|
|
11
9
|
const test_1 = require("@playwright/test");
|
|
10
|
+
const pixelmatch_1 = __importDefault(require("pixelmatch"));
|
|
11
|
+
const pngjs_1 = require("pngjs");
|
|
12
12
|
const BASELINE_DIR = path_1.default.resolve("e2e/snapshots/baseline");
|
|
13
13
|
const CURRENT_DIR = path_1.default.resolve("e2e/snapshots/current");
|
|
14
14
|
const DIFF_DIR = path_1.default.resolve("e2e/snapshots/diff");
|
|
@@ -36,7 +36,9 @@ function getSnapshotPaths(name) {
|
|
|
36
36
|
const current = pngjs_1.PNG.sync.read(fs_1.default.readFileSync(paths.current));
|
|
37
37
|
const { width, height } = baseline;
|
|
38
38
|
const diff = new pngjs_1.PNG({ width, height });
|
|
39
|
-
const pixelDiff = (0, pixelmatch_1.default)(baseline.data, current.data, diff.data, width, height, {
|
|
39
|
+
const pixelDiff = (0, pixelmatch_1.default)(baseline.data, current.data, diff.data, width, height, {
|
|
40
|
+
threshold: 0.1,
|
|
41
|
+
});
|
|
40
42
|
if (pixelDiff > 0) {
|
|
41
43
|
fs_1.default.writeFileSync(paths.diff, pngjs_1.PNG.sync.write(diff));
|
|
42
44
|
this.log?.(`❌ Visual mismatch detected, diff: ${paths.diff}`);
|
|
@@ -63,7 +65,9 @@ function getSnapshotPaths(name) {
|
|
|
63
65
|
}
|
|
64
66
|
const { width, height } = baseline;
|
|
65
67
|
const diff = new pngjs_1.PNG({ width, height });
|
|
66
|
-
const pixelDiff = (0, pixelmatch_1.default)(baseline.data, current.data, diff.data, width, height, {
|
|
68
|
+
const pixelDiff = (0, pixelmatch_1.default)(baseline.data, current.data, diff.data, width, height, {
|
|
69
|
+
threshold: 0.1,
|
|
70
|
+
});
|
|
67
71
|
if (pixelDiff > 0) {
|
|
68
72
|
fs_1.default.writeFileSync(paths.diff, pngjs_1.PNG.sync.write(diff));
|
|
69
73
|
this.log?.(`⚠️ Snapshot mismatch: ${alias}`);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const cucumber_1 = require("@cucumber/cucumber");
|
|
8
|
+
const resolveUtils_1 = require("../helpers/utils/resolveUtils");
|
|
9
|
+
// Step 1: Check and load existing session if valid
|
|
10
|
+
(0, cucumber_1.When)("I login with a session data {string}", async function (sessionName) {
|
|
11
|
+
const sessionPath = (0, resolveUtils_1.resolveSessionPath)(this, sessionName);
|
|
12
|
+
this.data.sessionFile = sessionPath;
|
|
13
|
+
if (fs_1.default.existsSync(sessionPath)) {
|
|
14
|
+
try {
|
|
15
|
+
await this.context?.addCookies(JSON.parse(fs_1.default.readFileSync(sessionPath, "utf-8")).cookies || []);
|
|
16
|
+
this.log?.(`✅ Loaded session from ${sessionPath}`);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
this.log?.(`⚠️ Failed to apply session: ${err.message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
this.log?.(`⚠️ Session file not found: ${sessionPath}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// Step 2: Save current context as session
|
|
27
|
+
(0, cucumber_1.When)(/^I save session as "([^"]+)"$/, async function (sessionName) {
|
|
28
|
+
const sessionPath = (0, resolveUtils_1.resolveSessionPath)(this, sessionName);
|
|
29
|
+
const cookies = await this.context.cookies();
|
|
30
|
+
const [localStorageData, sessionStorageData] = await this.page.evaluate(() => {
|
|
31
|
+
const toPairs = (store) => Object.entries(store);
|
|
32
|
+
return [
|
|
33
|
+
[{ origin: location.origin, values: toPairs(localStorage) }],
|
|
34
|
+
[{ origin: location.origin, values: toPairs(sessionStorage) }],
|
|
35
|
+
];
|
|
36
|
+
});
|
|
37
|
+
const sessionData = {
|
|
38
|
+
cookies,
|
|
39
|
+
localStorage: localStorageData,
|
|
40
|
+
sessionStorage: sessionStorageData,
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
fs_1.default.writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
|
|
44
|
+
this.log?.(`💾 Saved session as "${sessionName}"`);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
this.log?.(`❌ Failed to save session "${sessionName}": ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// Step 3: Remove a session
|
|
51
|
+
(0, cucumber_1.When)("I clear session {string}", function (sessionName) {
|
|
52
|
+
const sessionPath = (0, resolveUtils_1.resolveSessionPath)(this, sessionName);
|
|
53
|
+
if (fs_1.default.existsSync(sessionPath)) {
|
|
54
|
+
fs_1.default.unlinkSync(sessionPath);
|
|
55
|
+
this.log?.(`🧹 Cleared session: ${sessionPath}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.log?.(`⚠️ Session not found: ${sessionPath}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
(0, cucumber_1.When)(/^I restore session cookies "([^"]+)"(?: with reload "(true|false)")?$/, async function (sessionName, reload = "true") {
|
|
62
|
+
const sessionPath = (0, resolveUtils_1.resolveSessionPath)(this, sessionName);
|
|
63
|
+
if (!fs_1.default.existsSync(sessionPath)) {
|
|
64
|
+
this.log?.(`❌ Session file not found: ${sessionPath}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const sessionData = JSON.parse(fs_1.default.readFileSync(sessionPath, "utf-8"));
|
|
68
|
+
const { cookies = [], localStorage = [], sessionStorage = [] } = sessionData;
|
|
69
|
+
try {
|
|
70
|
+
// Clear & set cookies
|
|
71
|
+
if (cookies.length) {
|
|
72
|
+
const existing = await this.context.cookies();
|
|
73
|
+
if (existing.length)
|
|
74
|
+
await this.context.clearCookies();
|
|
75
|
+
await this.context.addCookies(cookies);
|
|
76
|
+
this.log?.(`🍪 Cookies restored from "${sessionName}"`);
|
|
77
|
+
}
|
|
78
|
+
// Apply storage into page context
|
|
79
|
+
await this.page.goto("about:blank");
|
|
80
|
+
if (localStorage.length > 0) {
|
|
81
|
+
for (const entry of localStorage) {
|
|
82
|
+
await this.page.addInitScript(([origin, values]) => {
|
|
83
|
+
if (window.origin === origin) {
|
|
84
|
+
for (const [key, val] of values) {
|
|
85
|
+
localStorage.setItem(key, val);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}, [entry.origin, entry.values]);
|
|
89
|
+
}
|
|
90
|
+
this.log?.("📦 localStorage restored");
|
|
91
|
+
}
|
|
92
|
+
if (sessionStorage.length > 0) {
|
|
93
|
+
for (const entry of sessionStorage) {
|
|
94
|
+
await this.page.addInitScript(([origin, values]) => {
|
|
95
|
+
if (window.origin === origin) {
|
|
96
|
+
for (const [key, val] of values) {
|
|
97
|
+
sessionStorage.setItem(key, val);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [entry.origin, entry.values]);
|
|
101
|
+
}
|
|
102
|
+
this.log?.("🗄️ sessionStorage restored");
|
|
103
|
+
}
|
|
104
|
+
// Final reload to apply context if requested
|
|
105
|
+
if (reload !== "false") {
|
|
106
|
+
await this.page.reload();
|
|
107
|
+
this.log?.("🔄 Page reloaded to apply restored session");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
this.log?.(`❌ Error restoring session: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const fs_1 = __importDefault(require("fs"));
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const cucumber_1 = require("@cucumber/cucumber");
|
|
42
|
+
const dotenv = __importStar(require("dotenv"));
|
|
43
|
+
const playwright_1 = require("playwright");
|
|
44
|
+
const compareSnapshots_1 = require("./compareSnapshots");
|
|
45
|
+
// Set to 30 seconds
|
|
46
|
+
(0, cucumber_1.setDefaultTimeout)(30 * 1000);
|
|
47
|
+
dotenv.config();
|
|
48
|
+
let sharedBrowser;
|
|
49
|
+
(0, cucumber_1.BeforeAll)(async () => {
|
|
50
|
+
sharedBrowser = await playwright_1.chromium.launch({
|
|
51
|
+
headless: process.env.HEADLESS !== "false",
|
|
52
|
+
});
|
|
53
|
+
console.log("🚀 Launched shared browser for all scenarios");
|
|
54
|
+
});
|
|
55
|
+
(0, cucumber_1.AfterAll)(async () => {
|
|
56
|
+
await sharedBrowser?.close();
|
|
57
|
+
console.log("🧹 Closed shared browser after all scenarios");
|
|
58
|
+
});
|
|
59
|
+
(0, cucumber_1.Before)(async function (scenario) {
|
|
60
|
+
const params = this.parameters || {};
|
|
61
|
+
const ARTIFACT_DIR = params.artifactDir || process.env.TEST_ARTIFACT_DIR || "test-artifacts";
|
|
62
|
+
const SCREENSHOT_DIR = path_1.default.resolve(ARTIFACT_DIR, "screenshots");
|
|
63
|
+
const VIDEO_DIR = path_1.default.resolve(ARTIFACT_DIR, "videos");
|
|
64
|
+
const TRACE_DIR = path_1.default.resolve(ARTIFACT_DIR, "traces");
|
|
65
|
+
const SESSION_FILE = path_1.default.resolve(ARTIFACT_DIR, "auth-cookies", "session.json");
|
|
66
|
+
this.data.artifactDir = ARTIFACT_DIR;
|
|
67
|
+
this.data.screenshotDir = SCREENSHOT_DIR;
|
|
68
|
+
this.data.videoDir = VIDEO_DIR;
|
|
69
|
+
this.data.traceDir = TRACE_DIR;
|
|
70
|
+
this.data.sessionFile = SESSION_FILE;
|
|
71
|
+
// Modes: "false" | "fail" | "all"
|
|
72
|
+
const traceMode = (params.enableTrace || process.env.ENABLE_TRACE || "false").toLowerCase();
|
|
73
|
+
const screenshotMode = (params.enableScreenshots ||
|
|
74
|
+
process.env.ENABLE_SCREENSHOTS ||
|
|
75
|
+
"false").toLowerCase();
|
|
76
|
+
const videoMode = (params.enableVideos || process.env.ENABLE_VIDEOS || "false").toLowerCase();
|
|
77
|
+
this.data.traceMode = traceMode;
|
|
78
|
+
this.data.screenshotMode = screenshotMode;
|
|
79
|
+
this.data.videoMode = videoMode;
|
|
80
|
+
const isMobileTag = scenario.pickle.tags.some((t) => t.name === "@mobile");
|
|
81
|
+
const deviceName = params.device || process.env.MOBILE_DEVICE || (isMobileTag ? "iPhone 13 Pro" : null);
|
|
82
|
+
const deviceSettings = deviceName ? playwright_1.devices[deviceName] : undefined;
|
|
83
|
+
if (deviceName && !deviceSettings) {
|
|
84
|
+
throw new Error(`🚫 Invalid MOBILE_DEVICE: "${deviceName}" is not recognized by Playwright.`);
|
|
85
|
+
}
|
|
86
|
+
const isVisualTest = params.enableVisualTest ??
|
|
87
|
+
(process.env.ENABLE_VISUAL_TEST === "true" ||
|
|
88
|
+
scenario.pickle.tags.some((t) => t.name === "@visual"));
|
|
89
|
+
this.data.enableVisualTest = isVisualTest;
|
|
90
|
+
if (isVisualTest)
|
|
91
|
+
process.env.VISUAL_TEST = "true";
|
|
92
|
+
const contextOptions = {
|
|
93
|
+
...(videoMode !== "false" ? { recordVideo: { dir: VIDEO_DIR } } : {}),
|
|
94
|
+
...(deviceSettings || {}),
|
|
95
|
+
};
|
|
96
|
+
if (fs_1.default.existsSync(SESSION_FILE)) {
|
|
97
|
+
contextOptions.storageState = SESSION_FILE;
|
|
98
|
+
this.log?.("✅ Reusing session from saved file.");
|
|
99
|
+
}
|
|
100
|
+
const context = await sharedBrowser.newContext(contextOptions);
|
|
101
|
+
const page = await context.newPage();
|
|
102
|
+
this.browser = sharedBrowser;
|
|
103
|
+
this.context = context;
|
|
104
|
+
this.page = page;
|
|
105
|
+
if (traceMode !== "false") {
|
|
106
|
+
await context.tracing.start({
|
|
107
|
+
screenshots: true,
|
|
108
|
+
snapshots: true,
|
|
109
|
+
sources: true,
|
|
110
|
+
});
|
|
111
|
+
this.data.tracingStarted = true;
|
|
112
|
+
this.log?.(`🧪 Tracing started (${traceMode})`);
|
|
113
|
+
}
|
|
114
|
+
if (deviceName)
|
|
115
|
+
this.log?.(`📱 Mobile emulation enabled (${deviceName})`);
|
|
116
|
+
});
|
|
117
|
+
(0, cucumber_1.After)(async function (scenario) {
|
|
118
|
+
const name = scenario.pickle.name.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
|
|
119
|
+
const failed = scenario.result?.status === "FAILED";
|
|
120
|
+
const mode = (value) => value?.toLowerCase();
|
|
121
|
+
const screenshotMode = mode(this.parameters?.enableScreenshots || process.env.ENABLE_SCREENSHOTS);
|
|
122
|
+
const videoMode = mode(this.parameters?.enableVideos || process.env.ENABLE_VIDEOS);
|
|
123
|
+
const traceMode = mode(this.parameters?.enableTrace || process.env.ENABLE_TRACE);
|
|
124
|
+
const shouldSaveScreenshot = screenshotMode === "all" || (screenshotMode === "fail" && failed);
|
|
125
|
+
const shouldSaveVideo = videoMode === "all" || (videoMode === "fail" && failed);
|
|
126
|
+
const shouldSaveTrace = traceMode === "all" || (traceMode === "fail" && failed);
|
|
127
|
+
// 📸 Screenshot
|
|
128
|
+
if (shouldSaveScreenshot && this.page) {
|
|
129
|
+
const screenshotPath = path_1.default.join(this.data.screenshotDir, `${failed ? "failed-" : ""}${name}.png`);
|
|
130
|
+
try {
|
|
131
|
+
fs_1.default.mkdirSync(this.data.screenshotDir, { recursive: true });
|
|
132
|
+
await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
133
|
+
console.log(`🖼️ Screenshot saved: ${screenshotPath}`);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.warn("❌ Failed to save screenshot:", err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 🎥 Video
|
|
140
|
+
if (this.page && videoMode !== "false") {
|
|
141
|
+
try {
|
|
142
|
+
const video = this.page.video();
|
|
143
|
+
if (video) {
|
|
144
|
+
const rawPath = await video.path();
|
|
145
|
+
if (fs_1.default.existsSync(rawPath)) {
|
|
146
|
+
const finalPath = path_1.default.join(this.data.videoDir, `${failed ? "failed-" : ""}${name}.webm`);
|
|
147
|
+
fs_1.default.mkdirSync(this.data.videoDir, { recursive: true });
|
|
148
|
+
shouldSaveVideo ? fs_1.default.renameSync(rawPath, finalPath) : fs_1.default.unlinkSync(rawPath);
|
|
149
|
+
console.log(`${shouldSaveVideo ? "🎥 Video saved" : "🧹 Deleted video"}: ${finalPath}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.warn(`⚠️ Video error: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// 🧪 Tracing
|
|
158
|
+
if (this.context && this.data.tracingStarted) {
|
|
159
|
+
const tracePath = path_1.default.join(this.data.artifactDir, "traces", `${name}.zip`);
|
|
160
|
+
try {
|
|
161
|
+
fs_1.default.mkdirSync(path_1.default.dirname(tracePath), { recursive: true });
|
|
162
|
+
await this.context.tracing.stop({ path: tracePath });
|
|
163
|
+
shouldSaveTrace
|
|
164
|
+
? console.log(`📦 Trace saved: ${tracePath}`)
|
|
165
|
+
: (fs_1.default.existsSync(tracePath) && fs_1.default.unlinkSync(tracePath),
|
|
166
|
+
console.log(`🧹 Trace discarded: ${tracePath}`));
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
console.warn("❌ Trace handling error:", err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 🧪 Visual regression
|
|
173
|
+
if (this.page && this.data.enableVisualTest) {
|
|
174
|
+
const BASELINE_DIR = path_1.default.resolve(this.data.artifactDir, "snapshots/baseline");
|
|
175
|
+
const DIFF_DIR = path_1.default.resolve(this.data.artifactDir, "snapshots/diff");
|
|
176
|
+
fs_1.default.mkdirSync(BASELINE_DIR, { recursive: true });
|
|
177
|
+
fs_1.default.mkdirSync(DIFF_DIR, { recursive: true });
|
|
178
|
+
const baselinePath = path_1.default.join(BASELINE_DIR, `${name}.png`);
|
|
179
|
+
const actualPath = path_1.default.join(DIFF_DIR, `${name}.actual.png`);
|
|
180
|
+
const diffPath = path_1.default.join(DIFF_DIR, `${name}.diff.png`);
|
|
181
|
+
await this.page.screenshot({ path: actualPath, fullPage: true });
|
|
182
|
+
if (!fs_1.default.existsSync(baselinePath)) {
|
|
183
|
+
fs_1.default.copyFileSync(actualPath, baselinePath);
|
|
184
|
+
console.log(`📸 Created baseline image: ${baselinePath}`);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
try {
|
|
188
|
+
const diffPixels = (0, compareSnapshots_1.compareSnapshots)({
|
|
189
|
+
actualPath,
|
|
190
|
+
baselinePath,
|
|
191
|
+
diffPath,
|
|
192
|
+
threshold: 0.1,
|
|
193
|
+
});
|
|
194
|
+
console.log(diffPixels > 0
|
|
195
|
+
? `⚠️ Visual diff found (${diffPixels} pixels): ${diffPath}`
|
|
196
|
+
: "✅ No visual changes detected");
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
console.warn("❌ Snapshot comparison failed:", err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Cleanup
|
|
204
|
+
try {
|
|
205
|
+
await this.cleanup(scenario);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
this.log?.("❌ Error during cleanup: " + err.message);
|
|
209
|
+
}
|
|
210
|
+
});
|