playwright-cucumber-ts-steps 0.0.9 โ 0.1.0
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/dist/actions/clickSteps.js +32 -49
- package/dist/actions/elementFindSteps.js +25 -46
- package/dist/actions/inputSteps.js +33 -40
- package/dist/actions/miscSteps.js +11 -21
- package/dist/actions/mouseSteps.js +3 -6
- package/dist/actions/storageSteps.js +1 -2
- package/dist/assertions/buttonAndTextVisibilitySteps.js +4 -8
- package/dist/assertions/elementSteps.js +4 -6
- package/dist/assertions/interceptionRequestsSteps.js +14 -6
- package/dist/assertions/storageSteps.js +2 -3
- package/dist/assertions/visualSteps.js +5 -8
- package/dist/custom_setups/loginHooks.js +5 -8
- package/dist/helpers/hooks.js +5 -7
- package/dist/helpers/world.js +7 -9
- package/package.json +17 -1
- package/dist/assertions/InterceptionRequests.d.ts +0 -1
- package/dist/assertions/InterceptionRequests.js +0 -191
- package/dist/assertions/button_and_text_visibility.d.ts +0 -1
- package/dist/assertions/button_and_text_visibility.js +0 -172
- package/dist/custom_setups/global-login.d.ts +0 -2
- package/dist/custom_setups/global-login.js +0 -20
|
@@ -84,16 +84,14 @@ When(/^I screenshot "(.*)"$/, async function (name) {
|
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
86
|
When("I screenshot", async function () {
|
|
87
|
-
var _a;
|
|
88
87
|
const path = `screenshots/screenshot-${Date.now()}.png`;
|
|
89
88
|
await this.page.screenshot({ path, fullPage: true });
|
|
90
|
-
|
|
89
|
+
this.log?.(`Saved screenshot to ${path}`);
|
|
91
90
|
});
|
|
92
91
|
//
|
|
93
92
|
// Page Navigation
|
|
94
93
|
//
|
|
95
94
|
When("I visit {string}", async function (urlOrAlias) {
|
|
96
|
-
var _a;
|
|
97
95
|
let url = urlOrAlias;
|
|
98
96
|
if (url.startsWith("@")) {
|
|
99
97
|
const alias = url.substring(1);
|
|
@@ -107,7 +105,7 @@ When("I visit {string}", async function (urlOrAlias) {
|
|
|
107
105
|
throw new Error("BASE_URL not defined");
|
|
108
106
|
url = `${baseUrl.replace(/\/+$/, "")}${url}`;
|
|
109
107
|
}
|
|
110
|
-
|
|
108
|
+
this.log?.(`Visiting: ${url}`);
|
|
111
109
|
await this.page.goto(url);
|
|
112
110
|
});
|
|
113
111
|
When("I reload the page", async function () {
|
|
@@ -142,8 +140,7 @@ const validUnits = [
|
|
|
142
140
|
"years",
|
|
143
141
|
];
|
|
144
142
|
When('I store {string} {int} {word} {word} as "{word}"', async function (baseAlias, amount, unit, direction, newAlias) {
|
|
145
|
-
|
|
146
|
-
const baseDateRaw = (_a = this.data) === null || _a === void 0 ? void 0 : _a[baseAlias];
|
|
143
|
+
const baseDateRaw = this.data?.[baseAlias];
|
|
147
144
|
if (!baseDateRaw)
|
|
148
145
|
throw new Error(`Alias "${baseAlias}" not found`);
|
|
149
146
|
if (!validUnits.includes(unit))
|
|
@@ -154,35 +151,32 @@ When('I store {string} {int} {word} {word} as "{word}"', async function (baseAli
|
|
|
154
151
|
const result = baseDate[direction === "before" ? "subtract" : "add"](amount, unit);
|
|
155
152
|
const formatted = result.format("YYYY-MM-DD");
|
|
156
153
|
this.data[newAlias] = formatted;
|
|
157
|
-
|
|
154
|
+
this.log?.(`๐
Stored ${amount} ${unit} ${direction} "${baseAlias}" as "@${newAlias}" = ${formatted}`);
|
|
158
155
|
});
|
|
159
156
|
//
|
|
160
157
|
// IFrame
|
|
161
158
|
//
|
|
162
159
|
When("I switch to iframe with selector {string}", async function (selector) {
|
|
163
|
-
var _a;
|
|
164
160
|
const frameLocator = this.page.frameLocator(selector);
|
|
165
161
|
await frameLocator
|
|
166
162
|
.locator("body")
|
|
167
163
|
.waitFor({ state: "visible", timeout: 10000 });
|
|
168
164
|
this.frame = frameLocator;
|
|
169
|
-
|
|
165
|
+
this.log?.(`๐ช Switched to iframe: ${selector}`);
|
|
170
166
|
});
|
|
171
167
|
When("I switch to iframe with title {string}", async function (title) {
|
|
172
|
-
var _a;
|
|
173
168
|
const frames = this.page.frames();
|
|
174
169
|
const match = frames.find((f) => f.title().then((t) => t.includes(title)));
|
|
175
170
|
if (!match)
|
|
176
171
|
throw new Error(`No iframe with title "${title}"`);
|
|
177
172
|
this.frame = this.page.frameLocator(`iframe[title*="${title}"]`);
|
|
178
|
-
|
|
173
|
+
this.log?.(`๐ช Switched to iframe titled: ${title}`);
|
|
179
174
|
});
|
|
180
175
|
When("I switch to iframe with selector {string} and wait for text {string}", async function (selector, expected) {
|
|
181
|
-
var _a;
|
|
182
176
|
const frameLocator = this.page.frameLocator(selector);
|
|
183
177
|
await frameLocator.locator(`text=${expected}`).waitFor({ timeout: 10000 });
|
|
184
178
|
this.frame = frameLocator;
|
|
185
|
-
|
|
179
|
+
this.log?.(`๐ช Switched to iframe: ${selector}, waited for "${expected}"`);
|
|
186
180
|
});
|
|
187
181
|
When("I exit iframe", function () {
|
|
188
182
|
this.exitIframe();
|
|
@@ -207,8 +201,7 @@ async function getReadableLabel(el) {
|
|
|
207
201
|
}
|
|
208
202
|
}
|
|
209
203
|
async function getElementsSubset(world, mode, count) {
|
|
210
|
-
|
|
211
|
-
const total = await ((_a = world.elements) === null || _a === void 0 ? void 0 : _a.count());
|
|
204
|
+
const total = await world.elements?.count();
|
|
212
205
|
if (!total || total < 1)
|
|
213
206
|
throw new Error("No elements stored");
|
|
214
207
|
if (count > total)
|
|
@@ -243,7 +236,6 @@ const locatorActions = {
|
|
|
243
236
|
fill: (el, table) => el.fill("", parseFillOptions(table)), // Extend this to support value
|
|
244
237
|
};
|
|
245
238
|
When(/^I (\w+) the (first|last|random) (\d+)$/, async function (action, mode, count, table) {
|
|
246
|
-
var _a;
|
|
247
239
|
const elements = await getElementsSubset(this, mode, count);
|
|
248
240
|
const actionFn = locatorActions[action];
|
|
249
241
|
if (!actionFn)
|
|
@@ -251,11 +243,10 @@ When(/^I (\w+) the (first|last|random) (\d+)$/, async function (action, mode, co
|
|
|
251
243
|
for (const el of elements) {
|
|
252
244
|
const label = await getReadableLabel(el);
|
|
253
245
|
await actionFn(el, table);
|
|
254
|
-
|
|
246
|
+
this.log?.(`โ
${actionDisplayNames[action] || action} element: "${label}"`);
|
|
255
247
|
}
|
|
256
248
|
});
|
|
257
249
|
When(/^I (\w+) the (\d+)(?:st|nd|rd|th) element$/, async function (action, nth, table) {
|
|
258
|
-
var _a;
|
|
259
250
|
const elements = await getElementsSubset(this, "nth", nth);
|
|
260
251
|
const actionFn = locatorActions[action];
|
|
261
252
|
if (!actionFn)
|
|
@@ -263,15 +254,14 @@ When(/^I (\w+) the (\d+)(?:st|nd|rd|th) element$/, async function (action, nth,
|
|
|
263
254
|
for (const el of elements) {
|
|
264
255
|
const label = await getReadableLabel(el);
|
|
265
256
|
await actionFn(el, table);
|
|
266
|
-
|
|
257
|
+
this.log?.(`โ
${actionDisplayNames[action] || action} the ${toOrdinal(nth)} element: "${label}"`);
|
|
267
258
|
}
|
|
268
259
|
});
|
|
269
260
|
When("I press key {string}", async function (key) {
|
|
270
|
-
var _a;
|
|
271
261
|
if (!this.element)
|
|
272
262
|
throw new Error("No element selected");
|
|
273
263
|
await this.element.focus();
|
|
274
264
|
await this.page.waitForTimeout(100); // buffer
|
|
275
265
|
await this.element.press(key);
|
|
276
|
-
|
|
266
|
+
this.log?.(`๐น Pressed {${key}}`);
|
|
277
267
|
});
|
|
@@ -25,7 +25,6 @@ When(/^I scroll window to position top:(\d+) left:(\d+)$/, async function (top,
|
|
|
25
25
|
}, top, left);
|
|
26
26
|
});
|
|
27
27
|
When('I scroll to "{word}"', async function (direction) {
|
|
28
|
-
var _a;
|
|
29
28
|
const validDirections = ["top", "bottom", "left", "right"];
|
|
30
29
|
if (!validDirections.includes(direction)) {
|
|
31
30
|
throw new Error(`Invalid scroll direction "${direction}". Must be one of: ${validDirections.join(", ")}.`);
|
|
@@ -50,18 +49,16 @@ When('I scroll to "{word}"', async function (direction) {
|
|
|
50
49
|
}
|
|
51
50
|
window.scrollTo(scrollOptions);
|
|
52
51
|
}, direction);
|
|
53
|
-
|
|
52
|
+
this.log?.(`๐ฑ๏ธ Scrolled to "${direction}"`);
|
|
54
53
|
await this.page.waitForTimeout(500); // allow scroll to complete
|
|
55
54
|
});
|
|
56
55
|
When("I hover over the element {string}", async function (selector) {
|
|
57
|
-
var _a;
|
|
58
56
|
const element = this.getScope().locator(selector);
|
|
59
57
|
await element.hover();
|
|
60
58
|
this.element = element;
|
|
61
|
-
|
|
59
|
+
this.log?.(`๐ฑ๏ธ Hovered: ${selector}`);
|
|
62
60
|
});
|
|
63
61
|
When("I move mouse to coordinates {int}, {int}", async function (x, y) {
|
|
64
|
-
var _a;
|
|
65
62
|
await this.page.mouse.move(x, y);
|
|
66
|
-
|
|
63
|
+
this.log?.(`๐งญ Mouse moved to (${x}, ${y})`);
|
|
67
64
|
});
|
|
@@ -37,7 +37,6 @@ When("I clear local storage", async function () {
|
|
|
37
37
|
await this.page.evaluate(() => localStorage.clear());
|
|
38
38
|
});
|
|
39
39
|
When("I store input text as {string}", async function (alias) {
|
|
40
|
-
var _a;
|
|
41
40
|
const activeElementHandle = await this.page.evaluateHandle(() => document.activeElement);
|
|
42
41
|
const tagName = await activeElementHandle.evaluate((el) => el ? el.tagName.toLowerCase() : "");
|
|
43
42
|
if (tagName !== "input" && tagName !== "textarea") {
|
|
@@ -45,5 +44,5 @@ When("I store input text as {string}", async function (alias) {
|
|
|
45
44
|
}
|
|
46
45
|
const value = await activeElementHandle.evaluate((el) => el.value);
|
|
47
46
|
this.data[alias] = value;
|
|
48
|
-
|
|
47
|
+
this.log?.(`๐ฅ Stored value from input as "${alias}": ${value}`);
|
|
49
48
|
});
|
|
@@ -52,11 +52,10 @@ Then(/^I do not see button "(.*)"$/, async function (rawText) {
|
|
|
52
52
|
* THEN: I see text "Welcome"
|
|
53
53
|
*/
|
|
54
54
|
Then("I see text {string}", async function (expected) {
|
|
55
|
-
var _a;
|
|
56
55
|
const scope = this.getScope(); // โ
Supports iframe OR main page
|
|
57
56
|
const locator = scope.locator(`text=${expected}`);
|
|
58
57
|
await locator.waitFor({ state: "visible", timeout: 5000 });
|
|
59
|
-
|
|
58
|
+
this.log?.(`โ
Verified text visible: ${expected}`);
|
|
60
59
|
});
|
|
61
60
|
/**
|
|
62
61
|
* THEN: I do not see text "Error"
|
|
@@ -118,7 +117,6 @@ Then(/^I do not see text "(.*)"$/, async function (unexpectedText) {
|
|
|
118
117
|
// ๐ Visible Text - Alias for clarity (optional if you want separate steps for naming)
|
|
119
118
|
//
|
|
120
119
|
Then("I see {string} in the element", async function (expected) {
|
|
121
|
-
var _a;
|
|
122
120
|
const element = this.element;
|
|
123
121
|
if (!element)
|
|
124
122
|
throw new Error("No element selected");
|
|
@@ -135,10 +133,9 @@ Then("I see {string} in the element", async function (expected) {
|
|
|
135
133
|
if (!textContent)
|
|
136
134
|
throw new Error("Element has no text content");
|
|
137
135
|
expect(textContent).toContain(expected);
|
|
138
|
-
|
|
136
|
+
this.log?.(`Verified "${expected}" in element text`);
|
|
139
137
|
});
|
|
140
138
|
Then("I see @{word} in the element", async function (alias) {
|
|
141
|
-
var _a;
|
|
142
139
|
const storedValue = this.data[alias];
|
|
143
140
|
if (!storedValue) {
|
|
144
141
|
throw new Error(`No value found in data storage under alias "@${alias}".`);
|
|
@@ -146,12 +143,11 @@ Then("I see @{word} in the element", async function (alias) {
|
|
|
146
143
|
if (!this.element) {
|
|
147
144
|
throw new Error("No element found. You must get an element before asserting its contents.");
|
|
148
145
|
}
|
|
149
|
-
const actualText = (
|
|
146
|
+
const actualText = (await this.element.textContent())?.trim() || "";
|
|
150
147
|
expect(actualText).toContain(storedValue);
|
|
151
148
|
this.log(`Verified element contains value from "@${alias}" = "${storedValue}". Actual: "${actualText}"`);
|
|
152
149
|
});
|
|
153
150
|
Then("I see button {string} is disabled", async function (rawText) {
|
|
154
|
-
var _a;
|
|
155
151
|
// Resolve alias
|
|
156
152
|
let buttonText = rawText.startsWith("@")
|
|
157
153
|
? this.data[rawText.slice(1)]
|
|
@@ -168,5 +164,5 @@ Then("I see button {string} is disabled", async function (rawText) {
|
|
|
168
164
|
if (!isDisabled) {
|
|
169
165
|
throw new Error(`๐ซ Button "${buttonText}" is not disabled as expected.`);
|
|
170
166
|
}
|
|
171
|
-
|
|
167
|
+
this.log?.(`โ
Verified button "${buttonText}" is disabled.`);
|
|
172
168
|
});
|
|
@@ -8,18 +8,16 @@ Then(/^I see element "([^"]+)" exists$/, async function (selector) {
|
|
|
8
8
|
await expect(el).toHaveCount(1);
|
|
9
9
|
});
|
|
10
10
|
Then("I see element exists", async function () {
|
|
11
|
-
var _a, _b, _c;
|
|
12
11
|
if (!this.element)
|
|
13
12
|
throw new Error("No element stored in context");
|
|
14
|
-
const count = (
|
|
13
|
+
const count = (await this.element.count?.()) ?? 1;
|
|
15
14
|
if (count === 0)
|
|
16
15
|
throw new Error("Element does not exist");
|
|
17
16
|
});
|
|
18
17
|
Then("I see element does not exist", async function () {
|
|
19
|
-
var _a, _b, _c;
|
|
20
18
|
if (!this.element)
|
|
21
19
|
throw new Error("No element stored in context");
|
|
22
|
-
const count = (
|
|
20
|
+
const count = (await this.element.count?.()) ?? 1;
|
|
23
21
|
if (count > 0)
|
|
24
22
|
throw new Error("Element exists but should not");
|
|
25
23
|
});
|
|
@@ -70,13 +68,13 @@ Then("I see element attribute {string} contains {string}", async function (attr,
|
|
|
70
68
|
if (!this.element)
|
|
71
69
|
throw new Error("No element in context");
|
|
72
70
|
const value = await this.element.getAttribute(attr);
|
|
73
|
-
if (!
|
|
71
|
+
if (!value?.includes(part)) {
|
|
74
72
|
throw new Error(`Attribute "${attr}" does not contain "${part}". Got: "${value}"`);
|
|
75
73
|
}
|
|
76
74
|
});
|
|
77
75
|
Then(/^I see element "([^"]+)" attribute "([^"]+)" contains "(.*)"$/, async function (selector, attribute, substring) {
|
|
78
76
|
const attr = await this.page.locator(selector).getAttribute(attribute);
|
|
79
|
-
expect(attr
|
|
77
|
+
expect(attr?.includes(substring)).toBeTruthy();
|
|
80
78
|
});
|
|
81
79
|
Then(/^I see element "([^"]+)" has attribute "([^"]+)"$/, async function (selector, attribute) {
|
|
82
80
|
const attr = await this.page.locator(selector).getAttribute(attribute);
|
|
@@ -2,13 +2,11 @@ import { Then } from "@cucumber/cucumber";
|
|
|
2
2
|
import { expect } from "@playwright/test";
|
|
3
3
|
// Accessing the Last Response
|
|
4
4
|
Then("I should see response status {int}", function (expectedStatus) {
|
|
5
|
-
|
|
6
|
-
expect((_a = this.data.lastResponse) === null || _a === void 0 ? void 0 : _a.status).toBe(expectedStatus);
|
|
5
|
+
expect(this.data.lastResponse?.status).toBe(expectedStatus);
|
|
7
6
|
this.log(`Verified response status is ${expectedStatus}`);
|
|
8
7
|
});
|
|
9
8
|
Then("I should see response body contains {string}", function (expectedText) {
|
|
10
|
-
|
|
11
|
-
expect((_a = this.data.lastResponse) === null || _a === void 0 ? void 0 : _a.body).toContain(expectedText);
|
|
9
|
+
expect(this.data.lastResponse?.body).toContain(expectedText);
|
|
12
10
|
this.log(`Verified response body contains "${expectedText}"`);
|
|
13
11
|
});
|
|
14
12
|
Then("I see response body {string}", async function (expected) {
|
|
@@ -173,7 +171,12 @@ Then("I see response body is JSON", async function () {
|
|
|
173
171
|
this.log(`Verified response body is valid JSON`);
|
|
174
172
|
}
|
|
175
173
|
catch (e) {
|
|
176
|
-
|
|
174
|
+
if (e instanceof Error) {
|
|
175
|
+
throw new Error(`Response body is not valid JSON: ${e.message}`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw new Error(`Response body is not valid JSON: ${String(e)}`);
|
|
179
|
+
}
|
|
177
180
|
}
|
|
178
181
|
});
|
|
179
182
|
Then("I see response body is not JSON", async function () {
|
|
@@ -186,6 +189,11 @@ Then("I see response body is not JSON", async function () {
|
|
|
186
189
|
throw new Error(`Expected response body to not be JSON, but it is`);
|
|
187
190
|
}
|
|
188
191
|
catch (e) {
|
|
189
|
-
|
|
192
|
+
if (e instanceof Error) {
|
|
193
|
+
this.log(`Verified response body is not JSON: ${e.message}`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
this.log(`Verified response body is not JSON: ${String(e)}`);
|
|
197
|
+
}
|
|
190
198
|
}
|
|
191
199
|
});
|
|
@@ -40,7 +40,6 @@ Then("I do not see session storage item {string}", async function (key) {
|
|
|
40
40
|
throw new Error(`Expected sessionStorage["${key}"] to be null, but got "${value}"`);
|
|
41
41
|
});
|
|
42
42
|
When("I clear all saved session files", async function () {
|
|
43
|
-
var _a, _b;
|
|
44
43
|
const authDir = path.resolve("e2e/support/helper/auth");
|
|
45
44
|
if (fs.existsSync(authDir)) {
|
|
46
45
|
const files = fs.readdirSync(authDir);
|
|
@@ -48,12 +47,12 @@ When("I clear all saved session files", async function () {
|
|
|
48
47
|
const filePath = path.join(authDir, file);
|
|
49
48
|
if (fs.lstatSync(filePath).isFile()) {
|
|
50
49
|
fs.unlinkSync(filePath);
|
|
51
|
-
|
|
50
|
+
this.log?.(`๐งน Deleted session file: ${file}`);
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
else {
|
|
56
|
-
|
|
55
|
+
this.log?.(`โ ๏ธ Auth directory not found at ${authDir}`);
|
|
57
56
|
}
|
|
58
57
|
});
|
|
59
58
|
Then("I see session storage item {string} equals {string}", async function (key, expected) {
|
|
@@ -16,7 +16,6 @@ function getSnapshotPaths(name) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
Then("I should see the page matches the snapshot {string}", async function (name) {
|
|
19
|
-
var _a, _b;
|
|
20
19
|
const { page } = this;
|
|
21
20
|
const paths = getSnapshotPaths(name);
|
|
22
21
|
fs.mkdirSync(BASELINE_DIR, { recursive: true });
|
|
@@ -25,7 +24,7 @@ Then("I should see the page matches the snapshot {string}", async function (name
|
|
|
25
24
|
await page.screenshot({ path: paths.current, fullPage: true });
|
|
26
25
|
if (!fs.existsSync(paths.baseline)) {
|
|
27
26
|
fs.copyFileSync(paths.current, paths.baseline);
|
|
28
|
-
|
|
27
|
+
this.log?.(`๐ธ Created baseline snapshot: ${paths.baseline}`);
|
|
29
28
|
return;
|
|
30
29
|
}
|
|
31
30
|
const baseline = PNG.sync.read(fs.readFileSync(paths.baseline));
|
|
@@ -35,20 +34,18 @@ Then("I should see the page matches the snapshot {string}", async function (name
|
|
|
35
34
|
const pixelDiff = pixelmatch(baseline.data, current.data, diff.data, width, height, { threshold: 0.1 });
|
|
36
35
|
if (pixelDiff > 0) {
|
|
37
36
|
fs.writeFileSync(paths.diff, PNG.sync.write(diff));
|
|
38
|
-
|
|
37
|
+
this.log?.(`โ Visual mismatch detected, diff: ${paths.diff}`);
|
|
39
38
|
}
|
|
40
39
|
expect(pixelDiff, "Pixels that differ").toBe(0);
|
|
41
40
|
});
|
|
42
41
|
Then("I capture a snapshot of the element {string} as {string}", async function (selector, alias) {
|
|
43
|
-
var _a;
|
|
44
42
|
const element = this.getScope().locator(selector);
|
|
45
43
|
const pathCurrent = path.join(CURRENT_DIR, `${alias}.png`);
|
|
46
44
|
fs.mkdirSync(CURRENT_DIR, { recursive: true });
|
|
47
45
|
await element.screenshot({ path: pathCurrent });
|
|
48
|
-
|
|
46
|
+
this.log?.(`๐ธ Snapshot for ${selector} saved as ${alias}`);
|
|
49
47
|
});
|
|
50
48
|
Then("The snapshot {string} should match baseline", async function (alias) {
|
|
51
|
-
var _a, _b;
|
|
52
49
|
const paths = getSnapshotPaths(alias);
|
|
53
50
|
const current = PNG.sync.read(fs.readFileSync(paths.current));
|
|
54
51
|
const baseline = fs.existsSync(paths.baseline)
|
|
@@ -56,7 +53,7 @@ Then("The snapshot {string} should match baseline", async function (alias) {
|
|
|
56
53
|
: null;
|
|
57
54
|
if (!baseline) {
|
|
58
55
|
fs.copyFileSync(paths.current, paths.baseline);
|
|
59
|
-
|
|
56
|
+
this.log?.(`๐ธ Created new baseline for ${alias}`);
|
|
60
57
|
return;
|
|
61
58
|
}
|
|
62
59
|
const { width, height } = baseline;
|
|
@@ -64,7 +61,7 @@ Then("The snapshot {string} should match baseline", async function (alias) {
|
|
|
64
61
|
const pixelDiff = pixelmatch(baseline.data, current.data, diff.data, width, height, { threshold: 0.1 });
|
|
65
62
|
if (pixelDiff > 0) {
|
|
66
63
|
fs.writeFileSync(paths.diff, PNG.sync.write(diff));
|
|
67
|
-
|
|
64
|
+
this.log?.(`โ ๏ธ Snapshot mismatch: ${alias}`);
|
|
68
65
|
}
|
|
69
66
|
expect(pixelDiff).toBe(0);
|
|
70
67
|
});
|
|
@@ -6,10 +6,9 @@ import { resolveValue, deriveSessionName, resolveLoginValue, } from "../helpers/
|
|
|
6
6
|
import fs from "fs";
|
|
7
7
|
import { log } from "console";
|
|
8
8
|
async function tryRestoreSession(world, sessionName) {
|
|
9
|
-
var _a;
|
|
10
9
|
const storagePath = path.resolve("e2e/support/helper/auth", `${sessionName}.json`);
|
|
11
10
|
if (fs.existsSync(storagePath)) {
|
|
12
|
-
await
|
|
11
|
+
await world.context?.addCookies([]);
|
|
13
12
|
await world.page.context().addInitScript(() => {
|
|
14
13
|
// preload logic if needed
|
|
15
14
|
});
|
|
@@ -91,15 +90,14 @@ When("I login as {string} user", async function (alias) {
|
|
|
91
90
|
this.log(`๐พ Saved session to ${alias}User.json`);
|
|
92
91
|
});
|
|
93
92
|
When("I perform login with:", async function (dataTable) {
|
|
94
|
-
var _a, _b, _c;
|
|
95
93
|
const loginData = Object.fromEntries(dataTable.raw());
|
|
96
94
|
const email = resolveLoginValue(loginData.email, this);
|
|
97
|
-
const password =
|
|
95
|
+
const password = resolveLoginValue(loginData.password, this) ?? process.env.USER_PASSWORD;
|
|
98
96
|
if (!email)
|
|
99
97
|
throw new Error("Missing or invalid email for login");
|
|
100
98
|
if (!password)
|
|
101
99
|
throw new Error("Missing or invalid password for login");
|
|
102
|
-
|
|
100
|
+
this.log?.(`๐ Logging in with: ${email}`);
|
|
103
101
|
await this.page.goto(`${process.env.BASE_URL}/login`);
|
|
104
102
|
await this.page.waitForLoadState("networkidle");
|
|
105
103
|
await this.page.fill('input[type="email"]', email);
|
|
@@ -107,10 +105,9 @@ When("I perform login with:", async function (dataTable) {
|
|
|
107
105
|
const loginButton = this.page.getByRole("button", { name: /login/i });
|
|
108
106
|
await loginButton.click();
|
|
109
107
|
await this.page.waitForLoadState("networkidle");
|
|
110
|
-
|
|
108
|
+
this.log?.("โ
Login successful");
|
|
111
109
|
});
|
|
112
110
|
async function pageLogin(world, baseUrl, email, password) {
|
|
113
|
-
var _a;
|
|
114
111
|
const { page } = world;
|
|
115
112
|
await page.goto(baseUrl);
|
|
116
113
|
await page.waitForLoadState("networkidle");
|
|
@@ -131,7 +128,7 @@ async function pageLogin(world, baseUrl, email, password) {
|
|
|
131
128
|
path: path.resolve("e2e/support/helper/auth", "session.json"),
|
|
132
129
|
});
|
|
133
130
|
world.data["loggedIn"] = true;
|
|
134
|
-
|
|
131
|
+
world.log?.("โ
Logged in and session saved.");
|
|
135
132
|
}
|
|
136
133
|
else {
|
|
137
134
|
console.log("Already logged in");
|
package/dist/helpers/hooks.js
CHANGED
|
@@ -35,11 +35,10 @@ BeforeAll(async () => {
|
|
|
35
35
|
console.log("๐ Launched shared browser for all scenarios");
|
|
36
36
|
});
|
|
37
37
|
AfterAll(async () => {
|
|
38
|
-
await
|
|
38
|
+
await sharedBrowser?.close();
|
|
39
39
|
console.log("๐งน Closed shared browser after all scenarios");
|
|
40
40
|
});
|
|
41
41
|
Before(async function (scenario) {
|
|
42
|
-
var _a, _b;
|
|
43
42
|
// ๐ก๏ธ Ensure browser is still usable
|
|
44
43
|
if (!sharedBrowser || !sharedBrowser.isConnected()) {
|
|
45
44
|
console.warn("โ ๏ธ Shared browser was disconnected. Restarting...");
|
|
@@ -57,7 +56,7 @@ Before(async function (scenario) {
|
|
|
57
56
|
};
|
|
58
57
|
if (fs.existsSync(SESSION_FILE)) {
|
|
59
58
|
contextOptions.storageState = SESSION_FILE;
|
|
60
|
-
|
|
59
|
+
this.log?.("โ
Reusing session from saved file.");
|
|
61
60
|
}
|
|
62
61
|
const context = await sharedBrowser.newContext(contextOptions);
|
|
63
62
|
const page = await context.newPage();
|
|
@@ -65,11 +64,10 @@ Before(async function (scenario) {
|
|
|
65
64
|
this.context = context;
|
|
66
65
|
this.page = page;
|
|
67
66
|
if (isMobile)
|
|
68
|
-
|
|
67
|
+
this.log?.("๐ฑ Mobile emulation enabled (iPhone 13 Pro)");
|
|
69
68
|
});
|
|
70
69
|
After(async function (scenario) {
|
|
71
|
-
|
|
72
|
-
const failed = ((_a = scenario.result) === null || _a === void 0 ? void 0 : _a.status) === "FAILED";
|
|
70
|
+
const failed = scenario.result?.status === "FAILED";
|
|
73
71
|
const name = scenario.pickle.name.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
|
|
74
72
|
// ๐ธ Screenshot on failure
|
|
75
73
|
if (failed && this.page) {
|
|
@@ -86,7 +84,7 @@ After(async function (scenario) {
|
|
|
86
84
|
// ๐ฅ Handle video recording
|
|
87
85
|
let rawPath;
|
|
88
86
|
try {
|
|
89
|
-
rawPath = await
|
|
87
|
+
rawPath = await this.page?.video()?.path();
|
|
90
88
|
}
|
|
91
89
|
catch (err) {
|
|
92
90
|
console.warn(`โ ๏ธ Unable to access video path: ${err.message}`);
|
package/dist/helpers/world.js
CHANGED
|
@@ -17,7 +17,7 @@ export class CustomWorld extends World {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
async init(testInfo) {
|
|
20
|
-
const isMobile = testInfo
|
|
20
|
+
const isMobile = testInfo?.pickle.tags.some((tag) => tag.name === "@mobile");
|
|
21
21
|
const device = isMobile ? devices["Pixel 5"] : undefined;
|
|
22
22
|
this.browser = await chromium.launch({ headless: isHeadless, slowMo });
|
|
23
23
|
this.context = await this.browser.newContext({
|
|
@@ -25,34 +25,32 @@ export class CustomWorld extends World {
|
|
|
25
25
|
recordVideo: { dir: "e2e/test-artifacts/videos" },
|
|
26
26
|
});
|
|
27
27
|
this.page = await this.context.newPage();
|
|
28
|
-
this.testName = testInfo
|
|
28
|
+
this.testName = testInfo?.pickle.name;
|
|
29
29
|
this.log(`๐งช Initialized context${isMobile ? " (mobile)" : ""}`);
|
|
30
30
|
}
|
|
31
31
|
getScope() {
|
|
32
|
-
|
|
33
|
-
return (_a = this.frame) !== null && _a !== void 0 ? _a : this.page;
|
|
32
|
+
return this.frame ?? this.page;
|
|
34
33
|
}
|
|
35
34
|
exitIframe() {
|
|
36
35
|
this.frame = undefined;
|
|
37
36
|
this.log("โฌ
๏ธ Exited iframe, scope is now main page");
|
|
38
37
|
}
|
|
39
38
|
async cleanup(testInfo) {
|
|
40
|
-
|
|
41
|
-
const failed = ((_a = testInfo === null || testInfo === void 0 ? void 0 : testInfo.result) === null || _a === void 0 ? void 0 : _a.status) === "FAILED";
|
|
39
|
+
const failed = testInfo?.result?.status === "FAILED";
|
|
42
40
|
try {
|
|
43
|
-
await
|
|
41
|
+
await this.page?.close();
|
|
44
42
|
}
|
|
45
43
|
catch (err) {
|
|
46
44
|
this.log(`โ ๏ธ Error closing page: ${err.message}`);
|
|
47
45
|
}
|
|
48
46
|
try {
|
|
49
|
-
await
|
|
47
|
+
await this.context?.close();
|
|
50
48
|
}
|
|
51
49
|
catch (err) {
|
|
52
50
|
this.log(`โ ๏ธ Error closing context: ${err.message}`);
|
|
53
51
|
}
|
|
54
52
|
try {
|
|
55
|
-
await
|
|
53
|
+
await this.browser?.close();
|
|
56
54
|
}
|
|
57
55
|
catch (err) {
|
|
58
56
|
this.log(`โ ๏ธ Error closing browser: ${err.message}`);
|
package/package.json
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright-cucumber-ts-steps",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A collection of reusable Playwright step definitions for Cucumber in TypeScript, designed to streamline end-to-end testing across web, API, and mobile applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./register": {
|
|
13
|
+
"import": "./dist/register.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"typesVersions": {
|
|
18
|
+
"*": {
|
|
19
|
+
"*": [
|
|
20
|
+
"dist/*"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
8
24
|
"scripts": {
|
|
9
25
|
"build": "tsc"
|
|
10
26
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|