playwright-cucumber-ts-steps 1.0.0 โ 1.0.2
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 +195 -256
- package/dist/backend/actions/index.js +4 -0
- package/dist/backend/actions/interactions.js +23 -0
- package/dist/backend/actions/navigation.js +19 -0
- package/dist/backend/api/assertions.js +26 -0
- package/dist/backend/api/index.js +4 -0
- package/dist/backend/api/requests.js +24 -0
- package/dist/backend/api/state.js +15 -0
- package/dist/backend/assertions/expectVisible.js +8 -0
- package/dist/backend/assertions/index.js +5 -0
- package/dist/backend/assertions/pageState.js +25 -0
- package/dist/backend/assertions/text.js +20 -0
- package/dist/backend/assertions/visibility.js +20 -0
- package/dist/backend/auth/index.js +71 -0
- package/dist/backend/elements/alerts.js +21 -0
- package/dist/backend/elements/forms.js +59 -0
- package/dist/backend/elements/frames.js +25 -0
- package/dist/backend/elements/index.js +5 -0
- package/dist/core/registry.js +20 -0
- package/dist/core/runner.js +136 -0
- package/dist/index.js +10 -0
- package/dist/reporting/index.js +43 -0
- package/package.json +19 -101
- package/LICENSE +0 -21
- package/src/actions/clickSteps.ts +0 -429
- package/src/actions/cookieSteps.ts +0 -95
- package/src/actions/debugSteps.ts +0 -21
- package/src/actions/elementFindSteps.ts +0 -961
- package/src/actions/fillFormSteps.ts +0 -270
- package/src/actions/index.ts +0 -12
- package/src/actions/inputSteps.ts +0 -354
- package/src/actions/interceptionSteps.ts +0 -325
- package/src/actions/miscSteps.ts +0 -1144
- package/src/actions/mouseSteps.ts +0 -256
- package/src/actions/scrollSteps.ts +0 -122
- package/src/actions/storageSteps.ts +0 -308
- package/src/assertions/buttonAndTextVisibilitySteps.ts +0 -436
- package/src/assertions/cookieSteps.ts +0 -131
- package/src/assertions/elementSteps.ts +0 -432
- package/src/assertions/formInputSteps.ts +0 -377
- package/src/assertions/index.ts +0 -11
- package/src/assertions/interceptionRequestsSteps.ts +0 -640
- package/src/assertions/locationSteps.ts +0 -315
- package/src/assertions/roleTestIdSteps.ts +0 -254
- package/src/assertions/semanticSteps.ts +0 -267
- package/src/assertions/storageSteps.ts +0 -250
- package/src/assertions/visualSteps.ts +0 -275
- package/src/custom_setups/loginHooks.ts +0 -154
- package/src/helpers/checkPeerDeps.ts +0 -19
- package/src/helpers/compareSnapshots.ts +0 -35
- package/src/helpers/hooks.ts +0 -212
- package/src/helpers/utils/fakerUtils.ts +0 -64
- package/src/helpers/utils/index.ts +0 -4
- package/src/helpers/utils/optionsUtils.ts +0 -104
- package/src/helpers/utils/resolveUtils.ts +0 -74
- package/src/helpers/utils/sessionUtils.ts +0 -36
- package/src/helpers/world.ts +0 -119
- package/src/iframes/frames.ts +0 -15
- package/src/index.ts +0 -18
- package/src/register.ts +0 -4
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { Then } from "@cucumber/cucumber";
|
|
4
|
-
import { expect } from "@playwright/test";
|
|
5
|
-
import pixelmatch from "pixelmatch"; // Ensure pixelmatch is installed: npm install pixelmatch
|
|
6
|
-
import { PNG } from "pngjs"; // Ensure pngjs is installed: npm install pngjs
|
|
7
|
-
import type { CustomWorld } from "../helpers/world"; // Assuming this path is correct
|
|
8
|
-
|
|
9
|
-
// --- Configuration for Snapshot Directories ---
|
|
10
|
-
// It's good practice to make these configurable (e.g., via world.config or env variables)
|
|
11
|
-
// For now, keeping them as resolved constants as per your original code.
|
|
12
|
-
const SNAPSHOTS_BASE_DIR = path.resolve("e2e/snapshots"); // Base directory for all snapshots
|
|
13
|
-
const BASELINE_DIR = path.join(SNAPSHOTS_BASE_DIR, "baseline");
|
|
14
|
-
const CURRENT_DIR = path.join(SNAPSHOTS_BASE_DIR, "current");
|
|
15
|
-
const DIFF_DIR = path.join(SNAPSHOTS_BASE_DIR, "diff");
|
|
16
|
-
|
|
17
|
-
// Helper function to generate standardized snapshot paths
|
|
18
|
-
function getSnapshotPaths(name: string) {
|
|
19
|
-
// Sanitize the name for use in filenames
|
|
20
|
-
const safeName = name.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
21
|
-
return {
|
|
22
|
-
baseline: path.join(BASELINE_DIR, `${safeName}.png`),
|
|
23
|
-
current: path.join(CURRENT_DIR, `${safeName}.png`),
|
|
24
|
-
diff: path.join(DIFF_DIR, `${safeName}.diff.png`),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Helper to ensure all necessary directories exist
|
|
29
|
-
function ensureSnapshotDirs() {
|
|
30
|
-
fs.mkdirSync(BASELINE_DIR, { recursive: true });
|
|
31
|
-
fs.mkdirSync(CURRENT_DIR, { recursive: true });
|
|
32
|
-
fs.mkdirSync(DIFF_DIR, { recursive: true });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ===================================================================================
|
|
36
|
-
// VISUAL REGRESSION ASSERTIONS: PAGE SNAPSHOTS
|
|
37
|
-
// ===================================================================================
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Asserts that the current page's visual appearance matches a named baseline snapshot.
|
|
41
|
-
* If no baseline exists for the given name, a new one is created from the current page.
|
|
42
|
-
* Differences between current and baseline snapshots are highlighted in a 'diff' image.
|
|
43
|
-
*
|
|
44
|
-
* ```gherkin
|
|
45
|
-
* Then I should see the page matches the snapshot {string}
|
|
46
|
-
* ```
|
|
47
|
-
*
|
|
48
|
-
* @param name - A unique name for the snapshot (e.g., "homepage", "product-details-page").
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* Then I should see the page matches the snapshot "homepage"
|
|
52
|
-
*
|
|
53
|
-
* @remarks
|
|
54
|
-
* This is a core step for visual regression testing.
|
|
55
|
-
* 1. Takes a screenshot of the current page and saves it to the `current` directory.
|
|
56
|
-
* 2. If a `baseline` snapshot does not exist, the `current` snapshot is copied to `baseline`,
|
|
57
|
-
* and the test passes (a new baseline is established).
|
|
58
|
-
* 3. If a `baseline` exists, it compares the `current` and `baseline` snapshots pixel by pixel
|
|
59
|
-
* using `pixelmatch`.
|
|
60
|
-
* 4. If a mismatch is detected (more than 0 differing pixels based on `threshold`), a `diff`
|
|
61
|
-
* image is generated, and the test fails.
|
|
62
|
-
*
|
|
63
|
-
* All snapshots (baseline, current, diff) are stored in `e2e/snapshots/`.
|
|
64
|
-
* Adjust `threshold` for sensitivity (0.1 means 10% difference in pixel color is allowed).
|
|
65
|
-
* @category Visual Regression Steps
|
|
66
|
-
*/
|
|
67
|
-
export async function Then_I_should_see_page_matches_snapshot(this: CustomWorld, name: string) {
|
|
68
|
-
const { page } = this;
|
|
69
|
-
const paths = getSnapshotPaths(name);
|
|
70
|
-
|
|
71
|
-
ensureSnapshotDirs(); // Ensure directories exist before taking screenshot
|
|
72
|
-
|
|
73
|
-
// Take current screenshot
|
|
74
|
-
await page.screenshot({ path: paths.current, fullPage: true });
|
|
75
|
-
this.log?.(`๐ธ Captured current snapshot: "${paths.current}".`);
|
|
76
|
-
|
|
77
|
-
if (!fs.existsSync(paths.baseline)) {
|
|
78
|
-
// If no baseline exists, create one from the current screenshot
|
|
79
|
-
fs.copyFileSync(paths.current, paths.baseline);
|
|
80
|
-
this.log?.(`โจ Created new baseline snapshot: "${paths.baseline}".`);
|
|
81
|
-
return; // Pass the test if a new baseline was created
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Load baseline and current images for comparison
|
|
85
|
-
const baselineImg = PNG.sync.read(fs.readFileSync(paths.baseline));
|
|
86
|
-
const currentImg = PNG.sync.read(fs.readFileSync(paths.current));
|
|
87
|
-
|
|
88
|
-
// Ensure images have the same dimensions for comparison
|
|
89
|
-
if (baselineImg.width !== currentImg.width || baselineImg.height !== currentImg.height) {
|
|
90
|
-
fs.writeFileSync(
|
|
91
|
-
paths.diff,
|
|
92
|
-
PNG.sync.write(
|
|
93
|
-
new PNG({
|
|
94
|
-
width: Math.max(baselineImg.width, currentImg.width),
|
|
95
|
-
height: Math.max(baselineImg.height, currentImg.height),
|
|
96
|
-
})
|
|
97
|
-
)
|
|
98
|
-
);
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Visual snapshot mismatch for "${name}": Dimensions differ! ` +
|
|
101
|
-
`Baseline: ${baselineImg.width}x${baselineImg.height}, Current: ${currentImg.width}x${currentImg.height}. ` +
|
|
102
|
-
`Diff image generated at "${paths.diff}".`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const { width, height } = baselineImg;
|
|
107
|
-
const diffImg = new PNG({ width, height });
|
|
108
|
-
|
|
109
|
-
// Compare images pixel by pixel
|
|
110
|
-
const pixelDiff = pixelmatch(
|
|
111
|
-
baselineImg.data,
|
|
112
|
-
currentImg.data,
|
|
113
|
-
diffImg.data,
|
|
114
|
-
width,
|
|
115
|
-
height,
|
|
116
|
-
{ threshold: 0.1 } // Adjust threshold for sensitivity (0.1 means 10% difference in pixel color is allowed)
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
if (pixelDiff > 0) {
|
|
120
|
-
// If differences found, write the diff image
|
|
121
|
-
fs.writeFileSync(paths.diff, PNG.sync.write(diffImg));
|
|
122
|
-
this.log?.(
|
|
123
|
-
`โ Visual mismatch detected for "${name}". ${pixelDiff} pixels differ. Diff image: "${paths.diff}".`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Assert that no pixels differ
|
|
128
|
-
expect(pixelDiff, `Visual snapshot "${name}" mismatch: ${pixelDiff} pixels differ.`).toBe(0);
|
|
129
|
-
this.log?.(`โ
Visual snapshot "${name}" matches baseline (0 pixels differ).`);
|
|
130
|
-
}
|
|
131
|
-
Then(
|
|
132
|
-
"I should see the page matches the snapshot {string}",
|
|
133
|
-
Then_I_should_see_page_matches_snapshot
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// ===================================================================================
|
|
137
|
-
// VISUAL REGRESSION: ELEMENT SNAPSHOT CAPTURE & MATCH
|
|
138
|
-
// ===================================================================================
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Captures a visual snapshot of a specific element identified by its selector and saves it under a given alias.
|
|
142
|
-
* This snapshot is saved to the `current` directory and can later be compared against a baseline.
|
|
143
|
-
*
|
|
144
|
-
* ```gherkin
|
|
145
|
-
* Then I capture a snapshot of the element {string} as {string}
|
|
146
|
-
* ```
|
|
147
|
-
*
|
|
148
|
-
* @param selector - The CSS selector of the element to capture.
|
|
149
|
-
* @param alias - A unique alias name for this element snapshot (e.g., "logo-image", "product-card").
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* Then I capture a snapshot of the element ".header .logo" as "logo-snapshot"
|
|
153
|
-
* Then I capture a snapshot of the element "#user-profile" as "user-profile-widget"
|
|
154
|
-
*
|
|
155
|
-
* @remarks
|
|
156
|
-
* This step is typically followed by {@link Then_the_snapshot_should_match_baseline | "Then the snapshot {string} should match baseline"}
|
|
157
|
-
* to perform the actual visual comparison.
|
|
158
|
-
* @category Visual Regression Steps
|
|
159
|
-
*/
|
|
160
|
-
export async function Then_I_capture_element_snapshot_as_alias(
|
|
161
|
-
this: CustomWorld,
|
|
162
|
-
selector: string,
|
|
163
|
-
alias: string
|
|
164
|
-
) {
|
|
165
|
-
const elementLocator = this.getScope().locator(selector);
|
|
166
|
-
ensureSnapshotDirs(); // Ensure directories exist
|
|
167
|
-
const pathCurrent = path.join(CURRENT_DIR, `${alias}.png`);
|
|
168
|
-
|
|
169
|
-
await elementLocator.screenshot({ path: pathCurrent });
|
|
170
|
-
this.log?.(`๐ธ Captured snapshot of element "${selector}" saved as "${alias}".`);
|
|
171
|
-
}
|
|
172
|
-
Then(
|
|
173
|
-
"I capture a snapshot of the element {string} as {string}",
|
|
174
|
-
Then_I_capture_element_snapshot_as_alias
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Asserts that a previously captured named snapshot (of an element) matches its baseline.
|
|
179
|
-
* If no baseline exists, a new one is created from the current snapshot.
|
|
180
|
-
*
|
|
181
|
-
* ```gherkin
|
|
182
|
-
* Then The snapshot {string} should match baseline
|
|
183
|
-
* ```
|
|
184
|
-
*
|
|
185
|
-
* @param alias - The unique alias name of the snapshot (as used in "Then I capture a snapshot...").
|
|
186
|
-
*
|
|
187
|
-
* @example
|
|
188
|
-
* Then I capture a snapshot of the element ".logo" as "logo-snapshot"
|
|
189
|
-
* Then The snapshot "logo-snapshot" should match baseline
|
|
190
|
-
*
|
|
191
|
-
* @remarks
|
|
192
|
-
* This step is designed to be used after a step like
|
|
193
|
-
* {@link Then_I_capture_element_snapshot_as_alias | "Then I capture a snapshot of the element {string} as {string}"}.
|
|
194
|
-
* It performs the same comparison logic as `Then I should see the page matches the snapshot`,
|
|
195
|
-
* but specifically for an element snapshot.
|
|
196
|
-
* All snapshots (baseline, current, diff) are stored in `e2e/snapshots/`.
|
|
197
|
-
* @category Visual Regression Steps
|
|
198
|
-
*/
|
|
199
|
-
export async function Then_the_snapshot_should_match_baseline(this: CustomWorld, alias: string) {
|
|
200
|
-
const paths = getSnapshotPaths(alias); // Get paths for baseline, current, diff based on alias
|
|
201
|
-
|
|
202
|
-
ensureSnapshotDirs(); // Ensure directories exist
|
|
203
|
-
|
|
204
|
-
// Check if the current snapshot file actually exists.
|
|
205
|
-
// This is crucial because `Then I capture a snapshot` must have been run first.
|
|
206
|
-
if (!fs.existsSync(paths.current)) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`Current snapshot file for alias "${alias}" not found at "${paths.current}".` +
|
|
209
|
-
`Ensure "Then I capture a snapshot of the element {string} as {string}" was run successfully before this step.`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Load current image
|
|
214
|
-
const currentImg = PNG.sync.read(fs.readFileSync(paths.current));
|
|
215
|
-
|
|
216
|
-
let baselineImg: PNG | null = null;
|
|
217
|
-
if (fs.existsSync(paths.baseline)) {
|
|
218
|
-
// Load baseline image if it exists
|
|
219
|
-
baselineImg = PNG.sync.read(fs.readFileSync(paths.baseline));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (!baselineImg) {
|
|
223
|
-
// If no baseline exists, create one from the current snapshot
|
|
224
|
-
fs.copyFileSync(paths.current, paths.baseline);
|
|
225
|
-
this.log?.(`โจ Created new baseline for snapshot "${alias}": "${paths.baseline}".`);
|
|
226
|
-
return; // Pass the test if a new baseline was created
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Ensure images have the same dimensions for comparison
|
|
230
|
-
if (baselineImg.width !== currentImg.width || baselineImg.height !== currentImg.height) {
|
|
231
|
-
fs.writeFileSync(
|
|
232
|
-
paths.diff,
|
|
233
|
-
PNG.sync.write(
|
|
234
|
-
new PNG({
|
|
235
|
-
width: Math.max(baselineImg.width, currentImg.width),
|
|
236
|
-
height: Math.max(baselineImg.height, currentImg.height),
|
|
237
|
-
})
|
|
238
|
-
)
|
|
239
|
-
);
|
|
240
|
-
throw new Error(
|
|
241
|
-
`Visual element snapshot mismatch for "${alias}": Dimensions differ! ` +
|
|
242
|
-
`Baseline: ${baselineImg.width}x${baselineImg.height}, Current: ${currentImg.width}x${currentImg.height}. ` +
|
|
243
|
-
`Diff image generated at "${paths.diff}".`
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const { width, height } = baselineImg;
|
|
248
|
-
const diffImg = new PNG({ width, height });
|
|
249
|
-
|
|
250
|
-
// Compare images pixel by pixel
|
|
251
|
-
const pixelDiff = pixelmatch(
|
|
252
|
-
baselineImg.data,
|
|
253
|
-
currentImg.data,
|
|
254
|
-
diffImg.data,
|
|
255
|
-
width,
|
|
256
|
-
height,
|
|
257
|
-
{ threshold: 0.1 } // Consistent threshold
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
if (pixelDiff > 0) {
|
|
261
|
-
// If differences found, write the diff image
|
|
262
|
-
fs.writeFileSync(paths.diff, PNG.sync.write(diffImg));
|
|
263
|
-
this.log?.(
|
|
264
|
-
`โ Visual mismatch detected for snapshot "${alias}". ${pixelDiff} pixels differ. Diff image: "${paths.diff}".`
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Assert that no pixels differ
|
|
269
|
-
expect(
|
|
270
|
-
pixelDiff,
|
|
271
|
-
`Visual element snapshot "${alias}" mismatch: ${pixelDiff} pixels differ.`
|
|
272
|
-
).toBe(0);
|
|
273
|
-
this.log?.(`โ
Visual element snapshot "${alias}" matches baseline (0 pixels differ).`);
|
|
274
|
-
}
|
|
275
|
-
Then("The snapshot {string} should match baseline", Then_the_snapshot_should_match_baseline);
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import { When } from "@cucumber/cucumber";
|
|
3
|
-
import { resolveSessionPath } from "../helpers/utils/resolveUtils";
|
|
4
|
-
import { CustomWorld } from "../helpers/world";
|
|
5
|
-
|
|
6
|
-
// Step 1: Check and load existing session if valid
|
|
7
|
-
When(
|
|
8
|
-
"I login with a session data {string}",
|
|
9
|
-
async function (this: CustomWorld, sessionName: string) {
|
|
10
|
-
const sessionPath = resolveSessionPath(this, sessionName);
|
|
11
|
-
this.data.sessionFile = sessionPath;
|
|
12
|
-
|
|
13
|
-
if (fs.existsSync(sessionPath)) {
|
|
14
|
-
try {
|
|
15
|
-
await this.context?.addCookies(
|
|
16
|
-
JSON.parse(fs.readFileSync(sessionPath, "utf-8")).cookies || []
|
|
17
|
-
);
|
|
18
|
-
this.log?.(`โ
Loaded session from ${sessionPath}`);
|
|
19
|
-
} catch (err) {
|
|
20
|
-
this.log?.(`โ ๏ธ Failed to apply session: ${(err as Error).message}`);
|
|
21
|
-
}
|
|
22
|
-
} else {
|
|
23
|
-
this.log?.(`โ ๏ธ Session file not found: ${sessionPath}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @step
|
|
30
|
-
* @description Saves the current browser context (cookies, localStorage, sessionStorage) as a session file.
|
|
31
|
-
* @example
|
|
32
|
-
* When I save session as "my-session"
|
|
33
|
-
*/
|
|
34
|
-
When(/^I save session as "([^"]+)"$/, async function (this: CustomWorld, sessionName: string) {
|
|
35
|
-
const sessionPath = resolveSessionPath(this, sessionName);
|
|
36
|
-
|
|
37
|
-
const cookies = await this.context.cookies();
|
|
38
|
-
|
|
39
|
-
const [localStorageData, sessionStorageData]: [
|
|
40
|
-
{ origin: string; values: [string, string][] }[],
|
|
41
|
-
{ origin: string; values: [string, string][] }[],
|
|
42
|
-
] = await this.page.evaluate(() => {
|
|
43
|
-
const toPairs = (store: Storage): [string, string][] => Object.entries(store);
|
|
44
|
-
|
|
45
|
-
return [
|
|
46
|
-
[{ origin: location.origin, values: toPairs(localStorage) }],
|
|
47
|
-
[{ origin: location.origin, values: toPairs(sessionStorage) }],
|
|
48
|
-
];
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const sessionData = {
|
|
52
|
-
cookies,
|
|
53
|
-
localStorage: localStorageData,
|
|
54
|
-
sessionStorage: sessionStorageData,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
fs.writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
|
|
59
|
-
this.log?.(`๐พ Saved session as "${sessionName}"`);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
this.log?.(`โ Failed to save session "${sessionName}": ${(err as Error).message}`);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @step
|
|
67
|
-
* @description Removes a session file with the given name.
|
|
68
|
-
* @example
|
|
69
|
-
* When I clear session "my-session"
|
|
70
|
-
*/
|
|
71
|
-
When("I clear session {string}", function (this: CustomWorld, sessionName: string) {
|
|
72
|
-
const sessionPath = resolveSessionPath(this, sessionName);
|
|
73
|
-
if (fs.existsSync(sessionPath)) {
|
|
74
|
-
fs.unlinkSync(sessionPath);
|
|
75
|
-
this.log?.(`๐งน Cleared session: ${sessionPath}`);
|
|
76
|
-
} else {
|
|
77
|
-
this.log?.(`โ ๏ธ Session not found: ${sessionPath}`);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* @step
|
|
83
|
-
* @description Restores cookies, localStorage, and sessionStorage from a session file. Optionally reloads the page.
|
|
84
|
-
* @example
|
|
85
|
-
* When I restore session cookies "my-session"
|
|
86
|
-
* When I restore session cookies "my-session" with reload "false"
|
|
87
|
-
*/
|
|
88
|
-
When(
|
|
89
|
-
/^I restore session cookies "([^"]+)"(?: with reload "(true|false)")?$/,
|
|
90
|
-
async function (this: CustomWorld, sessionName: string, reload = "true") {
|
|
91
|
-
const sessionPath = resolveSessionPath(this, sessionName);
|
|
92
|
-
|
|
93
|
-
if (!fs.existsSync(sessionPath)) {
|
|
94
|
-
this.log?.(`โ Session file not found: ${sessionPath}`);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const sessionData = JSON.parse(fs.readFileSync(sessionPath, "utf-8"));
|
|
99
|
-
const { cookies = [], localStorage = [], sessionStorage = [] } = sessionData;
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
// Clear & set cookies
|
|
103
|
-
if (cookies.length) {
|
|
104
|
-
const existing = await this.context.cookies();
|
|
105
|
-
if (existing.length) await this.context.clearCookies();
|
|
106
|
-
await this.context.addCookies(cookies);
|
|
107
|
-
this.log?.(`๐ช Cookies restored from "${sessionName}"`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Apply storage into page context
|
|
111
|
-
await this.page.goto("about:blank");
|
|
112
|
-
|
|
113
|
-
if (localStorage.length > 0) {
|
|
114
|
-
for (const entry of localStorage) {
|
|
115
|
-
await this.page.addInitScript(
|
|
116
|
-
([origin, values]) => {
|
|
117
|
-
if (window.origin === origin) {
|
|
118
|
-
for (const [key, val] of values) {
|
|
119
|
-
localStorage.setItem(key, val);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
[entry.origin, entry.values]
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
this.log?.("๐ฆ localStorage restored");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (sessionStorage.length > 0) {
|
|
130
|
-
for (const entry of sessionStorage) {
|
|
131
|
-
await this.page.addInitScript(
|
|
132
|
-
([origin, values]) => {
|
|
133
|
-
if (window.origin === origin) {
|
|
134
|
-
for (const [key, val] of values) {
|
|
135
|
-
sessionStorage.setItem(key, val);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
[entry.origin, entry.values]
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
this.log?.("๐๏ธ sessionStorage restored");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Final reload to apply context if requested
|
|
146
|
-
if (reload !== "false") {
|
|
147
|
-
await this.page.reload();
|
|
148
|
-
this.log?.("๐ Page reloaded to apply restored session");
|
|
149
|
-
}
|
|
150
|
-
} catch (err) {
|
|
151
|
-
this.log?.(`โ Error restoring session: ${(err as Error).message}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
);
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export function checkPeerDependencies(dependencies: string[]) {
|
|
2
|
-
const missing: string[] = [];
|
|
3
|
-
|
|
4
|
-
for (const dep of dependencies) {
|
|
5
|
-
try {
|
|
6
|
-
require.resolve(dep);
|
|
7
|
-
} catch {
|
|
8
|
-
missing.push(dep);
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (missing.length) {
|
|
13
|
-
console.warn(
|
|
14
|
-
`\nโ Missing peer dependencies: ${missing.join(", ")}` +
|
|
15
|
-
`\nPlease install them in your project:\n\n` +
|
|
16
|
-
`npm install --save-dev ${missing.join(" ")}\n`
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import pixelmatch from "pixelmatch";
|
|
3
|
-
import { PNG } from "pngjs";
|
|
4
|
-
|
|
5
|
-
export function compareSnapshots({
|
|
6
|
-
actualPath,
|
|
7
|
-
baselinePath,
|
|
8
|
-
diffPath,
|
|
9
|
-
threshold = 0.1,
|
|
10
|
-
}: {
|
|
11
|
-
actualPath: string;
|
|
12
|
-
baselinePath: string;
|
|
13
|
-
diffPath: string;
|
|
14
|
-
threshold?: number;
|
|
15
|
-
}): number {
|
|
16
|
-
const actual = PNG.sync.read(fs.readFileSync(actualPath));
|
|
17
|
-
const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
|
|
18
|
-
|
|
19
|
-
if (actual.width !== baseline.width || actual.height !== baseline.height) {
|
|
20
|
-
throw new Error("Snapshot size mismatch");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const diff = new PNG({ width: actual.width, height: actual.height });
|
|
24
|
-
const numDiffPixels = pixelmatch(
|
|
25
|
-
actual.data,
|
|
26
|
-
baseline.data,
|
|
27
|
-
diff.data,
|
|
28
|
-
actual.width,
|
|
29
|
-
actual.height,
|
|
30
|
-
{ threshold }
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
fs.writeFileSync(diffPath, PNG.sync.write(diff));
|
|
34
|
-
return numDiffPixels;
|
|
35
|
-
}
|
package/src/helpers/hooks.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import {
|
|
4
|
-
Before,
|
|
5
|
-
After,
|
|
6
|
-
BeforeAll,
|
|
7
|
-
AfterAll,
|
|
8
|
-
ITestCaseHookParameter,
|
|
9
|
-
setDefaultTimeout,
|
|
10
|
-
} from "@cucumber/cucumber";
|
|
11
|
-
import * as dotenv from "dotenv";
|
|
12
|
-
import { chromium, devices, Browser, BrowserContextOptions } from "playwright";
|
|
13
|
-
import { compareSnapshots } from "./compareSnapshots";
|
|
14
|
-
import { CustomWorld } from "./world";
|
|
15
|
-
|
|
16
|
-
// Set to 30 seconds
|
|
17
|
-
setDefaultTimeout(30 * 1000);
|
|
18
|
-
dotenv.config();
|
|
19
|
-
|
|
20
|
-
let sharedBrowser: Browser;
|
|
21
|
-
|
|
22
|
-
BeforeAll(async () => {
|
|
23
|
-
sharedBrowser = await chromium.launch({
|
|
24
|
-
headless: process.env.HEADLESS !== "false",
|
|
25
|
-
});
|
|
26
|
-
console.log("๐ Launched shared browser for all scenarios");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
AfterAll(async () => {
|
|
30
|
-
await sharedBrowser?.close();
|
|
31
|
-
console.log("๐งน Closed shared browser after all scenarios");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
Before(async function (this: CustomWorld, scenario: ITestCaseHookParameter) {
|
|
35
|
-
const params = this.parameters || {};
|
|
36
|
-
const ARTIFACT_DIR = params.artifactDir || process.env.TEST_ARTIFACT_DIR || "test-artifacts";
|
|
37
|
-
const SCREENSHOT_DIR = path.resolve(ARTIFACT_DIR, "screenshots");
|
|
38
|
-
const VIDEO_DIR = path.resolve(ARTIFACT_DIR, "videos");
|
|
39
|
-
const TRACE_DIR = path.resolve(ARTIFACT_DIR, "traces");
|
|
40
|
-
const SESSION_FILE = path.resolve(ARTIFACT_DIR, "auth-cookies", "session.json");
|
|
41
|
-
|
|
42
|
-
this.data.artifactDir = ARTIFACT_DIR;
|
|
43
|
-
this.data.screenshotDir = SCREENSHOT_DIR;
|
|
44
|
-
this.data.videoDir = VIDEO_DIR;
|
|
45
|
-
this.data.traceDir = TRACE_DIR;
|
|
46
|
-
this.data.sessionFile = SESSION_FILE;
|
|
47
|
-
|
|
48
|
-
// Modes: "false" | "fail" | "all"
|
|
49
|
-
const traceMode = (params.enableTrace || process.env.ENABLE_TRACE || "false").toLowerCase();
|
|
50
|
-
const screenshotMode = (
|
|
51
|
-
params.enableScreenshots ||
|
|
52
|
-
process.env.ENABLE_SCREENSHOTS ||
|
|
53
|
-
"false"
|
|
54
|
-
).toLowerCase();
|
|
55
|
-
const videoMode = (params.enableVideos || process.env.ENABLE_VIDEOS || "false").toLowerCase();
|
|
56
|
-
|
|
57
|
-
this.data.traceMode = traceMode;
|
|
58
|
-
this.data.screenshotMode = screenshotMode;
|
|
59
|
-
this.data.videoMode = videoMode;
|
|
60
|
-
|
|
61
|
-
const isMobileTag = scenario.pickle.tags.some((t) => t.name === "@mobile");
|
|
62
|
-
const deviceName =
|
|
63
|
-
params.device || process.env.MOBILE_DEVICE || (isMobileTag ? "iPhone 13 Pro" : null);
|
|
64
|
-
const deviceSettings = deviceName ? devices[deviceName] : undefined;
|
|
65
|
-
|
|
66
|
-
if (deviceName && !deviceSettings) {
|
|
67
|
-
throw new Error(`๐ซ Invalid MOBILE_DEVICE: "${deviceName}" is not recognized by Playwright.`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const isVisualTest =
|
|
71
|
-
params.enableVisualTest ??
|
|
72
|
-
(process.env.ENABLE_VISUAL_TEST === "true" ||
|
|
73
|
-
scenario.pickle.tags.some((t) => t.name === "@visual"));
|
|
74
|
-
|
|
75
|
-
this.data.enableVisualTest = isVisualTest;
|
|
76
|
-
if (isVisualTest) process.env.VISUAL_TEST = "true";
|
|
77
|
-
|
|
78
|
-
const contextOptions: BrowserContextOptions = {
|
|
79
|
-
...(videoMode !== "false" ? { recordVideo: { dir: VIDEO_DIR } } : {}),
|
|
80
|
-
...(deviceSettings || {}),
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
if (fs.existsSync(SESSION_FILE)) {
|
|
84
|
-
contextOptions.storageState = SESSION_FILE;
|
|
85
|
-
this.log?.("โ
Reusing session from saved file.");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const context = await sharedBrowser.newContext(contextOptions);
|
|
89
|
-
const page = await context.newPage();
|
|
90
|
-
|
|
91
|
-
this.browser = sharedBrowser;
|
|
92
|
-
this.context = context;
|
|
93
|
-
this.page = page;
|
|
94
|
-
|
|
95
|
-
if (traceMode !== "false") {
|
|
96
|
-
await context.tracing.start({
|
|
97
|
-
screenshots: true,
|
|
98
|
-
snapshots: true,
|
|
99
|
-
sources: true,
|
|
100
|
-
});
|
|
101
|
-
this.data.tracingStarted = true;
|
|
102
|
-
this.log?.(`๐งช Tracing started (${traceMode})`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (deviceName) this.log?.(`๐ฑ Mobile emulation enabled (${deviceName})`);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
After(async function (this: CustomWorld, scenario: ITestCaseHookParameter) {
|
|
109
|
-
const name = scenario.pickle.name.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
|
|
110
|
-
const failed = scenario.result?.status === "FAILED";
|
|
111
|
-
const mode = (value: string | undefined) => value?.toLowerCase();
|
|
112
|
-
|
|
113
|
-
const screenshotMode = mode(this.parameters?.enableScreenshots || process.env.ENABLE_SCREENSHOTS);
|
|
114
|
-
const videoMode = mode(this.parameters?.enableVideos || process.env.ENABLE_VIDEOS);
|
|
115
|
-
const traceMode = mode(this.parameters?.enableTrace || process.env.ENABLE_TRACE);
|
|
116
|
-
|
|
117
|
-
const shouldSaveScreenshot = screenshotMode === "all" || (screenshotMode === "fail" && failed);
|
|
118
|
-
const shouldSaveVideo = videoMode === "all" || (videoMode === "fail" && failed);
|
|
119
|
-
const shouldSaveTrace = traceMode === "all" || (traceMode === "fail" && failed);
|
|
120
|
-
|
|
121
|
-
// ๐ธ Screenshot
|
|
122
|
-
if (shouldSaveScreenshot && this.page) {
|
|
123
|
-
const screenshotPath = path.join(
|
|
124
|
-
this.data.screenshotDir,
|
|
125
|
-
`${failed ? "failed-" : ""}${name}.png`
|
|
126
|
-
);
|
|
127
|
-
try {
|
|
128
|
-
fs.mkdirSync(this.data.screenshotDir, { recursive: true });
|
|
129
|
-
await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
130
|
-
console.log(`๐ผ๏ธ Screenshot saved: ${screenshotPath}`);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
console.warn("โ Failed to save screenshot:", err);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ๐ฅ Video
|
|
137
|
-
if (this.page && videoMode !== "false") {
|
|
138
|
-
try {
|
|
139
|
-
const video = this.page.video();
|
|
140
|
-
if (video) {
|
|
141
|
-
const rawPath = await video.path();
|
|
142
|
-
if (fs.existsSync(rawPath)) {
|
|
143
|
-
const finalPath = path.join(this.data.videoDir, `${failed ? "failed-" : ""}${name}.webm`);
|
|
144
|
-
fs.mkdirSync(this.data.videoDir, { recursive: true });
|
|
145
|
-
shouldSaveVideo ? fs.renameSync(rawPath, finalPath) : fs.unlinkSync(rawPath);
|
|
146
|
-
console.log(`${shouldSaveVideo ? "๐ฅ Video saved" : "๐งน Deleted video"}: ${finalPath}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {
|
|
150
|
-
console.warn(`โ ๏ธ Video error: ${(err as Error).message}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ๐งช Tracing
|
|
155
|
-
if (this.context && this.data.tracingStarted) {
|
|
156
|
-
const tracePath = path.join(this.data.artifactDir, "traces", `${name}.zip`);
|
|
157
|
-
try {
|
|
158
|
-
fs.mkdirSync(path.dirname(tracePath), { recursive: true });
|
|
159
|
-
await this.context.tracing.stop({ path: tracePath });
|
|
160
|
-
shouldSaveTrace
|
|
161
|
-
? console.log(`๐ฆ Trace saved: ${tracePath}`)
|
|
162
|
-
: (fs.existsSync(tracePath) && fs.unlinkSync(tracePath),
|
|
163
|
-
console.log(`๐งน Trace discarded: ${tracePath}`));
|
|
164
|
-
} catch (err) {
|
|
165
|
-
console.warn("โ Trace handling error:", err);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ๐งช Visual regression
|
|
170
|
-
if (this.page && this.data.enableVisualTest) {
|
|
171
|
-
const BASELINE_DIR = path.resolve(this.data.artifactDir, "snapshots/baseline");
|
|
172
|
-
const DIFF_DIR = path.resolve(this.data.artifactDir, "snapshots/diff");
|
|
173
|
-
|
|
174
|
-
fs.mkdirSync(BASELINE_DIR, { recursive: true });
|
|
175
|
-
fs.mkdirSync(DIFF_DIR, { recursive: true });
|
|
176
|
-
|
|
177
|
-
const baselinePath = path.join(BASELINE_DIR, `${name}.png`);
|
|
178
|
-
const actualPath = path.join(DIFF_DIR, `${name}.actual.png`);
|
|
179
|
-
const diffPath = path.join(DIFF_DIR, `${name}.diff.png`);
|
|
180
|
-
|
|
181
|
-
await this.page.screenshot({ path: actualPath, fullPage: true });
|
|
182
|
-
|
|
183
|
-
if (!fs.existsSync(baselinePath)) {
|
|
184
|
-
fs.copyFileSync(actualPath, baselinePath);
|
|
185
|
-
console.log(`๐ธ Created baseline image: ${baselinePath}`);
|
|
186
|
-
} else {
|
|
187
|
-
try {
|
|
188
|
-
const diffPixels = compareSnapshots({
|
|
189
|
-
actualPath,
|
|
190
|
-
baselinePath,
|
|
191
|
-
diffPath,
|
|
192
|
-
threshold: 0.1,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
console.log(
|
|
196
|
-
diffPixels > 0
|
|
197
|
-
? `โ ๏ธ Visual diff found (${diffPixels} pixels): ${diffPath}`
|
|
198
|
-
: "โ
No visual changes detected"
|
|
199
|
-
);
|
|
200
|
-
} catch (err) {
|
|
201
|
-
console.warn("โ Snapshot comparison failed:", err);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Cleanup
|
|
207
|
-
try {
|
|
208
|
-
await this.cleanup(scenario);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
this.log?.("โ Error during cleanup: " + (err as Error).message);
|
|
211
|
-
}
|
|
212
|
-
});
|