playwright-cucumber-ts-steps 0.0.8 → 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/{button_and_text_visibility.js → buttonAndTextVisibilitySteps.js} +4 -8
- package/dist/assertions/elementSteps.js +4 -6
- package/dist/assertions/{InterceptionRequests.js → 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/dist/index.d.ts +4 -4
- package/dist/index.js +9 -6
- package/package.json +17 -1
- /package/dist/assertions/{InterceptionRequests.d.ts → buttonAndTextVisibilitySteps.d.ts} +0 -0
- /package/dist/assertions/{button_and_text_visibility.d.ts → interceptionRequestsSteps.d.ts} +0 -0
- /package/dist/custom_setups/{global-login.d.ts → globalLogin.d.ts} +0 -0
- /package/dist/custom_setups/{global-login.js → globalLogin.js} +0 -0
|
@@ -2,45 +2,40 @@
|
|
|
2
2
|
import { When } from "@cucumber/cucumber";
|
|
3
3
|
import { parseClickOptions } from "../helpers/utils/optionsUtils";
|
|
4
4
|
When("I click", async function (...rest) {
|
|
5
|
-
var _a;
|
|
6
5
|
const maybeTable = rest[0];
|
|
7
|
-
const options =
|
|
6
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
8
7
|
if (!this.element)
|
|
9
8
|
throw new Error("❌ No stored element to click.");
|
|
10
9
|
await this.element.click(options);
|
|
11
|
-
|
|
10
|
+
this.log?.("🖱️ Clicked on stored element");
|
|
12
11
|
});
|
|
13
12
|
When("I click on button {string}", async function (label, ...rest) {
|
|
14
|
-
var _a;
|
|
15
13
|
const maybeTable = rest[0];
|
|
16
|
-
const options =
|
|
14
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
17
15
|
const button = await this.page.getByRole("button", { name: label });
|
|
18
16
|
await button.click(options);
|
|
19
17
|
this.element = button;
|
|
20
|
-
|
|
18
|
+
this.log?.(`🖱️ Clicked on button "${label}"`);
|
|
21
19
|
});
|
|
22
20
|
When("I click on link {string}", async function (text, ...rest) {
|
|
23
|
-
var _a;
|
|
24
21
|
const maybeTable = rest[0];
|
|
25
|
-
const options =
|
|
22
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
26
23
|
const link = await this.page.getByRole("link", { name: text });
|
|
27
24
|
await link.click(options);
|
|
28
25
|
this.element = link;
|
|
29
|
-
|
|
26
|
+
this.log?.(`✅ Clicked on link "${text}"`);
|
|
30
27
|
});
|
|
31
28
|
When("I click on label {string}", async function (labelText, ...rest) {
|
|
32
|
-
var _a;
|
|
33
29
|
const maybeTable = rest[0];
|
|
34
|
-
const options =
|
|
30
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
35
31
|
const label = await this.page.getByLabel(labelText);
|
|
36
32
|
await label.click(options);
|
|
37
33
|
this.element = label;
|
|
38
|
-
|
|
34
|
+
this.log?.(`🏷️ Clicked on label "${labelText}"`);
|
|
39
35
|
});
|
|
40
36
|
When("I click on text {string}", async function (rawText, ...rest) {
|
|
41
|
-
var _a;
|
|
42
37
|
const maybeTable = rest[0];
|
|
43
|
-
const options =
|
|
38
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
44
39
|
let text = rawText;
|
|
45
40
|
if (rawText.startsWith("@")) {
|
|
46
41
|
const alias = rawText.slice(1);
|
|
@@ -52,22 +47,20 @@ When("I click on text {string}", async function (rawText, ...rest) {
|
|
|
52
47
|
await locator.first().waitFor({ state: "visible", timeout: 5000 });
|
|
53
48
|
await locator.first().click(options);
|
|
54
49
|
this.element = locator.first();
|
|
55
|
-
|
|
50
|
+
this.log?.(`🖱️ Clicked on text "${text}"`);
|
|
56
51
|
});
|
|
57
52
|
When("I click on exact text {string}", async function (exactText, ...rest) {
|
|
58
|
-
var _a;
|
|
59
53
|
const maybeTable = rest[0];
|
|
60
|
-
const options =
|
|
54
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
61
55
|
const locator = this.page.getByText(exactText, { exact: true });
|
|
62
56
|
await locator.waitFor({ state: "visible", timeout: 5000 });
|
|
63
57
|
await locator.click(options);
|
|
64
58
|
this.element = locator;
|
|
65
|
-
|
|
59
|
+
this.log?.(`🖱️ Clicked on exact text "${exactText}"`);
|
|
66
60
|
});
|
|
67
61
|
When("I click all", async function (...rest) {
|
|
68
|
-
var _a, _b;
|
|
69
62
|
const maybeTable = rest[0];
|
|
70
|
-
const options =
|
|
63
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
71
64
|
if (!this.elements)
|
|
72
65
|
throw new Error("❌ No stored elements to click.");
|
|
73
66
|
const count = await this.elements.count();
|
|
@@ -77,77 +70,68 @@ When("I click all", async function (...rest) {
|
|
|
77
70
|
const el = this.elements.nth(i);
|
|
78
71
|
await el.waitFor({ state: "visible", timeout: 5000 });
|
|
79
72
|
await el.click(options);
|
|
80
|
-
|
|
73
|
+
this.log?.(`🖱️ Clicked element #${i + 1}`);
|
|
81
74
|
}
|
|
82
|
-
|
|
75
|
+
this.log?.(`✅ Clicked all ${count} elements.`);
|
|
83
76
|
});
|
|
84
77
|
When("I double click on text {string}", async function (text, ...rest) {
|
|
85
|
-
var _a;
|
|
86
78
|
const maybeTable = rest[0];
|
|
87
|
-
const options =
|
|
79
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
88
80
|
const element = this.element || this.page.getByText(text);
|
|
89
81
|
await element.dblclick(options);
|
|
90
|
-
|
|
82
|
+
this.log?.(`🖱️ Double-clicked on text "${text}"`);
|
|
91
83
|
});
|
|
92
84
|
When("I double click position {int} {int}", async function (x, y, ...rest) {
|
|
93
|
-
var _a;
|
|
94
85
|
const maybeTable = rest[0];
|
|
95
|
-
const options =
|
|
86
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
96
87
|
await this.page.mouse.dblclick(x, y, options);
|
|
97
|
-
|
|
88
|
+
this.log?.(`🖱️ Double-clicked at (${x}, ${y})`);
|
|
98
89
|
});
|
|
99
90
|
When("I double click", async function (...rest) {
|
|
100
|
-
var _a;
|
|
101
91
|
const maybeTable = rest[0];
|
|
102
|
-
const options =
|
|
92
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
103
93
|
if (!this.element)
|
|
104
94
|
throw new Error("❌ No stored element to double-click.");
|
|
105
95
|
await this.element.dblclick(options);
|
|
106
|
-
|
|
96
|
+
this.log?.("🖱️ Double-clicked on stored element");
|
|
107
97
|
});
|
|
108
98
|
When("I right click", async function (...rest) {
|
|
109
|
-
var _a;
|
|
110
99
|
const maybeTable = rest[0];
|
|
111
|
-
const options =
|
|
100
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
112
101
|
if (!this.element)
|
|
113
102
|
throw new Error("❌ No stored element to right-click.");
|
|
114
103
|
await this.element.click({ button: "right", ...options });
|
|
115
|
-
|
|
104
|
+
this.log?.("🖱️ Right-clicked on stored element");
|
|
116
105
|
});
|
|
117
106
|
When("I right click on text {string}", async function (text, ...rest) {
|
|
118
|
-
var _a;
|
|
119
107
|
const maybeTable = rest[0];
|
|
120
|
-
const options =
|
|
108
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
121
109
|
const element = this.page.getByText(text);
|
|
122
110
|
await element.click({ button: "right", ...options });
|
|
123
|
-
|
|
111
|
+
this.log?.(`🖱️ Right-clicked on text "${text}"`);
|
|
124
112
|
});
|
|
125
113
|
When("I right click position {int} {int}", async function (x, y, ...rest) {
|
|
126
|
-
var _a;
|
|
127
114
|
const maybeTable = rest[0];
|
|
128
|
-
const options =
|
|
115
|
+
const options = maybeTable?.rowsHash ? parseClickOptions(maybeTable) : {};
|
|
129
116
|
await this.page.mouse.click(x, y, { button: "right", ...options });
|
|
130
|
-
|
|
117
|
+
this.log?.(`🖱️ Right-clicked at (${x}, ${y})`);
|
|
131
118
|
});
|
|
132
119
|
When("I blur", async function () {
|
|
133
|
-
var _a;
|
|
134
120
|
await this.page.evaluate(() => {
|
|
135
121
|
const active = document.activeElement;
|
|
136
122
|
if (active && typeof active.blur === "function") {
|
|
137
123
|
active.blur();
|
|
138
124
|
}
|
|
139
125
|
});
|
|
140
|
-
|
|
126
|
+
this.log?.("🌀 Blurred active element");
|
|
141
127
|
});
|
|
142
128
|
When("I focus", async function () {
|
|
143
|
-
var _a;
|
|
144
129
|
if (!this.element)
|
|
145
130
|
throw new Error("❌ No stored element to focus.");
|
|
146
131
|
await this.element.focus();
|
|
147
|
-
|
|
132
|
+
this.log?.("🎯 Focused stored element");
|
|
148
133
|
});
|
|
149
134
|
When("I click all", async function (dataTable) {
|
|
150
|
-
var _a, _b;
|
|
151
135
|
const options = parseClickOptions(dataTable);
|
|
152
136
|
if (!this.elements) {
|
|
153
137
|
throw new Error("❌ No elements stored. Use a 'find' step before 'I click all'.");
|
|
@@ -160,14 +144,13 @@ When("I click all", async function (dataTable) {
|
|
|
160
144
|
const element = this.elements.nth(i);
|
|
161
145
|
await element.waitFor({ state: "visible", timeout: 5000 });
|
|
162
146
|
await element.click(options);
|
|
163
|
-
|
|
147
|
+
this.log?.(`🖱️ Clicked element #${i + 1}`);
|
|
164
148
|
}
|
|
165
|
-
|
|
149
|
+
this.log?.(`✅ Clicked all ${count} stored elements.`);
|
|
166
150
|
});
|
|
167
151
|
When(/^I click on selector "([^"]+)"$/, async function (selector) {
|
|
168
|
-
var _a;
|
|
169
152
|
const scope = this.getScope();
|
|
170
153
|
const element = scope.locator(selector);
|
|
171
154
|
await element.click();
|
|
172
|
-
|
|
155
|
+
this.log?.(`🖱️ Clicked on selector: ${selector}`);
|
|
173
156
|
});
|
|
@@ -8,10 +8,9 @@ When("I find element by selector {string}", async function (selector) {
|
|
|
8
8
|
await expect(this.element).toHaveCount(1);
|
|
9
9
|
});
|
|
10
10
|
When("I find elements by selector {string}", async function (selector) {
|
|
11
|
-
var _a;
|
|
12
11
|
this.elements = this.page.locator(selector);
|
|
13
12
|
const count = await this.elements.count();
|
|
14
|
-
|
|
13
|
+
this.log?.(`Found ${count} elements with selector ${selector}`);
|
|
15
14
|
});
|
|
16
15
|
When("I find element by text {string}", async function (text) {
|
|
17
16
|
this.element = this.page.getByText(text, { exact: true });
|
|
@@ -46,11 +45,10 @@ When("I find element by name {string}", async function (name) {
|
|
|
46
45
|
await expect(this.element).toHaveCount(1);
|
|
47
46
|
});
|
|
48
47
|
When("I find buttons by text {string}", async function (buttonText) {
|
|
49
|
-
var _a, _b;
|
|
50
48
|
// 🧠 Resolve alias
|
|
51
49
|
if (buttonText.startsWith("@")) {
|
|
52
50
|
const alias = buttonText.slice(1);
|
|
53
|
-
buttonText =
|
|
51
|
+
buttonText = this.data?.[alias];
|
|
54
52
|
if (!buttonText) {
|
|
55
53
|
throw new Error(`No value found for alias "@${alias}"`);
|
|
56
54
|
}
|
|
@@ -60,7 +58,7 @@ When("I find buttons by text {string}", async function (buttonText) {
|
|
|
60
58
|
name: buttonText,
|
|
61
59
|
exact: false,
|
|
62
60
|
});
|
|
63
|
-
|
|
61
|
+
this.log?.(`🔘 Stored all buttons matching text "${buttonText}"`);
|
|
64
62
|
});
|
|
65
63
|
// =============================
|
|
66
64
|
// WHEN I GET ELEMENT(S)
|
|
@@ -82,7 +80,6 @@ When("I get last element", async function () {
|
|
|
82
80
|
this.element = this.elements.last();
|
|
83
81
|
});
|
|
84
82
|
When(/^I get (\d+)(?:st|nd|rd|th) element$/, async function (index) {
|
|
85
|
-
var _a;
|
|
86
83
|
if (!this.elements)
|
|
87
84
|
throw new Error("No elements stored to pick from");
|
|
88
85
|
const count = await this.elements.count();
|
|
@@ -90,17 +87,16 @@ When(/^I get (\d+)(?:st|nd|rd|th) element$/, async function (index) {
|
|
|
90
87
|
throw new Error(`Cannot get element ${index} — only ${count} found`);
|
|
91
88
|
}
|
|
92
89
|
this.element = this.elements.nth(index - 1);
|
|
93
|
-
|
|
90
|
+
this.log?.(`Selected ${index} element from stored elements`);
|
|
94
91
|
});
|
|
95
92
|
When("I find elements by role {string}", async function (role) {
|
|
96
|
-
var _a;
|
|
97
93
|
const locator = this.page.getByRole(role);
|
|
98
94
|
const count = await locator.count();
|
|
99
95
|
if (count === 0) {
|
|
100
96
|
throw new Error(`No elements found with role "${role}"`);
|
|
101
97
|
}
|
|
102
98
|
this.elements = locator;
|
|
103
|
-
|
|
99
|
+
this.log?.(`Stored ${count} elements with role "${role}"`);
|
|
104
100
|
});
|
|
105
101
|
// When(
|
|
106
102
|
// "I get {int}{ordinal} element",
|
|
@@ -129,104 +125,87 @@ When("I get focused element", async function () {
|
|
|
129
125
|
this.element = (await this.page.evaluateHandle(() => document.activeElement));
|
|
130
126
|
});
|
|
131
127
|
When("I store element text as {string}", async function (alias) {
|
|
132
|
-
var _a;
|
|
133
128
|
const element = this.element;
|
|
134
129
|
if (!element)
|
|
135
130
|
throw new Error("No element selected");
|
|
136
131
|
const text = await element.textContent();
|
|
137
|
-
this.data[alias] = text
|
|
138
|
-
|
|
132
|
+
this.data[alias] = text?.trim();
|
|
133
|
+
this.log?.(`Stored text "${text}" as "${alias}"`);
|
|
139
134
|
});
|
|
140
135
|
When("I find textarea by label text {string}", async function (label) {
|
|
141
|
-
var _a;
|
|
142
136
|
this.element = this.page.getByLabel(label);
|
|
143
|
-
|
|
137
|
+
this.log?.(`Stored textarea with label "${label}"`);
|
|
144
138
|
});
|
|
145
139
|
When("I find textarea by placeholder text {string}", async function (placeholder) {
|
|
146
|
-
var _a;
|
|
147
140
|
this.element = this.page.getByPlaceholder(placeholder);
|
|
148
|
-
|
|
141
|
+
this.log?.(`Stored textarea with placeholder "${placeholder}"`);
|
|
149
142
|
});
|
|
150
143
|
When("I find textareas by label text {string}", async function (label) {
|
|
151
|
-
var _a;
|
|
152
144
|
this.elements = this.page.locator(`label:has-text("${label}") + textarea`);
|
|
153
|
-
|
|
145
|
+
this.log?.(`Stored multiple textareas with label "${label}"`);
|
|
154
146
|
});
|
|
155
147
|
When("I find textarea by name {string}", async function (name) {
|
|
156
|
-
var _a;
|
|
157
148
|
this.element = this.page.locator(`textarea[name="${name}"]`);
|
|
158
|
-
|
|
149
|
+
this.log?.(`Stored textarea with name "${name}"`);
|
|
159
150
|
});
|
|
160
151
|
When("I find textareas by ID {string}", async function (id) {
|
|
161
|
-
var _a;
|
|
162
152
|
this.elements = this.page.locator(`textarea#${id}`);
|
|
163
|
-
|
|
153
|
+
this.log?.(`Stored multiple textareas with ID "${id}"`);
|
|
164
154
|
});
|
|
165
155
|
When("I find input by ID {string}", async function (id) {
|
|
166
|
-
var _a;
|
|
167
156
|
this.element = this.page.locator(`input#${id}`);
|
|
168
|
-
|
|
157
|
+
this.log?.(`Stored input with ID "${id}"`);
|
|
169
158
|
});
|
|
170
159
|
When("I find inputs by ID {string}", async function (id) {
|
|
171
|
-
var _a;
|
|
172
160
|
this.elements = this.page.locator(`input#${id}`);
|
|
173
|
-
|
|
161
|
+
this.log?.(`Stored multiple inputs with ID "${id}"`);
|
|
174
162
|
});
|
|
175
163
|
When("I find textareas by placeholder text {string}", async function (placeholder) {
|
|
176
|
-
var _a;
|
|
177
164
|
this.elements = this.page.locator(`textarea[placeholder="${placeholder}"]`);
|
|
178
|
-
|
|
165
|
+
this.log?.(`Stored multiple textareas with placeholder "${placeholder}"`);
|
|
179
166
|
});
|
|
180
167
|
When("I find input by label text {string}", async function (label) {
|
|
181
|
-
var _a;
|
|
182
168
|
this.element = this.page.getByLabel(label);
|
|
183
|
-
|
|
169
|
+
this.log?.(`Stored input with label "${label}"`);
|
|
184
170
|
});
|
|
185
171
|
When("I find input by name {string}", async function (name) {
|
|
186
|
-
var _a;
|
|
187
172
|
this.element = this.page.locator(`input[name="${name}"]`);
|
|
188
|
-
|
|
173
|
+
this.log?.(`Stored input with name "${name}"`);
|
|
189
174
|
});
|
|
190
175
|
When("I find input by placeholder text {string}", async function (placeholder) {
|
|
191
|
-
var _a;
|
|
192
176
|
this.element = this.page.getByPlaceholder(placeholder);
|
|
193
|
-
|
|
177
|
+
this.log?.(`Stored input with placeholder "${placeholder}"`);
|
|
194
178
|
});
|
|
195
179
|
When("I find inputs by name {string}", async function (name) {
|
|
196
|
-
var _a;
|
|
197
180
|
this.elements = this.page.locator(`input[name="${name}"]`);
|
|
198
|
-
|
|
181
|
+
this.log?.(`Stored multiple inputs with name "${name}"`);
|
|
199
182
|
});
|
|
200
183
|
When("I find inputs by placeholder text {string}", async function (placeholder) {
|
|
201
|
-
var _a;
|
|
202
184
|
this.elements = this.page.locator(`input[placeholder="${placeholder}"]`);
|
|
203
|
-
|
|
185
|
+
this.log?.(`Stored multiple inputs with placeholder "${placeholder}"`);
|
|
204
186
|
});
|
|
205
187
|
When("I find inputs by label text {string}", async function (label) {
|
|
206
|
-
var _a;
|
|
207
188
|
this.elements = this.page.locator(`label:has-text("${label}") + input`);
|
|
208
|
-
|
|
189
|
+
this.log?.(`Stored multiple inputs with label "${label}"`);
|
|
209
190
|
});
|
|
210
191
|
When("I find inputs by display value {string}", async function (value) {
|
|
211
|
-
var _a, _b;
|
|
212
192
|
// 🧠 Handle alias
|
|
213
193
|
if (value.startsWith("@")) {
|
|
214
194
|
const alias = value.slice(1);
|
|
215
|
-
value =
|
|
195
|
+
value = this.data?.[alias];
|
|
216
196
|
if (!value) {
|
|
217
197
|
throw new Error(`No value found for alias "@${alias}"`);
|
|
218
198
|
}
|
|
219
199
|
}
|
|
220
200
|
// 🔍 Find all matching inputs
|
|
221
201
|
this.elements = this.page.locator(`input[value="${value}"]`);
|
|
222
|
-
|
|
202
|
+
this.log?.(`📦 Stored multiple inputs with display value "${value}"`);
|
|
223
203
|
});
|
|
224
204
|
When("I find input by display value {string}", async function (value) {
|
|
225
|
-
var _a, _b;
|
|
226
205
|
// 🧠 Handle alias
|
|
227
206
|
if (value.startsWith("@")) {
|
|
228
207
|
const alias = value.slice(1);
|
|
229
|
-
value =
|
|
208
|
+
value = this.data?.[alias];
|
|
230
209
|
if (!value) {
|
|
231
210
|
throw new Error(`No value found for alias "@${alias}"`);
|
|
232
211
|
}
|
|
@@ -235,5 +214,5 @@ When("I find input by display value {string}", async function (value) {
|
|
|
235
214
|
const locator = this.page.locator(`input[value="${value}"]`);
|
|
236
215
|
await expect(locator).toBeVisible({ timeout: 5000 });
|
|
237
216
|
this.element = locator;
|
|
238
|
-
|
|
217
|
+
this.log?.(`🔍 Found input with value: "${value}"`);
|
|
239
218
|
});
|
|
@@ -6,37 +6,34 @@ import fs from "fs";
|
|
|
6
6
|
import { evaluateFaker } from "../helpers/utils/fakerUtils";
|
|
7
7
|
import { parseClickOptions, parseCheckOptions, parseFillOptions, parseSelectOptions, } from "../helpers/utils/optionsUtils";
|
|
8
8
|
When("I check", async function (...rest) {
|
|
9
|
-
var _a, _b;
|
|
10
9
|
const maybeTable = rest[0];
|
|
11
|
-
const options =
|
|
12
|
-
await
|
|
13
|
-
|
|
10
|
+
const options = maybeTable?.rowsHash ? parseCheckOptions(maybeTable) : {};
|
|
11
|
+
await this.element?.check(options);
|
|
12
|
+
this.log?.("✅ Checked stored checkbox");
|
|
14
13
|
});
|
|
15
14
|
When("I uncheck", async function (...rest) {
|
|
16
|
-
var _a, _b;
|
|
17
15
|
const maybeTable = rest[0];
|
|
18
|
-
const options =
|
|
19
|
-
await
|
|
20
|
-
|
|
16
|
+
const options = maybeTable?.rowsHash ? parseCheckOptions(maybeTable) : {};
|
|
17
|
+
await this.element?.uncheck(options);
|
|
18
|
+
this.log?.("✅ Unchecked stored checkbox");
|
|
21
19
|
});
|
|
22
20
|
When("I check input", async function (...rest) {
|
|
23
21
|
const maybeTable = rest[0];
|
|
24
|
-
const options =
|
|
22
|
+
const options = maybeTable?.rowsHash ? parseCheckOptions(maybeTable) : {};
|
|
25
23
|
if (!this.element)
|
|
26
24
|
throw new Error("No input selected");
|
|
27
25
|
await this.element.check(options);
|
|
28
26
|
});
|
|
29
27
|
When("I uncheck input", async function (...rest) {
|
|
30
28
|
const maybeTable = rest[0];
|
|
31
|
-
const options =
|
|
29
|
+
const options = maybeTable?.rowsHash ? parseCheckOptions(maybeTable) : {};
|
|
32
30
|
if (!this.element)
|
|
33
31
|
throw new Error("No input selected");
|
|
34
32
|
await this.element.uncheck(options);
|
|
35
33
|
});
|
|
36
34
|
When("I fill the following {string} form data:", async function (_formName, tableData, ...rest) {
|
|
37
|
-
var _a, _b, _c, _d;
|
|
38
35
|
const maybeOptionsTable = rest[0];
|
|
39
|
-
const globalOptions =
|
|
36
|
+
const globalOptions = maybeOptionsTable?.rowsHash
|
|
40
37
|
? parseClickOptions(maybeOptionsTable)
|
|
41
38
|
: {};
|
|
42
39
|
const rows = tableData.raw().slice(1); // Remove header row
|
|
@@ -67,100 +64,96 @@ When("I fill the following {string} form data:", async function (_formName, tabl
|
|
|
67
64
|
}
|
|
68
65
|
if (resolvedValue === "Click") {
|
|
69
66
|
await element.click(globalOptions);
|
|
70
|
-
|
|
67
|
+
this.log?.(`🖱️ Clicked on "${rawTarget}"`);
|
|
71
68
|
}
|
|
72
69
|
else if (rawTarget.includes('input[type="file"]')) {
|
|
73
70
|
const filePath = path.resolve("test-data", resolvedValue);
|
|
74
71
|
if (!fs.existsSync(filePath))
|
|
75
72
|
throw new Error(`File not found: ${filePath}`);
|
|
76
73
|
await element.setInputFiles(filePath);
|
|
77
|
-
|
|
74
|
+
this.log?.(`📁 Uploaded file to "${rawTarget}": ${resolvedValue}`);
|
|
78
75
|
}
|
|
79
76
|
else {
|
|
80
77
|
const keyPattern = /{(\w+)}/g;
|
|
81
78
|
const matches = [...resolvedValue.matchAll(keyPattern)];
|
|
82
79
|
const keys = matches.map((m) => m[1]);
|
|
83
80
|
const inputText = resolvedValue.replace(keyPattern, "").trim();
|
|
84
|
-
const fillOptions =
|
|
81
|
+
const fillOptions = maybeOptionsTable?.rowsHash
|
|
85
82
|
? parseFillOptions(maybeOptionsTable)
|
|
86
83
|
: {};
|
|
87
84
|
if (inputText) {
|
|
88
85
|
await element.fill(inputText, fillOptions);
|
|
89
|
-
|
|
86
|
+
this.log?.(`⌨️ Filled "${rawTarget}" with: ${inputText}`);
|
|
90
87
|
}
|
|
91
88
|
for (const key of keys) {
|
|
92
89
|
await element.focus();
|
|
93
90
|
await this.page.waitForTimeout(200);
|
|
94
91
|
await element.press(key);
|
|
95
|
-
|
|
92
|
+
this.log?.(`🎹 Pressed {${key}} on "${rawTarget}"`);
|
|
96
93
|
}
|
|
97
94
|
}
|
|
98
95
|
await expect(element).toBeVisible();
|
|
99
96
|
}
|
|
100
97
|
});
|
|
101
98
|
const typeStep = async function (textOrAlias, ...rest) {
|
|
102
|
-
var _a, _b;
|
|
103
99
|
if (!this.element)
|
|
104
100
|
throw new Error("No element selected");
|
|
105
101
|
const maybeTable = rest[0];
|
|
106
|
-
const options =
|
|
102
|
+
const options = maybeTable?.rowsHash ? parseFillOptions(maybeTable) : {};
|
|
107
103
|
const text = textOrAlias.startsWith("@")
|
|
108
|
-
?
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
? this.data[textOrAlias.slice(1)] ??
|
|
105
|
+
(() => {
|
|
106
|
+
throw new Error(`No value found for alias "${textOrAlias}"`);
|
|
107
|
+
})()
|
|
111
108
|
: evaluateFaker(textOrAlias);
|
|
112
109
|
await this.element.fill("");
|
|
113
110
|
await this.element.fill(text, options);
|
|
114
111
|
this.data.lastTyped = text;
|
|
115
|
-
|
|
112
|
+
this.log?.(`⌨️ Typed "${text}" into selected element`);
|
|
116
113
|
};
|
|
117
114
|
When("I type {string}", typeStep);
|
|
118
115
|
When("I type stored {string}", typeStep);
|
|
119
116
|
When("I type random {string}", typeStep);
|
|
120
117
|
When("I set value {string}", async function (valueOrAlias, ...rest) {
|
|
121
|
-
var _a, _b;
|
|
122
118
|
if (!this.element)
|
|
123
119
|
throw new Error("No element selected");
|
|
124
120
|
const maybeTable = rest[0];
|
|
125
|
-
const options =
|
|
121
|
+
const options = maybeTable?.rowsHash ? parseFillOptions(maybeTable) : {};
|
|
126
122
|
const value = valueOrAlias.startsWith("@")
|
|
127
|
-
?
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
? this.data[valueOrAlias.slice(1)] ??
|
|
124
|
+
(() => {
|
|
125
|
+
throw new Error(`No value found for alias "${valueOrAlias}"`);
|
|
126
|
+
})()
|
|
130
127
|
: evaluateFaker(valueOrAlias);
|
|
131
128
|
await this.element.fill(value, options);
|
|
132
129
|
this.data.lastValueSet = value;
|
|
133
|
-
|
|
130
|
+
this.log?.(`📝 Set value to "${value}"`);
|
|
134
131
|
});
|
|
135
132
|
When("I clear", async function () {
|
|
136
|
-
var _a;
|
|
137
133
|
if (!this.element)
|
|
138
134
|
throw new Error("No element selected");
|
|
139
135
|
await this.element.fill("");
|
|
140
|
-
|
|
136
|
+
this.log?.("🧼 Cleared value of selected element");
|
|
141
137
|
});
|
|
142
138
|
When("I submit", async function (...rest) {
|
|
143
|
-
var _a, _b;
|
|
144
139
|
const maybeTable = rest[0];
|
|
145
|
-
const form =
|
|
140
|
+
const form = this.element ?? this.page.locator("form");
|
|
146
141
|
await form.evaluate((f) => f.submit());
|
|
147
|
-
|
|
142
|
+
this.log?.("📨 Submitted form");
|
|
148
143
|
});
|
|
149
144
|
When("I select option {string}", async function (option, ...rest) {
|
|
150
|
-
var _a;
|
|
151
145
|
if (!this.element)
|
|
152
146
|
throw new Error("No select element stored");
|
|
153
147
|
const maybeTable = rest[0];
|
|
154
|
-
const options =
|
|
148
|
+
const options = maybeTable?.rowsHash ? parseSelectOptions(maybeTable) : {};
|
|
155
149
|
await this.element.selectOption({ label: option }, options);
|
|
156
|
-
|
|
150
|
+
this.log?.(`🔽 Selected option "${option}"`);
|
|
157
151
|
});
|
|
158
152
|
When("I select file {string}", async function (filePath, ...rest) {
|
|
159
|
-
var _a;
|
|
160
153
|
if (!this.element)
|
|
161
154
|
throw new Error("No file input selected");
|
|
162
155
|
const maybeTable = rest[0];
|
|
163
|
-
const options =
|
|
156
|
+
const options = maybeTable?.rowsHash ? parseSelectOptions(maybeTable) : {};
|
|
164
157
|
await this.element.setInputFiles(filePath, options);
|
|
165
|
-
|
|
158
|
+
this.log?.(`📁 Set input file to "${filePath}"`);
|
|
166
159
|
});
|
|
@@ -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/dist/index.d.ts
CHANGED
|
@@ -8,19 +8,19 @@ export * from "./actions/miscSteps";
|
|
|
8
8
|
export * from "./actions/mouseSteps";
|
|
9
9
|
export * from "./actions/scrollSteps";
|
|
10
10
|
export * from "./actions/storageSteps";
|
|
11
|
-
export * from "./assertions/
|
|
11
|
+
export * from "./assertions/buttonAndTextVisibilitySteps";
|
|
12
12
|
export * from "./assertions/cookieSteps";
|
|
13
13
|
export * from "./assertions/elementSteps";
|
|
14
14
|
export * from "./assertions/formInputSteps";
|
|
15
|
-
export * from "./assertions/
|
|
15
|
+
export * from "./assertions/interceptionRequestsSteps";
|
|
16
16
|
export * from "./assertions/locationSteps";
|
|
17
17
|
export * from "./assertions/roleTestIdSteps";
|
|
18
18
|
export * from "./assertions/semanticSteps";
|
|
19
19
|
export * from "./assertions/storageSteps";
|
|
20
20
|
export * from "./assertions/visualSteps";
|
|
21
|
-
export * from "./custom_setups/global-login";
|
|
22
|
-
export * from "./custom_setups/loginHooks";
|
|
23
21
|
export * from "./iframes/frames";
|
|
22
|
+
export * from "./custom_setups/globalLogin";
|
|
23
|
+
export * from "./custom_setups/loginHooks";
|
|
24
24
|
export * from "./helpers/compareSnapshots";
|
|
25
25
|
export * from "./helpers/hooks";
|
|
26
26
|
export * from "./helpers/utils";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Actions
|
|
2
2
|
export * from "./actions/clickSteps";
|
|
3
3
|
export * from "./actions/cookieSteps";
|
|
4
4
|
export * from "./actions/debugSteps";
|
|
@@ -9,20 +9,23 @@ export * from "./actions/miscSteps";
|
|
|
9
9
|
export * from "./actions/mouseSteps";
|
|
10
10
|
export * from "./actions/scrollSteps";
|
|
11
11
|
export * from "./actions/storageSteps";
|
|
12
|
-
|
|
12
|
+
// Assertions
|
|
13
|
+
export * from "./assertions/buttonAndTextVisibilitySteps";
|
|
13
14
|
export * from "./assertions/cookieSteps";
|
|
14
15
|
export * from "./assertions/elementSteps";
|
|
15
16
|
export * from "./assertions/formInputSteps";
|
|
16
|
-
export * from "./assertions/
|
|
17
|
+
export * from "./assertions/interceptionRequestsSteps";
|
|
17
18
|
export * from "./assertions/locationSteps";
|
|
18
19
|
export * from "./assertions/roleTestIdSteps";
|
|
19
20
|
export * from "./assertions/semanticSteps";
|
|
20
21
|
export * from "./assertions/storageSteps";
|
|
21
22
|
export * from "./assertions/visualSteps";
|
|
22
|
-
|
|
23
|
-
export * from "./custom_setups/loginHooks";
|
|
23
|
+
// Iframes
|
|
24
24
|
export * from "./iframes/frames";
|
|
25
|
-
//
|
|
25
|
+
// Setup (custom hooks, login, etc.)
|
|
26
|
+
export * from "./custom_setups/globalLogin";
|
|
27
|
+
export * from "./custom_setups/loginHooks";
|
|
28
|
+
// Core helpers and utilities
|
|
26
29
|
export * from "./helpers/compareSnapshots";
|
|
27
30
|
export * from "./helpers/hooks";
|
|
28
31
|
export * from "./helpers/utils";
|
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
|
},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|