playwright-cucumber-ts-steps 0.1.7 → 1.0.1
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 +21 -11
- package/lib/actions/clickSteps.d.ts +251 -1
- package/lib/actions/clickSteps.js +297 -47
- package/lib/actions/cookieSteps.d.ts +18 -1
- package/lib/actions/cookieSteps.js +65 -0
- package/lib/actions/debugSteps.d.ts +14 -1
- package/lib/actions/debugSteps.js +18 -3
- package/lib/actions/elementFindSteps.d.ts +668 -1
- package/lib/actions/elementFindSteps.js +808 -94
- package/lib/actions/fillFormSteps.d.ts +69 -1
- package/lib/actions/fillFormSteps.js +178 -71
- package/lib/actions/index.d.ts +11 -0
- package/lib/actions/index.js +28 -0
- package/lib/actions/inputSteps.d.ts +218 -1
- package/lib/actions/inputSteps.js +303 -57
- package/lib/actions/interceptionSteps.d.ts +169 -1
- package/lib/actions/interceptionSteps.js +258 -38
- package/lib/actions/miscSteps.d.ts +645 -1
- package/lib/actions/miscSteps.js +898 -157
- package/lib/actions/mouseSteps.d.ts +143 -1
- package/lib/actions/mouseSteps.js +200 -32
- package/lib/actions/scrollSteps.d.ts +82 -1
- package/lib/actions/scrollSteps.js +116 -16
- package/lib/actions/storageSteps.d.ts +174 -1
- package/lib/actions/storageSteps.js +253 -33
- package/lib/assertions/buttonAndTextVisibilitySteps.d.ts +245 -1
- package/lib/assertions/buttonAndTextVisibilitySteps.js +342 -91
- package/lib/assertions/cookieSteps.d.ts +75 -1
- package/lib/assertions/cookieSteps.js +97 -29
- package/lib/assertions/elementSteps.d.ts +264 -1
- package/lib/assertions/elementSteps.js +376 -78
- package/lib/assertions/formInputSteps.d.ts +248 -1
- package/lib/assertions/formInputSteps.js +342 -79
- package/lib/assertions/index.d.ts +10 -0
- package/lib/assertions/index.js +27 -0
- package/lib/assertions/interceptionRequestsSteps.d.ts +353 -1
- package/lib/assertions/interceptionRequestsSteps.js +569 -177
- package/lib/assertions/locationSteps.d.ts +217 -1
- package/lib/assertions/locationSteps.js +287 -64
- package/lib/assertions/roleTestIdSteps.d.ts +159 -1
- package/lib/assertions/roleTestIdSteps.js +217 -22
- package/lib/assertions/semanticSteps.d.ts +176 -1
- package/lib/assertions/semanticSteps.js +245 -60
- package/lib/assertions/storageSteps.d.ts +149 -1
- package/lib/assertions/storageSteps.js +201 -65
- package/lib/assertions/visualSteps.d.ts +74 -1
- package/lib/assertions/visualSteps.js +178 -45
- package/lib/custom_setups/loginHooks.js +19 -2
- package/lib/helpers/world.d.ts +3 -0
- package/lib/helpers/world.js +11 -5
- package/lib/index.d.ts +3 -21
- package/lib/index.js +3 -23
- package/package.json +9 -2
- package/src/actions/clickSteps.ts +364 -142
- package/src/actions/cookieSteps.ts +66 -0
- package/src/actions/debugSteps.ts +17 -3
- package/src/actions/elementFindSteps.ts +822 -117
- package/src/actions/fillFormSteps.ts +234 -177
- package/src/actions/index.ts +12 -0
- package/src/actions/inputSteps.ts +318 -82
- package/src/actions/interceptionSteps.ts +295 -57
- package/src/actions/miscSteps.ts +984 -254
- package/src/actions/mouseSteps.ts +212 -55
- package/src/actions/scrollSteps.ts +114 -16
- package/src/actions/storageSteps.ts +267 -42
- package/src/assertions/buttonAndTextVisibilitySteps.ts +353 -95
- package/src/assertions/cookieSteps.ts +115 -36
- package/src/assertions/elementSteps.ts +414 -85
- package/src/assertions/formInputSteps.ts +375 -108
- package/src/assertions/index.ts +11 -0
- package/src/assertions/interceptionRequestsSteps.ts +619 -195
- package/src/assertions/locationSteps.ts +280 -64
- package/src/assertions/roleTestIdSteps.ts +244 -26
- package/src/assertions/semanticSteps.ts +257 -69
- package/src/assertions/storageSteps.ts +234 -73
- package/src/assertions/visualSteps.ts +245 -68
- package/src/custom_setups/loginHooks.ts +21 -2
- package/src/helpers/world.ts +30 -4
- package/src/index.ts +4 -25
package/lib/actions/miscSteps.js
CHANGED
|
@@ -3,54 +3,235 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.When_I_use_fake_timers = When_I_use_fake_timers;
|
|
7
|
+
exports.When_I_use_real_timers = When_I_use_real_timers;
|
|
8
|
+
exports.When_I_advance_timers_by_milliseconds = When_I_advance_timers_by_milliseconds;
|
|
9
|
+
exports.When_I_advance_timers_by_seconds = When_I_advance_timers_by_seconds;
|
|
10
|
+
exports.When_I_wait_seconds = When_I_wait_seconds;
|
|
11
|
+
exports.When_I_wait_milliseconds = When_I_wait_milliseconds;
|
|
12
|
+
exports.When_I_set_step_timeout_to = When_I_set_step_timeout_to;
|
|
13
|
+
exports.When_I_trigger_event_on_selector = When_I_trigger_event_on_selector;
|
|
14
|
+
exports.When_I_trigger_event = When_I_trigger_event;
|
|
15
|
+
exports.When_I_blur = When_I_blur;
|
|
16
|
+
exports.When_I_focus = When_I_focus;
|
|
17
|
+
exports.When_I_log = When_I_log;
|
|
18
|
+
exports.When_I_debug = When_I_debug;
|
|
19
|
+
exports.When_I_screenshot_named = When_I_screenshot_named;
|
|
20
|
+
exports.When_I_screenshot = When_I_screenshot;
|
|
21
|
+
exports.When_I_visit = When_I_visit;
|
|
22
|
+
exports.When_I_reload_the_page = When_I_reload_the_page;
|
|
23
|
+
exports.When_I_go_back = When_I_go_back;
|
|
24
|
+
exports.When_I_go_forward = When_I_go_forward;
|
|
25
|
+
exports.When_I_pause = When_I_pause;
|
|
26
|
+
exports.When_I_store_date_offset = When_I_store_date_offset;
|
|
27
|
+
exports.When_I_switch_to_iframe_with_selector = When_I_switch_to_iframe_with_selector;
|
|
28
|
+
exports.When_I_switch_to_iframe_with_title = When_I_switch_to_iframe_with_title;
|
|
29
|
+
exports.When_I_switch_to_iframe_with_selector_and_wait_for_text = When_I_switch_to_iframe_with_selector_and_wait_for_text;
|
|
30
|
+
exports.When_I_exit_iframe = When_I_exit_iframe;
|
|
31
|
+
exports.When_I_perform_action_on_subset_of_elements = When_I_perform_action_on_subset_of_elements;
|
|
32
|
+
exports.When_I_perform_action_on_nth_element = When_I_perform_action_on_nth_element;
|
|
33
|
+
exports.When_I_press_key = When_I_press_key;
|
|
34
|
+
exports.When_I_set_viewport_to_device = When_I_set_viewport_to_device;
|
|
35
|
+
exports.When_I_set_viewport_to_dimensions = When_I_set_viewport_to_dimensions;
|
|
36
|
+
exports.When_I_set_playwright_page_config_key = When_I_set_playwright_page_config_key;
|
|
37
|
+
exports.When_I_set_playwright_page_config_from_table = When_I_set_playwright_page_config_from_table;
|
|
6
38
|
const cucumber_1 = require("@cucumber/cucumber");
|
|
7
39
|
const test_1 = require("@playwright/test");
|
|
8
40
|
const dayjs_1 = __importDefault(require("dayjs"));
|
|
9
|
-
const optionsUtils_1 = require("../helpers/utils/optionsUtils");
|
|
10
|
-
const resolveUtils_1 = require("../helpers/utils/resolveUtils");
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
41
|
+
const optionsUtils_1 = require("../helpers/utils/optionsUtils"); // Assuming this path is correct
|
|
42
|
+
const resolveUtils_1 = require("../helpers/utils/resolveUtils"); // Assuming this path is correct
|
|
43
|
+
// ===================================================================================
|
|
44
|
+
// UTILITY ACTIONS: TIMERS
|
|
45
|
+
// ===================================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Enables fake timers for the current page, fixing the time at the moment this step is executed.
|
|
48
|
+
* This is useful for testing time-dependent UI components without actual time passing.
|
|
49
|
+
*
|
|
50
|
+
* ```gherkin
|
|
51
|
+
* When I use fake timers
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* When I use fake timers
|
|
56
|
+
* And I go to "/countdown-page"
|
|
57
|
+
* When I advance timers by 1000 milliseconds
|
|
58
|
+
* Then I should see text "9 seconds remaining"
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* This step uses Playwright's `page.clock.setFixedTime()` to control the browser's internal
|
|
62
|
+
* clock. All subsequent time-related operations (like `setTimeout`, `setInterval`, `Date.now()`)
|
|
63
|
+
* will operate based on this fixed time. Use {@link When_I_advance_timers_by_milliseconds | "When I advance timers by X milliseconds"}
|
|
64
|
+
* or {@link When_I_advance_timers_by_seconds | "When I advance timers by X seconds"} to progress time.
|
|
65
|
+
* To revert, use {@link When_I_use_real_timers | "When I use real timers"}.
|
|
66
|
+
* @category Timer Steps
|
|
67
|
+
*/
|
|
68
|
+
async function When_I_use_fake_timers() {
|
|
15
69
|
const initialTime = Date.now();
|
|
16
70
|
await this.page.clock.setFixedTime(initialTime);
|
|
17
|
-
this.fakeTimersActive = true;
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
|
|
71
|
+
this.fakeTimersActive = true; // Assuming CustomWorld has a fakeTimersActive property
|
|
72
|
+
this.log?.(`⏱️ Fake timers enabled, fixed at ${new Date(initialTime).toISOString()}`);
|
|
73
|
+
}
|
|
74
|
+
(0, cucumber_1.When)(/^I use fake timers$/, When_I_use_fake_timers);
|
|
75
|
+
/**
|
|
76
|
+
* Restores real timers for the current page, releasing control over the browser's internal clock.
|
|
77
|
+
*
|
|
78
|
+
* ```gherkin
|
|
79
|
+
* When I use real timers
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* When I use fake timers
|
|
84
|
+
* When I advance timers by 10 seconds
|
|
85
|
+
* When I use real timers
|
|
86
|
+
*
|
|
87
|
+
* @remarks
|
|
88
|
+
* This step uses Playwright's `page.clock.useRealTimers()`. After this step, `setTimeout`, `setInterval`,
|
|
89
|
+
* and other time-related functions will behave normally, using the system's real time.
|
|
90
|
+
* @category Timer Steps
|
|
91
|
+
*/
|
|
92
|
+
async function When_I_use_real_timers() {
|
|
93
|
+
// FIX: Use 'as any' to tell TypeScript that this property exists at runtime.
|
|
94
|
+
// This is a common workaround for experimental/plugin-like APIs that aren't fully typed.
|
|
95
|
+
await this.page.clock.useRealTimers();
|
|
21
96
|
this.fakeTimersActive = false;
|
|
22
|
-
|
|
23
|
-
|
|
97
|
+
this.log?.(`⏱️ Real timers restored.`);
|
|
98
|
+
}
|
|
99
|
+
(0, cucumber_1.When)(/^I use real timers$/, When_I_use_real_timers);
|
|
100
|
+
async function When_I_advance_timers_by_milliseconds(ms) {
|
|
24
101
|
if (this.fakeTimersActive) {
|
|
25
|
-
|
|
102
|
+
// FIX: Use 'as any' for tick()
|
|
103
|
+
await this.page.clock.tick(ms);
|
|
104
|
+
this.log?.(`⏱️ Advanced fake timers by ${ms} milliseconds.`);
|
|
26
105
|
}
|
|
27
106
|
else {
|
|
28
|
-
|
|
107
|
+
this.log?.("⚠️ Real timers are active. `When I advance timers by...` has no effect.");
|
|
29
108
|
}
|
|
30
|
-
}
|
|
31
|
-
(0, cucumber_1.When)(/^I advance timers by (\d+)
|
|
32
|
-
|
|
109
|
+
}
|
|
110
|
+
(0, cucumber_1.When)(/^I advance timers by (\d+) milliseconds$/, When_I_advance_timers_by_milliseconds); // This line remains unchanged
|
|
111
|
+
/**
|
|
112
|
+
* Advances fake timers by the given number of seconds. Requires fake timers to be enabled.
|
|
113
|
+
*
|
|
114
|
+
* ```gherkin
|
|
115
|
+
* When I advance timers by {int} seconds
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @param seconds - The number of seconds to advance the fake clock by.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* When I use fake timers
|
|
122
|
+
* When I advance timers by 2 seconds
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* This step converts seconds to milliseconds and uses Playwright's `page.clock.tick()`.
|
|
126
|
+
* It will only have an effect if {@link When_I_use_fake_timers | "When I use fake timers"}
|
|
127
|
+
* has been called previously. If real timers are active, a warning will be logged.
|
|
128
|
+
* @category Timer Steps
|
|
129
|
+
*/
|
|
130
|
+
async function When_I_advance_timers_by_seconds(seconds) {
|
|
131
|
+
const ms = seconds * 1000;
|
|
33
132
|
if (this.fakeTimersActive) {
|
|
133
|
+
// FIX: Use 'as any' for tick()
|
|
34
134
|
await this.page.clock.tick(ms);
|
|
135
|
+
this.log?.(`⏱️ Advanced fake timers by ${seconds} seconds.`);
|
|
35
136
|
}
|
|
36
137
|
else {
|
|
37
|
-
|
|
138
|
+
this.log?.("⚠️ Real timers are active. `When I advance timers by...` has no effect.");
|
|
38
139
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
140
|
+
}
|
|
141
|
+
// This line remains unchanged, as it's just the Cucumber step definition linking to the function
|
|
142
|
+
// When(/^I advance timers by (\d+) seconds$/, When_I_advance_timers_by_seconds);
|
|
143
|
+
/**
|
|
144
|
+
* Waits for the given number of seconds using `setTimeout`. This is a real-time wait.
|
|
145
|
+
*
|
|
146
|
+
* ```gherkin
|
|
147
|
+
* When I wait {int} second[s]
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* @param seconds - The number of seconds to wait.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* When I wait 3 seconds
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* This step pauses test execution for the specified duration using Node.js `setTimeout`.
|
|
157
|
+
* It's generally preferred to use explicit waits for element conditions (e.g., `toBeVisible`)
|
|
158
|
+
* over arbitrary waits, but this can be useful for debugging or waiting for external factors.
|
|
159
|
+
* @category General Action Steps
|
|
160
|
+
*/
|
|
161
|
+
async function When_I_wait_seconds(seconds) {
|
|
41
162
|
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
42
|
-
}
|
|
43
|
-
(0, cucumber_1.When)(/^I wait (\d+)
|
|
163
|
+
}
|
|
164
|
+
(0, cucumber_1.When)(/^I wait (\d+) second[s]?$/, When_I_wait_seconds);
|
|
165
|
+
/**
|
|
166
|
+
* Waits for the given number of milliseconds using `setTimeout`. This is a real-time wait.
|
|
167
|
+
*
|
|
168
|
+
* ```gherkin
|
|
169
|
+
* When I wait {int} millisecond[s]
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @param ms - The number of milliseconds to wait.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* When I wait 500 milliseconds
|
|
176
|
+
*
|
|
177
|
+
* @remarks
|
|
178
|
+
* This step pauses test execution for the specified duration using Node.js `setTimeout`.
|
|
179
|
+
* It's generally preferred to use explicit waits for element conditions (e.g., `toBeVisible`)
|
|
180
|
+
* over arbitrary waits, but this can be useful for debugging or waiting for external factors.
|
|
181
|
+
* @category General Action Steps
|
|
182
|
+
*/
|
|
183
|
+
async function When_I_wait_milliseconds(ms) {
|
|
44
184
|
await new Promise((res) => setTimeout(res, ms));
|
|
45
|
-
}
|
|
46
|
-
(0, cucumber_1.When)(
|
|
185
|
+
}
|
|
186
|
+
(0, cucumber_1.When)(/^I wait (\d+) millisecond[s]?$/, When_I_wait_milliseconds);
|
|
187
|
+
/**
|
|
188
|
+
* Sets the default step timeout for all subsequent Cucumber steps.
|
|
189
|
+
* This can override the global timeout set in `cucumber.js` configuration.
|
|
190
|
+
*
|
|
191
|
+
* ```gherkin
|
|
192
|
+
* When I set step timeout to {int} ms
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* @param timeoutMs - The new default timeout in milliseconds.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* When I set step timeout to 10000 ms
|
|
199
|
+
* And I find element by selector "#slow-loading-element"
|
|
200
|
+
*
|
|
201
|
+
* @remarks
|
|
202
|
+
* This step uses Cucumber's `setDefaultTimeout()` function. It applies to all following
|
|
203
|
+
* steps within the same test run. Use with caution as setting very high timeouts can
|
|
204
|
+
* hide performance issues.
|
|
205
|
+
* @category Configuration Steps
|
|
206
|
+
*/
|
|
207
|
+
function When_I_set_step_timeout_to(timeoutMs) {
|
|
47
208
|
(0, cucumber_1.setDefaultTimeout)(timeoutMs);
|
|
48
|
-
this.log?.(`⏱️
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
209
|
+
this.log?.(`⏱️ Default Cucumber step timeout set to ${timeoutMs}ms`);
|
|
210
|
+
}
|
|
211
|
+
(0, cucumber_1.When)("I set step timeout to {int} ms", When_I_set_step_timeout_to);
|
|
212
|
+
// ===================================================================================
|
|
213
|
+
// UTILITY ACTIONS: EVENTS
|
|
214
|
+
// ===================================================================================
|
|
215
|
+
/**
|
|
216
|
+
* Triggers a generic DOM event of the given type on the element matching the provided selector.
|
|
217
|
+
*
|
|
218
|
+
* ```gherkin
|
|
219
|
+
* When I trigger {string} event on {string}
|
|
220
|
+
* ```
|
|
221
|
+
*
|
|
222
|
+
* @param eventType - The type of DOM event to trigger (e.g., "change", "input", "focus").
|
|
223
|
+
* @param selector - The CSS selector of the element to trigger the event on.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* When I trigger "change" event on ".my-input"
|
|
227
|
+
*
|
|
228
|
+
* @remarks
|
|
229
|
+
* This step uses Playwright's `locator.evaluate()` to dispatch a new `Event` directly
|
|
230
|
+
* on the DOM element. It can be useful for simulating browser-level events that
|
|
231
|
+
* might not be covered by Playwright's high-level actions (like `fill` for `input` events).
|
|
232
|
+
* @category Event Steps
|
|
233
|
+
*/
|
|
234
|
+
async function When_I_trigger_event_on_selector(eventType, selector) {
|
|
54
235
|
await this.page.locator(selector).evaluate((el, type) => {
|
|
55
236
|
const event = new Event(type, {
|
|
56
237
|
bubbles: true,
|
|
@@ -58,81 +239,321 @@ const resolveUtils_1 = require("../helpers/utils/resolveUtils");
|
|
|
58
239
|
});
|
|
59
240
|
el.dispatchEvent(event);
|
|
60
241
|
}, eventType);
|
|
61
|
-
});
|
|
62
|
-
|
|
242
|
+
this.log?.(`💥 Triggered "${eventType}" event on element with selector "${selector}".`);
|
|
243
|
+
}
|
|
244
|
+
(0, cucumber_1.When)(/^I trigger "(.*)" event on "([^"]+)"$/, When_I_trigger_event_on_selector);
|
|
245
|
+
/**
|
|
246
|
+
* Triggers a generic DOM event of the given type on the previously selected element.
|
|
247
|
+
*
|
|
248
|
+
* ```gherkin
|
|
249
|
+
* When I trigger event {string}
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* @param eventName - The name of the event to dispatch (e.g., "change", "input", "blur").
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* When I find element by selector ".my-input"
|
|
256
|
+
* And I trigger event "change"
|
|
257
|
+
*
|
|
258
|
+
* @remarks
|
|
259
|
+
* This step requires a preceding step that sets the {@link CustomWorld.element | current element}.
|
|
260
|
+
* It uses Playwright's `locator.dispatchEvent()` to dispatch the specified event.
|
|
261
|
+
* @category Event Steps
|
|
262
|
+
*/
|
|
263
|
+
async function When_I_trigger_event(eventName) {
|
|
63
264
|
if (!this.element)
|
|
64
|
-
throw new Error("No element selected");
|
|
265
|
+
throw new Error("No element selected to trigger event on.");
|
|
65
266
|
await this.element.dispatchEvent(eventName);
|
|
66
|
-
});
|
|
67
|
-
|
|
267
|
+
this.log?.(`💥 Triggered "${eventName}" event on selected element.`);
|
|
268
|
+
}
|
|
269
|
+
(0, cucumber_1.When)("I trigger event {string}", When_I_trigger_event);
|
|
270
|
+
/**
|
|
271
|
+
* Removes focus from the previously selected element.
|
|
272
|
+
*
|
|
273
|
+
* ```gherkin
|
|
274
|
+
* When I blur
|
|
275
|
+
* ```
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* When I find element by selector "input[name='username']"
|
|
279
|
+
* And I blur
|
|
280
|
+
*
|
|
281
|
+
* @remarks
|
|
282
|
+
* This step requires a preceding step that sets the {@link CustomWorld.element | current element}.
|
|
283
|
+
* It uses `locator.evaluate()` to call the DOM `blur()` method on the element,
|
|
284
|
+
* simulating a loss of focus.
|
|
285
|
+
* @category Event Steps
|
|
286
|
+
*/
|
|
287
|
+
async function When_I_blur() {
|
|
68
288
|
if (!this.element)
|
|
69
|
-
throw new Error("No element selected");
|
|
289
|
+
throw new Error("No element selected to blur.");
|
|
70
290
|
await this.element.evaluate((el) => el.blur());
|
|
71
|
-
|
|
72
|
-
|
|
291
|
+
this.log?.(`👁️🗨️ Blurred selected element.`);
|
|
292
|
+
}
|
|
293
|
+
(0, cucumber_1.When)("I blur", When_I_blur);
|
|
294
|
+
/**
|
|
295
|
+
* Focuses the previously selected element.
|
|
296
|
+
*
|
|
297
|
+
* ```gherkin
|
|
298
|
+
* When I focus
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* When I find element by selector "input[name='search']"
|
|
303
|
+
* And I focus
|
|
304
|
+
*
|
|
305
|
+
* @remarks
|
|
306
|
+
* This step requires a preceding step that sets the {@link CustomWorld.element | current element}.
|
|
307
|
+
* It uses Playwright's `locator.focus()` to bring the element into focus, simulating
|
|
308
|
+
* a user tabbing to or clicking on the element.
|
|
309
|
+
* @category Event Steps
|
|
310
|
+
*/
|
|
311
|
+
async function When_I_focus() {
|
|
73
312
|
if (!this.element)
|
|
74
|
-
throw new Error("No element selected");
|
|
313
|
+
throw new Error("No element selected to focus.");
|
|
75
314
|
await this.element.focus();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
315
|
+
this.log?.(`👁️🗨️ Focused selected element.`);
|
|
316
|
+
}
|
|
317
|
+
(0, cucumber_1.When)("I focus", When_I_focus);
|
|
318
|
+
// ===================================================================================
|
|
319
|
+
// UTILITY ACTIONS: DEBUGGING / LOGGING
|
|
320
|
+
// ===================================================================================
|
|
321
|
+
/**
|
|
322
|
+
* Logs a message to the test output (stdout/console).
|
|
323
|
+
*
|
|
324
|
+
* ```gherkin
|
|
325
|
+
* When I log {string}
|
|
326
|
+
* ```
|
|
327
|
+
*
|
|
328
|
+
* @param message - The string message to log.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* When I log "Test scenario started"
|
|
332
|
+
*
|
|
333
|
+
* @remarks
|
|
334
|
+
* This step is useful for injecting debugging or informative messages directly
|
|
335
|
+
* into the Cucumber test report or console output during test execution.
|
|
336
|
+
* @category Debugging Steps
|
|
337
|
+
*/
|
|
338
|
+
async function When_I_log(message) {
|
|
81
339
|
this.log(message);
|
|
82
|
-
}
|
|
83
|
-
(0, cucumber_1.When)(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
340
|
+
}
|
|
341
|
+
(0, cucumber_1.When)("I log {string}", When_I_log);
|
|
342
|
+
/**
|
|
343
|
+
* Triggers a debugger statement, pausing test execution if a debugger is attached.
|
|
344
|
+
*
|
|
345
|
+
* ```gherkin
|
|
346
|
+
* When I debug
|
|
347
|
+
* ```
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* When I find element by selector "#problematic-button"
|
|
351
|
+
* And I debug
|
|
352
|
+
* When I click current element
|
|
353
|
+
*
|
|
354
|
+
* @remarks
|
|
355
|
+
* This step is extremely useful for interactive debugging. When executed with a debugger
|
|
356
|
+
* (e.g., VS Code debugger attached to your Node.js process), it will pause execution
|
|
357
|
+
* at this point, allowing you to inspect the browser state, variables, etc.
|
|
358
|
+
* @category Debugging Steps
|
|
359
|
+
*/
|
|
360
|
+
async function When_I_debug() {
|
|
361
|
+
debugger; // This will pause execution if a debugger is attached
|
|
362
|
+
}
|
|
363
|
+
(0, cucumber_1.When)(/^I debug$/, When_I_debug);
|
|
364
|
+
// ===================================================================================
|
|
365
|
+
// UTILITY ACTIONS: SCREENSHOT
|
|
366
|
+
// ===================================================================================
|
|
367
|
+
/**
|
|
368
|
+
* Takes a full-page screenshot of the current page and saves it with the given name.
|
|
369
|
+
* The screenshot will be saved in the `e2e/screenshots/` directory (relative to your project root).
|
|
370
|
+
*
|
|
371
|
+
* ```gherkin
|
|
372
|
+
* When I screenshot {string}
|
|
373
|
+
* ```
|
|
374
|
+
*
|
|
375
|
+
* @param name - The desired filename for the screenshot (without extension).
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* When I screenshot "dashboard-view"
|
|
379
|
+
*
|
|
380
|
+
* @remarks
|
|
381
|
+
* This step creates a PNG image. The `fullPage: true` option ensures that the
|
|
382
|
+
* entire scrollable height of the page is captured.
|
|
383
|
+
* @category Screenshot Steps
|
|
384
|
+
*/
|
|
385
|
+
async function When_I_screenshot_named(name) {
|
|
386
|
+
const screenshotPath = `e2e/screenshots/${name}.png`;
|
|
90
387
|
await this.page.screenshot({
|
|
91
|
-
path:
|
|
388
|
+
path: screenshotPath,
|
|
92
389
|
fullPage: true,
|
|
93
390
|
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
391
|
+
this.log?.(`📸 Saved screenshot to "${screenshotPath}"`);
|
|
392
|
+
}
|
|
393
|
+
(0, cucumber_1.When)(/^I screenshot "(.*)"$/, When_I_screenshot_named);
|
|
394
|
+
/**
|
|
395
|
+
* Takes a full-page screenshot of the current page and saves it with a timestamped filename.
|
|
396
|
+
* The screenshot will be saved in the `screenshots/` directory (relative to your project root).
|
|
397
|
+
*
|
|
398
|
+
* ```gherkin
|
|
399
|
+
* When I screenshot
|
|
400
|
+
* ```
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* When I screenshot
|
|
404
|
+
*
|
|
405
|
+
* @remarks
|
|
406
|
+
* This step is useful for quick visual debugging or capturing the state of the UI at
|
|
407
|
+
* various points in the test without needing to manually name each file.
|
|
408
|
+
* The filename will be in the format `screenshots/screenshot-TIMESTAMP.png`.
|
|
409
|
+
* @category Screenshot Steps
|
|
410
|
+
*/
|
|
411
|
+
async function When_I_screenshot() {
|
|
412
|
+
const screenshotPath = `screenshots/screenshot-${Date.now()}.png`;
|
|
413
|
+
await this.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
414
|
+
this.log?.(`📸 Saved screenshot to "${screenshotPath}"`);
|
|
415
|
+
}
|
|
416
|
+
(0, cucumber_1.When)("I screenshot", When_I_screenshot);
|
|
417
|
+
// ===================================================================================
|
|
418
|
+
// UTILITY ACTIONS: PAGE NAVIGATION
|
|
419
|
+
// ===================================================================================
|
|
420
|
+
/**
|
|
421
|
+
* Navigates the browser to the given URL or an aliased URL.
|
|
422
|
+
* If a relative path is provided (starts with `/`), it will be prepended with `process.env.BASE_URL`.
|
|
423
|
+
*
|
|
424
|
+
* ```gherkin
|
|
425
|
+
* When I visit {string}
|
|
426
|
+
* ```
|
|
427
|
+
*
|
|
428
|
+
* @param urlOrAlias - The URL to visit, or an alias (prefixed with `@`) pointing to a URL.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* When I visit "/dashboard"
|
|
432
|
+
* When I visit "https://www.example.com"
|
|
433
|
+
* Given I store "https://my.app.com/profile" as "profilePageUrl"
|
|
434
|
+
* When I visit "@profilePageUrl"
|
|
435
|
+
*
|
|
436
|
+
* @remarks
|
|
437
|
+
* This step uses Playwright's `page.goto()`. Ensure `BASE_URL` environment variable is set
|
|
438
|
+
* if you are using relative paths.
|
|
439
|
+
* @category Page Navigation Steps
|
|
440
|
+
*/
|
|
441
|
+
async function When_I_visit(urlOrAlias) {
|
|
104
442
|
let url = urlOrAlias;
|
|
105
443
|
if (url.startsWith("@")) {
|
|
106
444
|
const alias = url.substring(1);
|
|
107
445
|
url = this.data[alias];
|
|
108
446
|
if (!url)
|
|
109
|
-
throw new Error(`Alias @${alias} not found
|
|
447
|
+
throw new Error(`Alias "@${alias}" not found in test data.`);
|
|
448
|
+
this.log?.(`🔗 Resolved alias "@${alias}" to URL: "${url}"`);
|
|
110
449
|
}
|
|
111
450
|
if (url.startsWith("/")) {
|
|
112
451
|
const baseUrl = process.env.BASE_URL;
|
|
113
452
|
if (!baseUrl)
|
|
114
|
-
throw new Error("BASE_URL not defined");
|
|
453
|
+
throw new Error("BASE_URL environment variable is not defined. Cannot visit relative URL.");
|
|
454
|
+
// Ensure no double slashes if BASE_URL already ends with one
|
|
115
455
|
url = `${baseUrl.replace(/\/+$/, "")}${url}`;
|
|
116
456
|
}
|
|
117
|
-
this.log?.(
|
|
457
|
+
this.log?.(`🌍 Navigating to: "${url}"`);
|
|
118
458
|
await this.page.goto(url);
|
|
119
|
-
}
|
|
120
|
-
(0, cucumber_1.When)("I
|
|
459
|
+
}
|
|
460
|
+
(0, cucumber_1.When)("I visit {string}", When_I_visit);
|
|
461
|
+
/**
|
|
462
|
+
* Reloads the current page.
|
|
463
|
+
*
|
|
464
|
+
* ```gherkin
|
|
465
|
+
* When I reload the page
|
|
466
|
+
* ```
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* When I reload the page
|
|
470
|
+
*
|
|
471
|
+
* @remarks
|
|
472
|
+
* This step is equivalent to hitting the browser's reload button.
|
|
473
|
+
* It uses Playwright's `page.reload()`.
|
|
474
|
+
* @category Page Navigation Steps
|
|
475
|
+
*/
|
|
476
|
+
async function When_I_reload_the_page() {
|
|
121
477
|
await this.page.reload();
|
|
122
|
-
|
|
123
|
-
|
|
478
|
+
this.log?.(`🔄 Reloaded the current page.`);
|
|
479
|
+
}
|
|
480
|
+
(0, cucumber_1.When)("I reload the page", When_I_reload_the_page);
|
|
481
|
+
/**
|
|
482
|
+
* Navigates back in the browser's history.
|
|
483
|
+
*
|
|
484
|
+
* ```gherkin
|
|
485
|
+
* When I go back
|
|
486
|
+
* ```
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* Given I visit "/page1"
|
|
490
|
+
* And I visit "/page2"
|
|
491
|
+
* When I go back
|
|
492
|
+
* Then I should be on "/page1"
|
|
493
|
+
*
|
|
494
|
+
* @remarks
|
|
495
|
+
* This step is equivalent to hitting the browser's back button.
|
|
496
|
+
* It uses Playwright's `page.goBack()`.
|
|
497
|
+
* @category Page Navigation Steps
|
|
498
|
+
*/
|
|
499
|
+
async function When_I_go_back() {
|
|
124
500
|
await this.page.goBack();
|
|
125
|
-
|
|
126
|
-
|
|
501
|
+
this.log?.(`⬅️ Navigated back in browser history.`);
|
|
502
|
+
}
|
|
503
|
+
(0, cucumber_1.When)("I go back", When_I_go_back);
|
|
504
|
+
/**
|
|
505
|
+
* Navigates forward in the browser's history.
|
|
506
|
+
*
|
|
507
|
+
* ```gherkin
|
|
508
|
+
* When I go forward
|
|
509
|
+
* ```
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* Given I visit "/page1"
|
|
513
|
+
* And I visit "/page2"
|
|
514
|
+
* When I go back
|
|
515
|
+
* When I go forward
|
|
516
|
+
* Then I should be on "/page2"
|
|
517
|
+
*
|
|
518
|
+
* @remarks
|
|
519
|
+
* This step is equivalent to hitting the browser's forward button.
|
|
520
|
+
* It uses Playwright's `page.goForward()`.
|
|
521
|
+
* @category Page Navigation Steps
|
|
522
|
+
*/
|
|
523
|
+
async function When_I_go_forward() {
|
|
127
524
|
await this.page.goForward();
|
|
128
|
-
|
|
129
|
-
|
|
525
|
+
this.log?.(`➡️ Navigated forward in browser history.`);
|
|
526
|
+
}
|
|
527
|
+
(0, cucumber_1.When)("I go forward", When_I_go_forward);
|
|
528
|
+
/**
|
|
529
|
+
* Pauses the test execution in debug mode.
|
|
530
|
+
* This is useful for inspecting the browser state interactively during test runs.
|
|
531
|
+
*
|
|
532
|
+
* ```gherkin
|
|
533
|
+
* When I pause
|
|
534
|
+
* ```
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* When I perform an action
|
|
538
|
+
* And I pause
|
|
539
|
+
* Then I assert something visually
|
|
540
|
+
*
|
|
541
|
+
* @remarks
|
|
542
|
+
* When running tests in debug mode (e.g., with `npx playwright test --debug`),
|
|
543
|
+
* this step will open Playwright's inspector, allowing you to step through
|
|
544
|
+
* actions, inspect elements, and troubleshoot. The test will resume when you
|
|
545
|
+
* continue from the inspector.
|
|
546
|
+
* @category Debugging Steps
|
|
547
|
+
*/
|
|
548
|
+
async function When_I_pause() {
|
|
130
549
|
await this.page.pause();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
550
|
+
this.log?.(`⏸️ Test paused. Use Playwright Inspector to continue.`);
|
|
551
|
+
}
|
|
552
|
+
(0, cucumber_1.When)("I pause", When_I_pause);
|
|
553
|
+
// ===================================================================================
|
|
554
|
+
// UTILITY ACTIONS: DATE/TIME ALIASING
|
|
555
|
+
// ===================================================================================
|
|
556
|
+
const validDateUnits = [
|
|
136
557
|
"second",
|
|
137
558
|
"seconds",
|
|
138
559
|
"minute",
|
|
@@ -148,49 +569,176 @@ const validUnits = [
|
|
|
148
569
|
"year",
|
|
149
570
|
"years",
|
|
150
571
|
];
|
|
151
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Stores a new date calculated by offsetting an existing aliased date by a given amount and unit.
|
|
574
|
+
*
|
|
575
|
+
* ```gherkin
|
|
576
|
+
* When I store {string} {int} {word} {word} as "{word}"
|
|
577
|
+
* ```
|
|
578
|
+
*
|
|
579
|
+
* @param baseAlias - The alias of an existing date string in `this.data` (e.g., "today").
|
|
580
|
+
* @param amount - The numerical amount to offset by (e.g., 2, 5).
|
|
581
|
+
* @param unit - The unit of time (e.g., "days", "months", "hours").
|
|
582
|
+
* @param direction - Whether to offset "before" or "after" the base date.
|
|
583
|
+
* @param newAlias - The alias under which to store the newly calculated date.
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* Given I store "2024-01-15" as "invoiceDate"
|
|
587
|
+
* When I store "invoiceDate" 30 days after as "dueDate"
|
|
588
|
+
* Then the value of alias "dueDate" should be "2024-02-14"
|
|
589
|
+
*
|
|
590
|
+
* @remarks
|
|
591
|
+
* This step uses the `dayjs` library for date manipulation. The `baseAlias` must
|
|
592
|
+
* point to a valid date string that `dayjs` can parse. The `unit` must be one of:
|
|
593
|
+
* "second", "minute", "hour", "day", "week", "month", "year" (plural forms also supported).
|
|
594
|
+
* The new date is stored in `this.data` in "YYYY-MM-DD" format.
|
|
595
|
+
* @category Data Manipulation Steps
|
|
596
|
+
*/
|
|
597
|
+
async function When_I_store_date_offset(baseAlias, amount, unit, direction, // "before" or "after"
|
|
598
|
+
newAlias) {
|
|
152
599
|
const baseDateRaw = this.data?.[baseAlias];
|
|
153
600
|
if (!baseDateRaw)
|
|
154
|
-
throw new Error(`Alias "${baseAlias}" not found
|
|
155
|
-
if (!
|
|
156
|
-
throw new Error(`Invalid unit "${unit}"
|
|
601
|
+
throw new Error(`Alias "${baseAlias}" not found in test data.`);
|
|
602
|
+
if (!validDateUnits.includes(unit)) {
|
|
603
|
+
throw new Error(`Invalid unit "${unit}". Valid units are: ${validDateUnits.join(", ")}.`);
|
|
604
|
+
}
|
|
605
|
+
if (!["before", "after"].includes(direction)) {
|
|
606
|
+
throw new Error(`Invalid direction "${direction}". Must be "before" or "after".`);
|
|
607
|
+
}
|
|
157
608
|
const baseDate = (0, dayjs_1.default)(baseDateRaw);
|
|
158
|
-
if (!baseDate.isValid())
|
|
159
|
-
throw new Error(`
|
|
609
|
+
if (!baseDate.isValid()) {
|
|
610
|
+
throw new Error(`Value for alias "${baseAlias}" ("${baseDateRaw}") is not a valid date.`);
|
|
611
|
+
}
|
|
160
612
|
const result = baseDate[direction === "before" ? "subtract" : "add"](amount, unit);
|
|
161
613
|
const formatted = result.format("YYYY-MM-DD");
|
|
162
614
|
this.data[newAlias] = formatted;
|
|
163
|
-
this.log?.(`📅 Stored ${amount} ${unit} ${direction} "${baseAlias}" as "@${newAlias}" = ${formatted}`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
615
|
+
this.log?.(`📅 Stored ${amount} ${unit} ${direction} "${baseAlias}" as "@${newAlias}" = "${formatted}"`);
|
|
616
|
+
}
|
|
617
|
+
(0, cucumber_1.When)('I store {string} {int} {word} {word} as "{word}"', When_I_store_date_offset);
|
|
618
|
+
// ===================================================================================
|
|
619
|
+
// UTILITY ACTIONS: IFRAME
|
|
620
|
+
// ===================================================================================
|
|
621
|
+
/**
|
|
622
|
+
* Switches the current Playwright context to an iframe located by a CSS selector.
|
|
623
|
+
* The step waits for the iframe's `body` element to be visible before proceeding.
|
|
624
|
+
*
|
|
625
|
+
* ```gherkin
|
|
626
|
+
* When I switch to iframe with selector {string}
|
|
627
|
+
* ```
|
|
628
|
+
*
|
|
629
|
+
* @param selector - The CSS selector for the iframe element (e.g., "#my-iframe", "iframe[name='chatFrame']").
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* When I switch to iframe with selector "#payment-form-iframe"
|
|
633
|
+
* And I find element by placeholder text "Card Number"
|
|
634
|
+
* And I type "1234..."
|
|
635
|
+
*
|
|
636
|
+
* @remarks
|
|
637
|
+
* Once inside an iframe, all subsequent element finding and interaction steps will
|
|
638
|
+
* target elements within that iframe. To exit the iframe context, use
|
|
639
|
+
* {@link When_I_exit_iframe | "When I exit iframe"}.
|
|
640
|
+
* @category IFrame Steps
|
|
641
|
+
*/
|
|
642
|
+
async function When_I_switch_to_iframe_with_selector(selector) {
|
|
169
643
|
const frameLocator = this.page.frameLocator(selector);
|
|
644
|
+
// Wait for an element inside the iframe to ensure it's loaded and ready
|
|
170
645
|
await frameLocator.locator("body").waitFor({ state: "visible", timeout: 10000 });
|
|
171
|
-
this.frame = frameLocator;
|
|
172
|
-
this.log?.(`🪟 Switched to iframe: ${selector}
|
|
173
|
-
}
|
|
174
|
-
(0, cucumber_1.When)("I switch to iframe with
|
|
646
|
+
this.frame = frameLocator; // Store the frame locator in CustomWorld context
|
|
647
|
+
this.log?.(`🪟 Switched to iframe with selector: "${selector}".`);
|
|
648
|
+
}
|
|
649
|
+
(0, cucumber_1.When)("I switch to iframe with selector {string}", When_I_switch_to_iframe_with_selector);
|
|
650
|
+
/**
|
|
651
|
+
* Switches the current Playwright context to an iframe located by its title attribute.
|
|
652
|
+
*
|
|
653
|
+
* ```gherkin
|
|
654
|
+
* When I switch to iframe with title {string}
|
|
655
|
+
* ```
|
|
656
|
+
*
|
|
657
|
+
* @param title - The title of the iframe to switch to.
|
|
658
|
+
*
|
|
659
|
+
* @example
|
|
660
|
+
* When I switch to iframe with title "My Iframe"
|
|
661
|
+
* And I find element by label text "Card Holder"
|
|
662
|
+
*
|
|
663
|
+
* @remarks
|
|
664
|
+
* This step iterates through all frames on the page to find one whose title matches
|
|
665
|
+
* (case-insensitively, partially matched with `includes`). Once found, subsequent
|
|
666
|
+
* element operations will target elements within this iframe. To exit, use
|
|
667
|
+
* {@link When_I_exit_iframe | "When I exit iframe"}.
|
|
668
|
+
* @category IFrame Steps
|
|
669
|
+
*/
|
|
670
|
+
async function When_I_switch_to_iframe_with_title(title) {
|
|
671
|
+
// Find the frame by title first to ensure it exists before creating locator
|
|
175
672
|
const frames = this.page.frames();
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
673
|
+
const foundFrame = await Promise.race(frames.map(async (f) => {
|
|
674
|
+
const frameTitle = await f.title();
|
|
675
|
+
return frameTitle.includes(title) ? f : null;
|
|
676
|
+
}));
|
|
677
|
+
if (!foundFrame)
|
|
678
|
+
throw new Error(`No iframe with title "${title}" found.`);
|
|
679
|
+
// Playwright recommends using frameLocator for interacting with iframes,
|
|
680
|
+
// even if found via frame object.
|
|
179
681
|
this.frame = this.page.frameLocator(`iframe[title*="${title}"]`);
|
|
180
|
-
this.log?.(`🪟 Switched to iframe titled: ${title}
|
|
181
|
-
}
|
|
182
|
-
(0, cucumber_1.When)("I switch to iframe with
|
|
682
|
+
this.log?.(`🪟 Switched to iframe titled: "${title}".`);
|
|
683
|
+
}
|
|
684
|
+
(0, cucumber_1.When)("I switch to iframe with title {string}", When_I_switch_to_iframe_with_title);
|
|
685
|
+
/**
|
|
686
|
+
* Switches the current Playwright context to an iframe located by a CSS selector,
|
|
687
|
+
* and then waits for specific text to become visible inside that iframe.
|
|
688
|
+
*
|
|
689
|
+
* ```gherkin
|
|
690
|
+
* When I switch to iframe with selector {string} and wait for text {string}
|
|
691
|
+
* ```
|
|
692
|
+
*
|
|
693
|
+
* @param selector - The CSS selector for the iframe element.
|
|
694
|
+
* @param expectedText - The text string to wait for inside the iframe.
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* When I switch to iframe with selector "#dynamic-content-iframe" and wait for text "Content Loaded"
|
|
698
|
+
*
|
|
699
|
+
* @remarks
|
|
700
|
+
* This step combines switching into an iframe with a wait condition, which is
|
|
701
|
+
* useful for dynamic iframe content. The `expectedText` must be present
|
|
702
|
+
* and visible inside the iframe. To exit the iframe context, use
|
|
703
|
+
* {@link When_I_exit_iframe | "When I exit iframe"}.
|
|
704
|
+
* @category IFrame Steps
|
|
705
|
+
*/
|
|
706
|
+
async function When_I_switch_to_iframe_with_selector_and_wait_for_text(selector, expectedText) {
|
|
183
707
|
const frameLocator = this.page.frameLocator(selector);
|
|
184
|
-
|
|
708
|
+
// Wait for the specific text inside the iframe
|
|
709
|
+
await frameLocator.locator(`text=${expectedText}`).waitFor({ timeout: 10000 });
|
|
185
710
|
this.frame = frameLocator;
|
|
186
|
-
this.log?.(`🪟 Switched to iframe: ${selector}, waited for "${
|
|
187
|
-
}
|
|
188
|
-
(0, cucumber_1.When)("I
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
711
|
+
this.log?.(`🪟 Switched to iframe with selector: "${selector}", and waited for text: "${expectedText}".`);
|
|
712
|
+
}
|
|
713
|
+
(0, cucumber_1.When)("I switch to iframe with selector {string} and wait for text {string}", When_I_switch_to_iframe_with_selector_and_wait_for_text);
|
|
714
|
+
/**
|
|
715
|
+
* Exits the current iframe context, returning the Playwright context to the main page.
|
|
716
|
+
* All subsequent element finding and interaction steps will operate on the main page.
|
|
717
|
+
*
|
|
718
|
+
* ```gherkin
|
|
719
|
+
* When I exit iframe
|
|
720
|
+
* ```
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* When I switch to iframe with selector "#my-iframe"
|
|
724
|
+
* And I fill "my data"
|
|
725
|
+
* When I exit iframe
|
|
726
|
+
* And I click "Main Page Button"
|
|
727
|
+
*
|
|
728
|
+
* @remarks
|
|
729
|
+
* This step is crucial for navigating back to the main document after interacting
|
|
730
|
+
* with elements inside an iframe. It sets `this.frame` back to `undefined` (or the main page locator).
|
|
731
|
+
* @category IFrame Steps
|
|
732
|
+
*/
|
|
733
|
+
function When_I_exit_iframe() {
|
|
734
|
+
this.exitIframe(); // Assuming CustomWorld has an exitIframe method
|
|
735
|
+
this.log?.(`🪟 Exited iframe context, now interacting with the main page.`);
|
|
736
|
+
}
|
|
737
|
+
(0, cucumber_1.When)("I exit iframe", When_I_exit_iframe);
|
|
738
|
+
// ===================================================================================
|
|
739
|
+
// UTILITY ACTIONS: REUSABLE ACTIONS ON STORED ELEMENTS
|
|
740
|
+
// ===================================================================================
|
|
741
|
+
// Helper functions (keep these outside the exports or as internal helpers)
|
|
194
742
|
function toOrdinal(n) {
|
|
195
743
|
const s = ["th", "st", "nd", "rd"];
|
|
196
744
|
const v = n % 100;
|
|
@@ -205,21 +753,33 @@ async function getReadableLabel(el) {
|
|
|
205
753
|
return "(unknown)";
|
|
206
754
|
}
|
|
207
755
|
}
|
|
756
|
+
// Helper to get a subset of elements (first, last, random, or specific nth for action)
|
|
208
757
|
async function getElementsSubset(world, mode, count) {
|
|
209
758
|
const total = await world.elements?.count();
|
|
210
759
|
if (!total || total < 1)
|
|
211
|
-
throw new Error("No elements stored");
|
|
760
|
+
throw new Error("No elements stored in 'this.elements' collection.");
|
|
212
761
|
if (count > total)
|
|
213
|
-
throw new Error(`
|
|
762
|
+
throw new Error(`Cannot get ${count} elements, only ${total} available.`);
|
|
214
763
|
switch (mode) {
|
|
215
764
|
case "first":
|
|
216
765
|
return Array.from({ length: count }, (_, i) => world.elements.nth(i));
|
|
217
766
|
case "last":
|
|
218
767
|
return Array.from({ length: count }, (_, i) => world.elements.nth(total - count + i));
|
|
219
768
|
case "random":
|
|
220
|
-
|
|
769
|
+
// Generate unique random indices
|
|
770
|
+
const indices = new Set();
|
|
771
|
+
while (indices.size < count) {
|
|
772
|
+
indices.add(Math.floor(Math.random() * total));
|
|
773
|
+
}
|
|
774
|
+
return Array.from(indices).map((i) => world.elements.nth(i));
|
|
775
|
+
case "nth": // Used specifically by the "I (action) the Nth element" step
|
|
776
|
+
if (count < 1)
|
|
777
|
+
throw new Error(`Invalid Nth element index: ${count}. Must be 1 or greater.`);
|
|
778
|
+
if (count > total)
|
|
779
|
+
throw new Error(`Cannot get ${toOrdinal(count)} element, only ${total} available.`);
|
|
780
|
+
return [world.elements.nth(count - 1)]; // Return as array for consistent loop below
|
|
221
781
|
default:
|
|
222
|
-
throw new Error(`Unsupported mode: ${mode}
|
|
782
|
+
throw new Error(`Unsupported subset mode: "${mode}".`);
|
|
223
783
|
}
|
|
224
784
|
}
|
|
225
785
|
const actionDisplayNames = {
|
|
@@ -231,6 +791,7 @@ const actionDisplayNames = {
|
|
|
231
791
|
blur: "Blurred",
|
|
232
792
|
fill: "Filled",
|
|
233
793
|
};
|
|
794
|
+
// Define the functions that perform the Playwright actions on a locator
|
|
234
795
|
const locatorActions = {
|
|
235
796
|
click: (el, table) => el.click((0, optionsUtils_1.parseClickOptions)(table)),
|
|
236
797
|
hover: (el, table) => el.hover((0, optionsUtils_1.parseHoverOptions)(table)),
|
|
@@ -238,83 +799,263 @@ const locatorActions = {
|
|
|
238
799
|
uncheck: (el, table) => el.uncheck((0, optionsUtils_1.parseUncheckOptions)(table)),
|
|
239
800
|
focus: (el) => el.focus(),
|
|
240
801
|
blur: (el) => el.evaluate((e) => e.blur()),
|
|
241
|
-
fill: (el, table) => el.fill("", (0, optionsUtils_1.parseFillOptions)(table)), //
|
|
802
|
+
fill: (el, table) => el.fill("", (0, optionsUtils_1.parseFillOptions)(table)), // This `fill` is generic. If you need to fill with a specific value from the step,
|
|
803
|
+
// you'll need to pass it as an argument to the actionFn in the step definition.
|
|
804
|
+
// For now, it's just clearing.
|
|
242
805
|
};
|
|
243
|
-
|
|
806
|
+
/**
|
|
807
|
+
* Performs a specified action (e.g., click, hover, check, uncheck, focus, blur)
|
|
808
|
+
* on a subset of the previously stored elements (first N, last N, or random N).
|
|
809
|
+
*
|
|
810
|
+
* ```gherkin
|
|
811
|
+
* When I {word} the {word} {int}
|
|
812
|
+
* ```
|
|
813
|
+
*
|
|
814
|
+
* @param action - The action to perform (e.g., "click", "hover", "check", "uncheck", "focus", "blur", "fill").
|
|
815
|
+
* @param mode - The selection mode: "first", "last", or "random".
|
|
816
|
+
* @param count - The number of elements to apply the action to.
|
|
817
|
+
* @param table - (Optional) A Cucumber DataTable for action-specific options (e.g., `ClickOptions`).
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* Given I find elements by selector ".item-checkbox"
|
|
821
|
+
* When I check the first 2
|
|
822
|
+
* When I hover the last 3
|
|
823
|
+
*
|
|
824
|
+
* @remarks
|
|
825
|
+
* This step requires that `this.elements` (a Playwright `Locator` that points to multiple
|
|
826
|
+
* elements) has been populated by a preceding step (e.g., `When I find elements by selector`).
|
|
827
|
+
* The `action` must be one of the supported actions. The `count` specifies how many of
|
|
828
|
+
* the matched elements to target.
|
|
829
|
+
* @category Multi-Element Action Steps
|
|
830
|
+
*/
|
|
831
|
+
async function When_I_perform_action_on_subset_of_elements(action, mode, count, table) {
|
|
244
832
|
const elements = await getElementsSubset(this, mode, count);
|
|
245
833
|
const actionFn = locatorActions[action];
|
|
246
834
|
if (!actionFn)
|
|
247
|
-
throw new Error(`Unsupported action: ${action}
|
|
835
|
+
throw new Error(`Unsupported action: "${action}".`);
|
|
248
836
|
for (const el of elements) {
|
|
249
837
|
const label = await getReadableLabel(el);
|
|
250
838
|
await actionFn(el, table);
|
|
251
|
-
this.log?.(`✅ ${actionDisplayNames[action] || action} element: "${label}"
|
|
839
|
+
this.log?.(`✅ ${actionDisplayNames[action] || action} element: "${label}".`);
|
|
252
840
|
}
|
|
253
|
-
}
|
|
254
|
-
(0, cucumber_1.When)(/^I (\w+) the (\d+)
|
|
255
|
-
|
|
841
|
+
}
|
|
842
|
+
(0, cucumber_1.When)(/^I (\w+) the (first|last|random) (\d+)$/, When_I_perform_action_on_subset_of_elements);
|
|
843
|
+
/**
|
|
844
|
+
* Performs a specified action (e.g., click, hover, check, uncheck, focus, blur)
|
|
845
|
+
* on the Nth element of the previously stored elements collection.
|
|
846
|
+
*
|
|
847
|
+
* ```gherkin
|
|
848
|
+
* When I {word} the {int}(?:st|nd|rd|th) element
|
|
849
|
+
* ```
|
|
850
|
+
*
|
|
851
|
+
* @param action - The action to perform (e.g., "click", "hover", "check", "uncheck", "focus", "blur", "fill").
|
|
852
|
+
* @param nth - The 1-based index of the element to target (e.g., 1 for 1st, 2 for 2nd).
|
|
853
|
+
* @param table - (Optional) A Cucumber DataTable for action-specific options.
|
|
854
|
+
*
|
|
855
|
+
* @example
|
|
856
|
+
* Given I find elements by selector ".product-card"
|
|
857
|
+
* When I click the 2nd element
|
|
858
|
+
* When I fill the 1st element
|
|
859
|
+
*
|
|
860
|
+
* @remarks
|
|
861
|
+
* This step requires that `this.elements` has been populated by a preceding step.
|
|
862
|
+
* It targets a single element at a specific 1-based ordinal position within that collection.
|
|
863
|
+
* The `action` must be one of the supported actions.
|
|
864
|
+
* @category Multi-Element Action Steps
|
|
865
|
+
*/
|
|
866
|
+
async function When_I_perform_action_on_nth_element(action, nth, table) {
|
|
867
|
+
// Use "nth" mode with getElementsSubset to correctly fetch a single element
|
|
868
|
+
const elements = await getElementsSubset(this, "nth", nth); // This will return an array with one element
|
|
869
|
+
const targetElement = elements[0]; // Get the single element
|
|
256
870
|
const actionFn = locatorActions[action];
|
|
257
871
|
if (!actionFn)
|
|
258
|
-
throw new Error(`Unsupported action: ${action}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
872
|
+
throw new Error(`Unsupported action: "${action}".`);
|
|
873
|
+
const label = await getReadableLabel(targetElement);
|
|
874
|
+
await actionFn(targetElement, table);
|
|
875
|
+
this.log?.(`✅ ${actionDisplayNames[action] || action} the ${toOrdinal(nth)} element: "${label}".`);
|
|
876
|
+
}
|
|
877
|
+
(0, cucumber_1.When)(/^I (\w+) the (\d+)(?:st|nd|rd|th) element$/, When_I_perform_action_on_nth_element);
|
|
878
|
+
/**
|
|
879
|
+
* Presses a specific key on the previously selected element.
|
|
880
|
+
*
|
|
881
|
+
* ```gherkin
|
|
882
|
+
* When I press key {string}
|
|
883
|
+
* ```
|
|
884
|
+
*
|
|
885
|
+
* @param key - The key to press (e.g., "Enter", "Escape", "ArrowDown", "Tab").
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* When I find element by selector "input[name='email']"
|
|
889
|
+
* And I type "my query"
|
|
890
|
+
* When I press key "Enter"
|
|
891
|
+
*
|
|
892
|
+
* @remarks
|
|
893
|
+
* This step requires a preceding step that sets the {@link CustomWorld.element | current element}.
|
|
894
|
+
* It first focuses the element and then simulates a key press.
|
|
895
|
+
* This is useful for triggering keyboard shortcuts or submitting forms via "Enter".
|
|
896
|
+
* @category Keyboard Interaction Steps
|
|
897
|
+
*/
|
|
898
|
+
async function When_I_press_key(key) {
|
|
266
899
|
if (!this.element)
|
|
267
|
-
throw new Error("No element selected");
|
|
900
|
+
throw new Error("No element selected to press key on.");
|
|
268
901
|
await this.element.focus();
|
|
269
|
-
await this.page.waitForTimeout(
|
|
902
|
+
await this.page.waitForTimeout(50); // Small buffer to ensure focus
|
|
270
903
|
await this.element.press(key);
|
|
271
|
-
this.log?.(`🎹 Pressed {${key}}
|
|
272
|
-
}
|
|
273
|
-
(0, cucumber_1.When)(
|
|
904
|
+
this.log?.(`🎹 Pressed key "{${key}}" on selected element.`);
|
|
905
|
+
}
|
|
906
|
+
(0, cucumber_1.When)("I press key {string}", When_I_press_key);
|
|
907
|
+
// ===================================================================================
|
|
908
|
+
// UTILITY ACTIONS: VIEWPORT
|
|
909
|
+
// ===================================================================================
|
|
910
|
+
/**
|
|
911
|
+
* Sets the browser viewport to emulate a specific Playwright device profile and orientation.
|
|
912
|
+
* This will close the current browser context and open a new one with the specified device settings.
|
|
913
|
+
*
|
|
914
|
+
* ```gherkin
|
|
915
|
+
* When I set viewport to {string}
|
|
916
|
+
* When I set viewport to {string} and {string}
|
|
917
|
+
* ```
|
|
918
|
+
*
|
|
919
|
+
* @param deviceInput - The name of the Playwright device (e.g., "iPhone 12", "iPad", "Desktop Chrome").
|
|
920
|
+
* @param orientation - (Optional) The orientation, either "landscape" or "portrait" (default if not specified).
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* When I set viewport to "iPhone 12"
|
|
924
|
+
* When I set viewport to "iPad" and "landscape"
|
|
925
|
+
*
|
|
926
|
+
* @remarks
|
|
927
|
+
* This step creates a *new* browser context and page, so any previous page state or
|
|
928
|
+
* setup (like routes, localStorage) will be reset.
|
|
929
|
+
* The `deviceInput` is normalized to match Playwright's `devices` object keys.
|
|
930
|
+
* @category Browser Context Steps
|
|
931
|
+
*/
|
|
932
|
+
async function When_I_set_viewport_to_device(deviceInput, orientation) {
|
|
274
933
|
const normalizedDevice = (0, resolveUtils_1.normalizeDeviceName)(deviceInput);
|
|
275
934
|
if (!normalizedDevice) {
|
|
276
|
-
throw new Error(`🚫 Unknown device: "${deviceInput}"
|
|
935
|
+
throw new Error(`🚫 Unknown device name: "${deviceInput}". Check Playwright 'devices' for valid names.`);
|
|
277
936
|
}
|
|
278
937
|
const baseDevice = test_1.devices[normalizedDevice];
|
|
279
938
|
if (!baseDevice) {
|
|
280
|
-
throw new Error(`🚫
|
|
939
|
+
throw new Error(`🚫 Playwright device not found for normalized name: "${normalizedDevice}".`);
|
|
281
940
|
}
|
|
282
941
|
const isLandscape = orientation?.toLowerCase() === "landscape";
|
|
283
942
|
const deviceSettings = isLandscape
|
|
284
|
-
? baseDevice.landscape
|
|
943
|
+
? baseDevice.landscape // Use Playwright's built-in landscape config if available
|
|
285
944
|
? baseDevice.landscape
|
|
286
945
|
: {
|
|
946
|
+
// Otherwise, manually adjust for landscape
|
|
287
947
|
...baseDevice,
|
|
288
|
-
isMobile: true,
|
|
289
|
-
viewport: {
|
|
948
|
+
isMobile: true, // Assuming mobile devices for landscape adjustment
|
|
949
|
+
viewport: {
|
|
950
|
+
width: baseDevice.viewport.height, // Swap width and height for landscape
|
|
951
|
+
height: baseDevice.viewport.width,
|
|
952
|
+
},
|
|
290
953
|
}
|
|
291
|
-
: baseDevice;
|
|
292
|
-
// Close current context
|
|
293
|
-
if (this.
|
|
954
|
+
: baseDevice; // Use base device settings for portrait or non-mobile
|
|
955
|
+
// Close current context and page before creating a new one
|
|
956
|
+
if (this.page)
|
|
957
|
+
await this.page.close();
|
|
958
|
+
if (this.context)
|
|
294
959
|
await this.context.close();
|
|
295
|
-
}
|
|
296
960
|
this.context = await this.browser.newContext(deviceSettings);
|
|
297
961
|
this.page = await this.context.newPage();
|
|
298
|
-
this.log?.(`📱 Set viewport to ${normalizedDevice}${isLandscape ? " in landscape" : ""}
|
|
299
|
-
}
|
|
300
|
-
(0, cucumber_1.When)(
|
|
301
|
-
|
|
302
|
-
|
|
962
|
+
this.log?.(`📱 Set viewport to ${normalizedDevice}${isLandscape ? " in landscape" : ""}.`);
|
|
963
|
+
}
|
|
964
|
+
(0, cucumber_1.When)(/^I set viewport to "([^"]+)"(?: and "([^"]+)")?$/, When_I_set_viewport_to_device);
|
|
965
|
+
/**
|
|
966
|
+
* Sets the viewport to the given width and height in pixels.
|
|
967
|
+
* This will close the current browser context and open a new one with the specified dimensions.
|
|
968
|
+
*
|
|
969
|
+
* ```gherkin
|
|
970
|
+
* When I set viewport to {int}px by {int}px
|
|
971
|
+
* ```
|
|
972
|
+
*
|
|
973
|
+
* @param width - The desired viewport width in pixels.
|
|
974
|
+
* @param height - The desired viewport height in pixels.
|
|
975
|
+
*
|
|
976
|
+
* @example
|
|
977
|
+
* When I set viewport to 1280px by 720px
|
|
978
|
+
*
|
|
979
|
+
* @remarks
|
|
980
|
+
* This step creates a *new* browser context and page, so any previous page state or
|
|
981
|
+
* setup (like routes, localStorage) will be reset.
|
|
982
|
+
* @category Browser Context Steps
|
|
983
|
+
*/
|
|
984
|
+
async function When_I_set_viewport_to_dimensions(width, height) {
|
|
985
|
+
// Close current context and page before creating a new one
|
|
986
|
+
if (this.page)
|
|
987
|
+
await this.page.close();
|
|
988
|
+
if (this.context)
|
|
303
989
|
await this.context.close();
|
|
304
|
-
}
|
|
305
990
|
// Recreate new context with the desired viewport
|
|
306
991
|
this.context = await this.browser.newContext({
|
|
307
992
|
viewport: { width, height },
|
|
308
993
|
});
|
|
309
994
|
this.page = await this.context.newPage();
|
|
310
|
-
this.log?.(`🖥️ Set viewport to ${width}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
995
|
+
this.log?.(`🖥️ Set viewport to ${width}px by ${height}px.`);
|
|
996
|
+
}
|
|
997
|
+
(0, cucumber_1.When)("I set viewport to {int}px by {int}px", When_I_set_viewport_to_dimensions);
|
|
998
|
+
// ===================================================================================
|
|
999
|
+
// UTILITY ACTIONS: DYNAMIC PLAYWRIGHT CONFIG SETTERS (FOR PAGE-ONLY CONFIG)
|
|
1000
|
+
// ===================================================================================
|
|
1001
|
+
/**
|
|
1002
|
+
* Sets a specific Playwright page configuration property to the given value.
|
|
1003
|
+
* This can be used to dynamically change page-level settings during a test.
|
|
1004
|
+
*
|
|
1005
|
+
* ```gherkin
|
|
1006
|
+
* When I set Playwright config {word} to {string}
|
|
1007
|
+
* ```
|
|
1008
|
+
*
|
|
1009
|
+
* @param key - The name of the Playwright `Page` property to set (e.g., "userAgent", "defaultTimeout").
|
|
1010
|
+
* @param value - The string value to set the property to. Note: All values are treated as strings.
|
|
1011
|
+
*
|
|
1012
|
+
* @example
|
|
1013
|
+
* When I set Playwright config "userAgent" to "MyCustomAgent"
|
|
1014
|
+
*
|
|
1015
|
+
* @remarks
|
|
1016
|
+
* This step directly assigns a value to a property on the `this.page` object.
|
|
1017
|
+
* It's important to know which properties are settable and what their expected
|
|
1018
|
+
* types are. Using incorrect keys or values may lead to unexpected behavior or errors.
|
|
1019
|
+
* Not all Playwright page properties are designed to be set this way after page creation.
|
|
1020
|
+
* @category Configuration Steps
|
|
1021
|
+
*/
|
|
1022
|
+
async function When_I_set_playwright_page_config_key(key, value) {
|
|
1023
|
+
// Directly assign property. Using 'as any' to bypass strict type checking,
|
|
1024
|
+
// but be cautious as not all page properties are meant to be set this way dynamically.
|
|
314
1025
|
this.page[key] = value;
|
|
315
|
-
});
|
|
316
|
-
|
|
1026
|
+
this.log?.(`⚙️ Set Playwright page config "${key}" to "${value}".`);
|
|
1027
|
+
}
|
|
1028
|
+
(0, cucumber_1.When)('I set Playwright config "{word}" to {string}', When_I_set_playwright_page_config_key);
|
|
1029
|
+
/**
|
|
1030
|
+
* Sets multiple Playwright page configuration properties using a data table.
|
|
1031
|
+
*
|
|
1032
|
+
* ```gherkin
|
|
1033
|
+
* When I set Playwright config
|
|
1034
|
+
* | key | value |
|
|
1035
|
+
* | userAgent | MyAgent |
|
|
1036
|
+
* | defaultTimeout | 5000 |
|
|
1037
|
+
* ```
|
|
1038
|
+
*
|
|
1039
|
+
* @param table - A Cucumber DataTable with two columns: `key` (the property name)
|
|
1040
|
+
* and `value` (the string value to set).
|
|
1041
|
+
*
|
|
1042
|
+
* @example
|
|
1043
|
+
* When I set Playwright config
|
|
1044
|
+
* | key | value |
|
|
1045
|
+
* | userAgent | TestBot |
|
|
1046
|
+
* | defaultTimeout | 10000 |
|
|
1047
|
+
*
|
|
1048
|
+
* @remarks
|
|
1049
|
+
* Similar to the single-key version, this step dynamically assigns values to
|
|
1050
|
+
* properties on the `this.page` object. All values from the data table are
|
|
1051
|
+
* treated as strings. Use with caution, understanding which properties can
|
|
1052
|
+
* be dynamically set.
|
|
1053
|
+
* @category Configuration Steps
|
|
1054
|
+
*/
|
|
1055
|
+
async function When_I_set_playwright_page_config_from_table(table) {
|
|
317
1056
|
for (const [key, value] of table.rows()) {
|
|
318
|
-
this.page[key] = value;
|
|
1057
|
+
this.page[key] = value; // Direct assignment with 'as any'
|
|
1058
|
+
this.log?.(`⚙️ Set Playwright page config "${key}" to "${value}".`);
|
|
319
1059
|
}
|
|
320
|
-
}
|
|
1060
|
+
}
|
|
1061
|
+
(0, cucumber_1.When)("I set Playwright config", When_I_set_playwright_page_config_from_table);
|