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.
Files changed (60) hide show
  1. package/README.md +195 -256
  2. package/dist/backend/actions/index.js +4 -0
  3. package/dist/backend/actions/interactions.js +23 -0
  4. package/dist/backend/actions/navigation.js +19 -0
  5. package/dist/backend/api/assertions.js +26 -0
  6. package/dist/backend/api/index.js +4 -0
  7. package/dist/backend/api/requests.js +24 -0
  8. package/dist/backend/api/state.js +15 -0
  9. package/dist/backend/assertions/expectVisible.js +8 -0
  10. package/dist/backend/assertions/index.js +5 -0
  11. package/dist/backend/assertions/pageState.js +25 -0
  12. package/dist/backend/assertions/text.js +20 -0
  13. package/dist/backend/assertions/visibility.js +20 -0
  14. package/dist/backend/auth/index.js +71 -0
  15. package/dist/backend/elements/alerts.js +21 -0
  16. package/dist/backend/elements/forms.js +59 -0
  17. package/dist/backend/elements/frames.js +25 -0
  18. package/dist/backend/elements/index.js +5 -0
  19. package/dist/core/registry.js +20 -0
  20. package/dist/core/runner.js +136 -0
  21. package/dist/index.js +10 -0
  22. package/dist/reporting/index.js +43 -0
  23. package/package.json +19 -101
  24. package/LICENSE +0 -21
  25. package/src/actions/clickSteps.ts +0 -429
  26. package/src/actions/cookieSteps.ts +0 -95
  27. package/src/actions/debugSteps.ts +0 -21
  28. package/src/actions/elementFindSteps.ts +0 -961
  29. package/src/actions/fillFormSteps.ts +0 -270
  30. package/src/actions/index.ts +0 -12
  31. package/src/actions/inputSteps.ts +0 -354
  32. package/src/actions/interceptionSteps.ts +0 -325
  33. package/src/actions/miscSteps.ts +0 -1144
  34. package/src/actions/mouseSteps.ts +0 -256
  35. package/src/actions/scrollSteps.ts +0 -122
  36. package/src/actions/storageSteps.ts +0 -308
  37. package/src/assertions/buttonAndTextVisibilitySteps.ts +0 -436
  38. package/src/assertions/cookieSteps.ts +0 -131
  39. package/src/assertions/elementSteps.ts +0 -432
  40. package/src/assertions/formInputSteps.ts +0 -377
  41. package/src/assertions/index.ts +0 -11
  42. package/src/assertions/interceptionRequestsSteps.ts +0 -640
  43. package/src/assertions/locationSteps.ts +0 -315
  44. package/src/assertions/roleTestIdSteps.ts +0 -254
  45. package/src/assertions/semanticSteps.ts +0 -267
  46. package/src/assertions/storageSteps.ts +0 -250
  47. package/src/assertions/visualSteps.ts +0 -275
  48. package/src/custom_setups/loginHooks.ts +0 -154
  49. package/src/helpers/checkPeerDeps.ts +0 -19
  50. package/src/helpers/compareSnapshots.ts +0 -35
  51. package/src/helpers/hooks.ts +0 -212
  52. package/src/helpers/utils/fakerUtils.ts +0 -64
  53. package/src/helpers/utils/index.ts +0 -4
  54. package/src/helpers/utils/optionsUtils.ts +0 -104
  55. package/src/helpers/utils/resolveUtils.ts +0 -74
  56. package/src/helpers/utils/sessionUtils.ts +0 -36
  57. package/src/helpers/world.ts +0 -119
  58. package/src/iframes/frames.ts +0 -15
  59. package/src/index.ts +0 -18
  60. package/src/register.ts +0 -4
@@ -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);