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