@vardot/varbase-e2e 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/AGENTS.md +148 -0
  2. package/CLAUDE.md +544 -0
  3. package/README.md +22 -0
  4. package/bin/generate-reports.js +363 -0
  5. package/bin/init-varbase-e2e.js +330 -0
  6. package/bin/postinstall.js +26 -0
  7. package/cucumber.js +125 -0
  8. package/docs/00-quick-start.md +102 -0
  9. package/docs/01-getting-started.md +102 -0
  10. package/docs/02-bbr-smart-waits.md +77 -0
  11. package/docs/03-selector-registry.md +95 -0
  12. package/docs/04-step-reference.md +801 -0
  13. package/docs/05-web-first-assertions.md +91 -0
  14. package/docs/06-network-and-dialogs.md +83 -0
  15. package/docs/07-auth-state.md +75 -0
  16. package/docs/08-clock-mocking.md +59 -0
  17. package/docs/09-api-testing.md +114 -0
  18. package/docs/10-accessibility.md +167 -0
  19. package/docs/11-debugging.md +129 -0
  20. package/docs/12-ai-agent-guide.md +338 -0
  21. package/docs/13-faq.md +236 -0
  22. package/docs/14-recipes-cookbook.md +255 -0
  23. package/docs/15-tag-conventions.md +76 -0
  24. package/docs/16-ci-cd.md +90 -0
  25. package/docs/README.md +125 -0
  26. package/docs/advanced-screenshots/then-i-save-fullscreen-screenshot.md +36 -0
  27. package/docs/advanced-screenshots/then-i-save-width-x-height-screenshot.md +34 -0
  28. package/docs/advanced-screenshots/when-i-save-screenshot-name-filename.md +45 -0
  29. package/docs/advanced-screenshots.md +52 -0
  30. package/docs/advanced-selectors.md +18 -0
  31. package/docs/api-step-definitions.md +45 -0
  32. package/docs/global-settings.md +267 -0
  33. package/docs/install-varbase-e2e/ddev-varbase-e2e.md +52 -0
  34. package/docs/install-varbase-e2e.md +34 -0
  35. package/docs/overview.md +40 -0
  36. package/docs/step-definitions/given-i-am-anonymous-user.md +67 -0
  37. package/docs/step-definitions/given-i-am-homepage.md +68 -0
  38. package/docs/step-definitions/given-i-am-specific-page.md +72 -0
  39. package/docs/step-definitions/then-i-should-be.md +56 -0
  40. package/docs/step-definitions/then-i-should-see.md +62 -0
  41. package/docs/step-definitions/then-item-checkbox.md +54 -0
  42. package/docs/step-definitions/then-item-link-should-contain-url.md +84 -0
  43. package/docs/step-definitions/then-item-should-contain.md +67 -0
  44. package/docs/step-definitions/then-radio-button-value-should-be-selected.md +68 -0
  45. package/docs/step-definitions/then-response-status-code-should-be-number.md +69 -0
  46. package/docs/step-definitions/then-value-radio-button-selected.md +62 -0
  47. package/docs/step-definitions/when-i-attach-file-file-name-element.md +71 -0
  48. package/docs/step-definitions/when-i-check-checkbox.md +60 -0
  49. package/docs/step-definitions/when-i-click-link.md +51 -0
  50. package/docs/step-definitions/when-i-click-operation-text-row.md +71 -0
  51. package/docs/step-definitions/when-i-fill.md +59 -0
  52. package/docs/step-definitions/when-i-go-homepage.md +60 -0
  53. package/docs/step-definitions/when-i-go-specific-page.md +67 -0
  54. package/docs/step-definitions/when-i-move-backward-one-page.md +49 -0
  55. package/docs/step-definitions/when-i-move-forward-one-page.md +58 -0
  56. package/docs/step-definitions/when-i-press-button.md +58 -0
  57. package/docs/step-definitions/when-i-reload-page.md +51 -0
  58. package/docs/step-definitions/when-i-scroll.md +73 -0
  59. package/docs/step-definitions/when-i-select-option-select-list.md +53 -0
  60. package/docs/step-definitions/when-i-select-radio-button-value.md +62 -0
  61. package/docs/step-definitions/when-i-uncheck-checkbox.md +57 -0
  62. package/docs/step-definitions/when-i-wait-ajax-finish.md +64 -0
  63. package/docs/step-definitions/when-i-wait.md +62 -0
  64. package/docs/step-definitions.md +82 -0
  65. package/docs/steps/README.md +137 -0
  66. package/docs/steps/a11y.md +731 -0
  67. package/docs/steps/action.md +181 -0
  68. package/docs/steps/api.md +679 -0
  69. package/docs/steps/assertion.md +328 -0
  70. package/docs/steps/auth.md +108 -0
  71. package/docs/steps/clock.md +171 -0
  72. package/docs/steps/cookie.md +293 -0
  73. package/docs/steps/debug.md +52 -0
  74. package/docs/steps/dialog.md +226 -0
  75. package/docs/steps/drupal-canvas.md +414 -0
  76. package/docs/steps/drupal-ckeditor.md +118 -0
  77. package/docs/steps/drupal-core.md +688 -0
  78. package/docs/steps/drupal-layout-builder.md +437 -0
  79. package/docs/steps/drupal-media.md +107 -0
  80. package/docs/steps/drupal-moderation.md +81 -0
  81. package/docs/steps/drupal-paragraphs.md +36 -0
  82. package/docs/steps/element.md +455 -0
  83. package/docs/steps/field.md +648 -0
  84. package/docs/steps/file-download.md +235 -0
  85. package/docs/steps/form.md +344 -0
  86. package/docs/steps/iframe.md +248 -0
  87. package/docs/steps/input.md +220 -0
  88. package/docs/steps/javascript.md +111 -0
  89. package/docs/steps/keyboard.md +107 -0
  90. package/docs/steps/link.md +217 -0
  91. package/docs/steps/metatag.md +99 -0
  92. package/docs/steps/modal.md +232 -0
  93. package/docs/steps/navigation.md +275 -0
  94. package/docs/steps/network.md +284 -0
  95. package/docs/steps/path.md +203 -0
  96. package/docs/steps/response.md +102 -0
  97. package/docs/steps/responsive.md +139 -0
  98. package/docs/steps/rest.md +140 -0
  99. package/docs/steps/screenshot.md +211 -0
  100. package/docs/steps/scroll.md +266 -0
  101. package/docs/steps/selectors.md +848 -0
  102. package/docs/steps/storage.md +254 -0
  103. package/docs/steps/table.md +216 -0
  104. package/docs/steps/varbase.md +391 -0
  105. package/docs/steps/video.md +111 -0
  106. package/docs/steps/wait.md +553 -0
  107. package/docs/steps/web-first.md +289 -0
  108. package/docs/steps/xml.md +505 -0
  109. package/docs/steps/yaml.md +951 -0
  110. package/examples/a11y.html +31 -0
  111. package/examples/about-us.html +11 -0
  112. package/examples/account.html +32 -0
  113. package/examples/ajax-wait-examples.html +147 -0
  114. package/examples/contact-us.html +10 -0
  115. package/examples/cookies.html +10 -0
  116. package/examples/date.html +12 -0
  117. package/examples/dynamic-examples.html +498 -0
  118. package/examples/element.html +11 -0
  119. package/examples/example-api.json +28 -0
  120. package/examples/existing-page.html +11 -0
  121. package/examples/field.html +24 -0
  122. package/examples/index.html +37 -0
  123. package/examples/keyboard.html +11 -0
  124. package/examples/links.html +9 -0
  125. package/examples/modal.html +14 -0
  126. package/examples/path.html +3 -0
  127. package/examples/table.html +13 -0
  128. package/examples/test--modal-dialogs.html +252 -0
  129. package/examples/test--then--i-should--not--see-text-in-element.html +48 -0
  130. package/examples/test--then--i-should--see-text-in-element.html +48 -0
  131. package/examples/test--then--i-should--see-text-matching-pattern.html +31 -0
  132. package/examples/test--then--i-should-see-text-in-table-row.html +232 -0
  133. package/examples/test--then--the-checkbox-checked.html +68 -0
  134. package/examples/test--then--the-element-should_not-contain-cssproperty.html +47 -0
  135. package/examples/test--then--the-link-should-contain.html +21 -0
  136. package/examples/test--when--i-additionally-select.html +27 -0
  137. package/examples/test--when--i-attach-file-to-feild.html +50 -0
  138. package/examples/test--when--i-check-uncheck-checkbox.html +47 -0
  139. package/examples/test--when--i-click-link.html +18 -0
  140. package/examples/test--when--i-click-text-in-table-row.html +178 -0
  141. package/examples/test--when--i-fill-in.html +48 -0
  142. package/examples/test--when--i-press-button.html +31 -0
  143. package/examples/test--when--i-reload-page.html +27 -0
  144. package/examples/test--when--i-scroll-functionality.html +177 -0
  145. package/examples/test--when--i-scroll-left-right-functionality.html +149 -0
  146. package/examples/test--when--i-select-option-from-dropdownlist.html +35 -0
  147. package/examples/test--when--i-select-radio-button.html +95 -0
  148. package/examples/test--when-i-wait-max-of-time.html +39 -0
  149. package/examples/test--when-i-wait-page-until-is-loaded.html +47 -0
  150. package/examples/test--when-i-wait-specific-time.html +39 -0
  151. package/examples/test-acceptable-url-path.html +11 -0
  152. package/examples/video-demo.html +43 -0
  153. package/package.json +56 -0
  154. package/playwright.config.ts +39 -0
  155. package/tests/assets/test-document.pdf +14 -0
  156. package/tests/assets/vardot.png +0 -0
  157. package/tests/features/a11y.feature +39 -0
  158. package/tests/features/ajax-wait-examples.feature +24 -0
  159. package/tests/features/api-step-definitions-individual.feature +185 -0
  160. package/tests/features/complete-api-testing-examples.feature +205 -0
  161. package/tests/features/cookie.feature +13 -0
  162. package/tests/features/date.feature +11 -0
  163. package/tests/features/dynamic-content-examples.feature +156 -0
  164. package/tests/features/element.feature +16 -0
  165. package/tests/features/field.feature +25 -0
  166. package/tests/features/keyboard.feature +10 -0
  167. package/tests/features/link.feature +11 -0
  168. package/tests/features/modal-advanced-example.feature +54 -0
  169. package/tests/features/modal-basic-example.feature +42 -0
  170. package/tests/features/modal-error-handling-example.feature +37 -0
  171. package/tests/features/modal.feature +11 -0
  172. package/tests/features/path.feature +16 -0
  173. package/tests/features/response.feature +8 -0
  174. package/tests/features/responsive.feature +10 -0
  175. package/tests/features/table.feature +18 -0
  176. package/tests/features/test--given--i-am-an-anonymous-user.feature +10 -0
  177. package/tests/features/test--given--i-am-on-homepage.feature +16 -0
  178. package/tests/features/test--then--i-save-screenshot.feature +33 -0
  179. package/tests/features/test--then--i-see-locator.feature +320 -0
  180. package/tests/features/test--then--i-should--not--see-text-in-element.feature +13 -0
  181. package/tests/features/test--then--i-should--see-text-in-element-by-attr.feature +10 -0
  182. package/tests/features/test--then--i-should--see-text-in-element.feature +10 -0
  183. package/tests/features/test--then--i-should--see-text-matching-pattern-in-element.feature +13 -0
  184. package/tests/features/test--then--i-should--see-text-matching-pattern.feature +18 -0
  185. package/tests/features/test--then--i-should-be-on-page.feature +10 -0
  186. package/tests/features/test--then--i-should-be-on-the-homepage.feature +10 -0
  187. package/tests/features/test--then--i-should-not-see-an-element-by-attr.feature +8 -0
  188. package/tests/features/test--then--i-should-not-see-an-element.feature +8 -0
  189. package/tests/features/test--then--i-should-see-an-element-by-attr.feature +8 -0
  190. package/tests/features/test--then--i-should-see-an-element.feature +8 -0
  191. package/tests/features/test--then--i-should-see-n-elements.feature +24 -0
  192. package/tests/features/test--then--i-should-see-text-in-table-row.feature +96 -0
  193. package/tests/features/test--then--print-current-url-and-response.feature +21 -0
  194. package/tests/features/test--then--the-checkbox-is-checked.feature +18 -0
  195. package/tests/features/test--then--the-checkbox-should-be-checked.feature +16 -0
  196. package/tests/features/test--then--the-element-should-contain-cssproperty.feature +9 -0
  197. package/tests/features/test--then--the-element-should-not-contain-cssproperty.feature +10 -0
  198. package/tests/features/test--then--the-field-should-contain-value.feature +11 -0
  199. package/tests/features/test--then--the-response-should-contain.feature +11 -0
  200. package/tests/features/test--then--the-response-status-code-should-be.feature +31 -0
  201. package/tests/features/test--then--the-url-should-match.feature +7 -0
  202. package/tests/features/test--then-the-link-should-contain-by-attribute.feature +18 -0
  203. package/tests/features/test--then-the-link-should-contain.feature +7 -0
  204. package/tests/features/test--when--i-additionally-select.feature +30 -0
  205. package/tests/features/test--when--i-attach-file-to-feild.feature +9 -0
  206. package/tests/features/test--when--i-check-checkbox.feature +21 -0
  207. package/tests/features/test--when--i-click-link-by-attribute.feature +21 -0
  208. package/tests/features/test--when--i-click-link.feature +9 -0
  209. package/tests/features/test--when--i-click-text-in-table-row.feature +37 -0
  210. package/tests/features/test--when--i-fill-in-by-attr.feature +31 -0
  211. package/tests/features/test--when--i-fill-in-table-by-attr.feature +19 -0
  212. package/tests/features/test--when--i-fill-in.feature +32 -0
  213. package/tests/features/test--when--i-follow.feature +22 -0
  214. package/tests/features/test--when--i-go-to-homepage.feature +9 -0
  215. package/tests/features/test--when--i-move-forward-backward-one-page.feature +14 -0
  216. package/tests/features/test--when--i-press-button-by-attribute.feature +18 -0
  217. package/tests/features/test--when--i-press-button.feature +13 -0
  218. package/tests/features/test--when--i-reload-page.feature +11 -0
  219. package/tests/features/test--when--i-scroll-left-right.feature +48 -0
  220. package/tests/features/test--when--i-scroll.feature +48 -0
  221. package/tests/features/test--when--i-select-option-from-dropdownlist.feature +21 -0
  222. package/tests/features/test--when--i-select-radio-button.feature +37 -0
  223. package/tests/features/test--when--i-wait-page-is-loaded.feature +8 -0
  224. package/tests/features/test--when--i-wait-time.feature +21 -0
  225. package/tests/features/video.feature +18 -0
  226. package/tests/features/wait.feature +7 -0
  227. package/tests/features/yaml.feature +102 -0
  228. package/tests/selectors/_canonical-keys.json +113 -0
  229. package/tests/selectors/back-end-selectors.json +19 -0
  230. package/tests/selectors/cms-contentful-admin.json +35 -0
  231. package/tests/selectors/cms-craft-admin.json +39 -0
  232. package/tests/selectors/cms-drupal-cms-gin.json +56 -0
  233. package/tests/selectors/cms-drupal-core-claro.json +50 -0
  234. package/tests/selectors/cms-generic-admin.json +34 -0
  235. package/tests/selectors/cms-ghost-admin.json +34 -0
  236. package/tests/selectors/cms-joomla-admin.json +41 -0
  237. package/tests/selectors/cms-magento2-admin.json +32 -0
  238. package/tests/selectors/cms-prestashop-admin.json +38 -0
  239. package/tests/selectors/cms-shopify-admin.json +25 -0
  240. package/tests/selectors/cms-strapi-admin.json +42 -0
  241. package/tests/selectors/cms-typo3-admin.json +41 -0
  242. package/tests/selectors/cms-woocommerce-front.json +50 -0
  243. package/tests/selectors/cms-wordpress-admin.json +31 -0
  244. package/tests/selectors/framework-ant-design.json +80 -0
  245. package/tests/selectors/framework-bootstrap.json +86 -0
  246. package/tests/selectors/framework-bulma.json +74 -0
  247. package/tests/selectors/framework-chakra.json +58 -0
  248. package/tests/selectors/framework-foundation.json +51 -0
  249. package/tests/selectors/framework-material-ui.json +72 -0
  250. package/tests/selectors/framework-shadcn.json +51 -0
  251. package/tests/selectors/framework-tailwind.json +40 -0
  252. package/tests/selectors/framework-vuetify.json +61 -0
  253. package/tests/selectors/front-end-selectors.json +16 -0
  254. package/tests/selectors/homepage-selectors.json +13 -0
  255. package/tests/step-definitions/a11y.steps.js +800 -0
  256. package/tests/step-definitions/action.steps.js +167 -0
  257. package/tests/step-definitions/api.steps.js +732 -0
  258. package/tests/step-definitions/assertion.steps.js +352 -0
  259. package/tests/step-definitions/auth.steps.js +126 -0
  260. package/tests/step-definitions/clock.steps.js +128 -0
  261. package/tests/step-definitions/cookie.steps.js +216 -0
  262. package/tests/step-definitions/debug.steps.js +39 -0
  263. package/tests/step-definitions/dialog.steps.js +193 -0
  264. package/tests/step-definitions/drupal-canvas.steps.js +764 -0
  265. package/tests/step-definitions/drupal-ckeditor.steps.js +108 -0
  266. package/tests/step-definitions/drupal-core.steps.js +665 -0
  267. package/tests/step-definitions/drupal-helpers.js +371 -0
  268. package/tests/step-definitions/drupal-layout-builder.steps.js +466 -0
  269. package/tests/step-definitions/drupal-media.steps.js +99 -0
  270. package/tests/step-definitions/drupal-moderation.steps.js +71 -0
  271. package/tests/step-definitions/drupal-paragraphs.steps.js +50 -0
  272. package/tests/step-definitions/element.steps.js +356 -0
  273. package/tests/step-definitions/field.steps.js +653 -0
  274. package/tests/step-definitions/file-download.steps.js +211 -0
  275. package/tests/step-definitions/form.steps.js +333 -0
  276. package/tests/step-definitions/iframe.steps.js +228 -0
  277. package/tests/step-definitions/input.steps.js +202 -0
  278. package/tests/step-definitions/javascript.steps.js +232 -0
  279. package/tests/step-definitions/keyboard.steps.js +112 -0
  280. package/tests/step-definitions/link.steps.js +142 -0
  281. package/tests/step-definitions/metatag.steps.js +91 -0
  282. package/tests/step-definitions/modal.steps.js +244 -0
  283. package/tests/step-definitions/navigation.steps.js +218 -0
  284. package/tests/step-definitions/network.steps.js +280 -0
  285. package/tests/step-definitions/path.steps.js +141 -0
  286. package/tests/step-definitions/response.steps.js +98 -0
  287. package/tests/step-definitions/responsive.steps.js +125 -0
  288. package/tests/step-definitions/rest.steps.js +111 -0
  289. package/tests/step-definitions/screenshot.steps.js +423 -0
  290. package/tests/step-definitions/scroll.steps.js +189 -0
  291. package/tests/step-definitions/selectors.steps.js +1003 -0
  292. package/tests/step-definitions/storage.steps.js +194 -0
  293. package/tests/step-definitions/table.steps.js +181 -0
  294. package/tests/step-definitions/varbase-e2e.js +897 -0
  295. package/tests/step-definitions/varbase.steps.js +458 -0
  296. package/tests/step-definitions/video.steps.js +178 -0
  297. package/tests/step-definitions/wait.steps.js +400 -0
  298. package/tests/step-definitions/web-first.steps.js +298 -0
  299. package/tests/step-definitions/xml.steps.js +411 -0
  300. package/tests/step-definitions/yaml.steps.js +913 -0
  301. package/tsconfig.json +26 -0
@@ -0,0 +1,352 @@
1
+ 'use strict';
2
+
3
+ // All page-level assertion step definitions live here:
4
+ // see / not see text, in element, in row, link href, response body, response
5
+ // status code, element existence, CSS property, regex matching, count.
6
+ //
7
+ // Field-level (checkbox/radio/field-contains) assertions live in field.steps.js.
8
+ // URL/path assertions live in navigation.steps.js.
9
+ // Modal assertions live in modal.steps.js.
10
+
11
+ const { Then } = require('@cucumber/cucumber');
12
+ const assert = require('assert');
13
+ const axios = require('axios');
14
+ const { buildSelector, getLocatorText, friendly } = require('./varbase-e2e');
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Plain page-text
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Assert that text is or is not present on the page.
22
+ *
23
+ * Example #1: Then I should see "Welcome"
24
+ * Example #2: Then we should see "Your accounts for the group is public"
25
+ * Example #3: Then I should not see "Access denied"
26
+ * Example #4: Then we should not see "Edit layout"
27
+ *
28
+ */
29
+ Then(/^(I |we )*should( not)* see "([^"]*)?"$/, async function (pronounCase, notCase, expectedText) {
30
+ if (notCase) {
31
+ const text = await this.page.locator('body').textContent() || '';
32
+ assert.ok(!text.includes(expectedText), `Page should NOT contain "${expectedText}" but it does.`);
33
+ } else {
34
+ await this.page.locator('body').filter({ hasText: expectedText }).waitFor({ timeout: 5000 });
35
+ }
36
+ });
37
+
38
+ /**
39
+ * Assert that page contains or does not contain text matching a regex pattern.
40
+ *
41
+ * Example #1: Then I should see text matching "^T\w+"
42
+ * Example #2: Then I should not see text matching "^O\w+"
43
+ * Example #3: And we should see text matching "\d{4}"
44
+ *
45
+ */
46
+ Then(/^(I |we )*should( not)* see text matching "([^"]*)?"$/, async function (pronounCase, notCase, textPattern) {
47
+ const bodyText = await this.page.evaluate(() => document.body.innerText || '');
48
+ const regex = new RegExp(textPattern);
49
+ if (notCase) {
50
+ assert.ok(!regex.test(bodyText), `Page text should NOT match "${textPattern}" but it does.`);
51
+ } else {
52
+ assert.ok(regex.test(bodyText), `Page text should match "${textPattern}" but it does not.`);
53
+ }
54
+ });
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Table-row text
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Assert text is or is not visible inside a table row identified by text.
62
+ *
63
+ * Example #1: Then I should see "Active" in the "John Smith" row
64
+ * Example #2: Then I should see "In Stock" in the "Product A" row
65
+ * Example #3: Then we should see "Processing" in the "Order #12345" row
66
+ * Example #4: And I should see "Admin" in the "john.smith@example.com" row
67
+ * Example #5: Then I should not see "Admin" in the "Jane Doe" row
68
+ * Example #6: Then I should not see "Out of Stock" in the "Product A" row
69
+ * Example #7: And I should not see "Inactive" in the "john.smith@example.com" row
70
+ *
71
+ */
72
+ Then(/^(I |we )*should( not)* see "([^"]*)?" in( the)* "([^"]*)?" row$/, async function (pronounCase, notCase, expectedText, theCase, rowIdentifier) {
73
+ const found = await this.page.evaluate(({ id, text }) => {
74
+ const rows = Array.from(document.querySelectorAll('tr'));
75
+ return rows.filter(r => r.textContent.includes(id)).some(r => r.textContent.includes(text));
76
+ }, { id: rowIdentifier, text: expectedText });
77
+ if (notCase) {
78
+ assert.ok(!found, `Found "${expectedText}" in the "${rowIdentifier}" row, but it should not be there.`);
79
+ } else {
80
+ assert.ok(found, `Could not find "${expectedText}" in the "${rowIdentifier}" row.`);
81
+ }
82
+ });
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Element existence + content
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * Assert that an element with a given label exists (or does not) on the page.
90
+ *
91
+ * Example #1: Then I should see a "Username" element
92
+ * Example #2: Then I should not see a "Username" element
93
+ * Example #3: And we should see an "Email" element
94
+ *
95
+ */
96
+ Then(/^(I |we )*should( not)* see (a|an) "([^"]*)?" element$/, async function (pronounCase, notCase, aAnCase, element) {
97
+ if (notCase) {
98
+ const text = await this.page.locator('body').textContent() || '';
99
+ assert.ok(!text.includes(element), `Page should NOT contain element text "${element}" but it does.`);
100
+ } else {
101
+ const loc = this.page.getByText(element, { exact: true }).first();
102
+ const forAttr = await loc.getAttribute('for').catch(() => null);
103
+ if (forAttr) {
104
+ await this.page.locator('#' + forAttr).waitFor({ state: 'visible', timeout: 3000 });
105
+ } else {
106
+ await loc.waitFor({ state: 'visible', timeout: 3000 });
107
+ }
108
+ }
109
+ });
110
+
111
+ /**
112
+ * Assert that an element, identified by attribute, exists (or does not) on the page.
113
+ *
114
+ * Example #1: Then I should see a "uname" element by its "id" attr
115
+ * Example #2: Then I should see a "pwordcss" element by attr
116
+ * Example #3: Then I should not see an "emailId" element by its "id" attr
117
+ * Example #4: And I should not see a "countryCss" element by attr
118
+ *
119
+ */
120
+ Then(/^(I |we )*should( not)* see (a|an) "([^"]*)?" element by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (pronounCase, notCase, aAnCase, attrValue, itsCase, attr, attrWord) {
121
+ const loc = this.page.locator(buildSelector(attrValue, attr));
122
+ if (notCase) {
123
+ assert.strictEqual(await loc.count(), 0, `Element should NOT be present but it is.`);
124
+ } else {
125
+ await loc.first().waitFor({ state: 'visible', timeout: 3000 });
126
+ }
127
+ });
128
+
129
+ /**
130
+ * Assert that an element (found by its label) contains or does not contain text.
131
+ *
132
+ * Example #1: Then I should see "John Smith" in the "Username" element
133
+ * Example #2: Then I should not see "Joe Smith" in the "Username" element
134
+ * Example #3: And we should see "1234" in the "Password" element
135
+ *
136
+ */
137
+ Then(/^(I |we )*should( not)* see "([^"]*)?" in( the)* "([^"]*)?" element$/, async function (pronounCase, notCase, expectedText, theCase, element) {
138
+ const forAttr = await this.page.getByText(element, { exact: true }).getAttribute('for').catch(() => null);
139
+ const loc = this.page.locator(forAttr ? '#' + forAttr : element).first();
140
+ await loc.waitFor({ timeout: 5000 });
141
+ const content = await getLocatorText(loc);
142
+ if (notCase) {
143
+ assert.ok(!content.includes(expectedText), `Element should NOT contain "${expectedText}" but it does.`);
144
+ } else {
145
+ assert.ok(content.includes(expectedText), `Element should contain "${expectedText}" but it does not.`);
146
+ }
147
+ });
148
+
149
+ /**
150
+ * Assert that an element (found by its attribute) contains or does not contain text.
151
+ *
152
+ * Example #1: Then I should see "John Smith" in the "uname" element by its "id" attr
153
+ * Example #2: Then I should see "1234" in the "pwordcss" element by attr
154
+ * Example #3: Then I should not see "John Smith" in the "uname" element by its "id" attr
155
+ * Example #4: Then I should not see "1234" in the "pwordcss" element by attr
156
+ *
157
+ */
158
+ Then(/^(I |we )*should( not)* see "([^"]*)?" in( the)* "([^"]*)?" element by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (pronounCase, notCase, expectedText, theCase, attrValue, itsCase, attr, attrWord) {
159
+ const loc = this.page.locator(buildSelector(attrValue, attr)).first();
160
+ await loc.waitFor({ timeout: 10000 });
161
+ const content = await getLocatorText(loc);
162
+ if (notCase) {
163
+ assert.ok(!content.includes(expectedText), `Element should NOT contain "${expectedText}" but it does.`);
164
+ } else {
165
+ assert.ok(content.includes(expectedText), `Element should contain "${expectedText}" but it does not.`);
166
+ }
167
+ });
168
+
169
+ /**
170
+ * Assert that an element's text content does or does not match a regex.
171
+ *
172
+ * Example #1: Then I should see text matching "\d{4}" in the "#year" element
173
+ * Example #2: Then I should see text matching "\$\d+\.\d{2}" in the "#price" element
174
+ * Example #3: Then I should not see text matching "Error" in the "#status" element
175
+ * Example #4: Then we should see text matching "^[A-Z][a-z]+ [A-Z][a-z]+$" in the "#full-name" element
176
+ * Example #5: Then I should see text matching "(0?[1-9]|[12][0-9]|3[01])[\/\-](0?[1-9]|1[012])[\/\-]\d{4}" in the "#date" element
177
+ *
178
+ */
179
+ Then(/^(I |we )*should( not)* see text matching "([^"]*)?" in( the)* "([^"]*)?" element$/, async function (pronounCase, notCase, textPattern, theCase, element) {
180
+ const loc = this.page.locator(element).first();
181
+ await loc.waitFor({ timeout: 5000 });
182
+ const text = await getLocatorText(loc);
183
+ const regex = new RegExp(textPattern);
184
+ if (notCase) {
185
+ assert.ok(!regex.test(text), `Element "${element}" text should NOT match "${textPattern}" but it does.`);
186
+ } else {
187
+ assert.ok(regex.test(text), `Element "${element}" text should match "${textPattern}" but it does not.`);
188
+ }
189
+ });
190
+
191
+ /**
192
+ * Assert that an element has or does not have a specific CSS property.
193
+ *
194
+ * Example #1: Then the "body" element should contain "color:white;"
195
+ * Example #2: Then the "body" element should not contain "color:white;"
196
+ * Example #3: Then the "#uname" element should not contain "border:solid 5px red;"
197
+ * Example #4: Then the "pword" element should not contain "font-size: 26px;"
198
+ *
199
+ */
200
+ Then(/^(the )*"([^"]*)?" element should( not)* contain "([^"]*)?"$/, async function (theCase, selectorRaw, notCase, elementCss) {
201
+ const cssClean = elementCss.replace(/;$/, '');
202
+ const colonIdx = cssClean.indexOf(':');
203
+ const cssProperty = cssClean.substring(0, colonIdx).trim();
204
+ const expectedValue = cssClean.substring(colonIdx + 1).trim();
205
+
206
+ const loc = this.page.locator(buildSelector(selectorRaw)).first();
207
+ await loc.waitFor({ timeout: 10000 });
208
+ const matches = await loc.evaluate((el, { prop, expectedVal }) => {
209
+ function colorToRgb(color) {
210
+ const canvas = document.createElement('canvas');
211
+ canvas.width = canvas.height = 1;
212
+ const ctx = canvas.getContext('2d');
213
+ ctx.fillStyle = 'rgb(1, 2, 3)';
214
+ ctx.fillStyle = color.trim();
215
+ ctx.fillRect(0, 0, 1, 1);
216
+ const d = ctx.getImageData(0, 0, 1, 1).data;
217
+ return 'rgb(' + d[0] + ', ' + d[1] + ', ' + d[2] + ')';
218
+ }
219
+ function isValidColor(value) { return colorToRgb(value) !== 'rgb(1, 2, 3)'; }
220
+ function normalizeColorTokens(value) {
221
+ return value.replace(/rgb\([^)]+\)|rgba\([^)]+\)|#[0-9a-fA-F]+|\b[a-zA-Z]+\b/g,
222
+ (token) => isValidColor(token) ? colorToRgb(token) : token);
223
+ }
224
+ function tokenize(value) {
225
+ const tokens = [], regex = /rgb\([^)]+\)|rgba\([^)]+\)|[^\s]+/g;
226
+ let m;
227
+ while ((m = regex.exec(value)) !== null) tokens.push(m[0]);
228
+ return tokens;
229
+ }
230
+ const computed = window.getComputedStyle(el).getPropertyValue(prop).trim();
231
+ return tokenize(normalizeColorTokens(expectedVal.toLowerCase()))
232
+ .every(token => normalizeColorTokens(computed.toLowerCase()).includes(token));
233
+ }, { prop: cssProperty, expectedVal: expectedValue });
234
+
235
+ if (notCase) {
236
+ assert.ok(!matches, `Element should NOT have CSS "${cssProperty}: ${expectedValue}" but it does.`);
237
+ } else {
238
+ assert.ok(matches, `Element should have CSS "${cssProperty}: ${expectedValue}".`);
239
+ }
240
+ });
241
+
242
+ /**
243
+ * Assert the exact number of elements matching a CSS selector.
244
+ *
245
+ * Example #1: Then I should see 3 "li" elements
246
+ * Example #2: Then I should see 1 "h1" element
247
+ * Example #3: Then we should see 5 ".card" elements
248
+ * Example #4: Then I should see 0 ".error" elements
249
+ * Example #5: And I should see 10 "table tr" elements
250
+ * Example #6: Then I should see 2 "nav a" elements
251
+ * Example #7: Then we should see 4 "ul li" elements
252
+ * Example #8: Then I should see 1 "form" element
253
+ * Example #9: And should see 6 ".product" elements
254
+ * Example #10: Then I should see 8 "[data-testid='row']" elements
255
+ */
256
+ Then(/^(I |we )*should see (\d+) "([^"]*)" elements?$/, async function (pronounCase, num, selector) {
257
+ const expected = parseInt(num, 10);
258
+ const actual = await this.page.locator(selector).count();
259
+ if (actual !== expected) {
260
+ throw friendly(`Expected ${expected} "${selector}" element(s), got ${actual}.`);
261
+ }
262
+ });
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Link href
266
+ // ---------------------------------------------------------------------------
267
+
268
+ /**
269
+ * Assert that a link (located by its visible text) contains the given URL.
270
+ *
271
+ * Example #1: Then the "Login" link should contain "/log-in"
272
+ * Example #2: And the "About Us" link should contain "/about"
273
+ * Example #3: Then "Home" link should contain "/"
274
+ *
275
+ */
276
+ Then(/^(the )*"([^"]*)?" link should contain "([^"]*)?"$/, async function (theCase, element, url) {
277
+ const loc = this.page.getByText(element, { exact: true }).first();
278
+ await loc.waitFor({ timeout: 5000 });
279
+ const href = await loc.evaluate(el => el.href || el.getAttribute('href') || '');
280
+ assert.ok(href.includes(url), `Expected link "${element}" href to contain "${url}" but got "${href}"`);
281
+ });
282
+
283
+ /**
284
+ * Assert that a link located by its attribute contains the given URL.
285
+ *
286
+ * Example #1: Then the "#about-us-id" link should contain "about" by attr
287
+ * Example #2: And the "aboutUs" link should contain "about" by its "class" attribute
288
+ * Example #3: And the ".contactUs" link should contain "/contact-" by attr
289
+ *
290
+ */
291
+ Then(/^(the )*"([^"]*)?" link should contain "([^"]*)?" by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (theCase, attrValue, url, itsCase, attr, attrWord) {
292
+ const loc = this.page.locator(buildSelector(attrValue, attr)).first();
293
+ await loc.waitFor({ timeout: 5000 });
294
+ const href = await loc.evaluate(el => el.href || el.getAttribute('href') || '');
295
+ assert.ok(href.includes(url), `Expected element href to contain "${url}" but got "${href}"`);
296
+ });
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Response body / status
300
+ // ---------------------------------------------------------------------------
301
+
302
+ /**
303
+ * Assert that the rendered HTML response contains or does not contain text.
304
+ *
305
+ * Reads the entire `<html>` text — useful for checking text that lives in
306
+ * `<head>` (meta descriptions, JSON-LD), `<noscript>`, or hidden regions.
307
+ *
308
+ * Example #1: Then the response should contain "Welcome visitor"
309
+ * Example #2: Then the response should not contain "Access denied"
310
+ * Example #3: And the response should contain "<title>Home</title>"
311
+ * Example #4: Then the response should contain "<html"
312
+ * Example #5: When I am on "/about"
313
+ * Then the response should contain "Our mission"
314
+ * And the response should not contain "TODO"
315
+ *
316
+ */
317
+ Then(/^(the )*response should( not)* contain "([^"]*)?"$/, async function (theCase, notCase, expectedText) {
318
+ const text = await this.page.locator('html').textContent() || '';
319
+ if (notCase) {
320
+ assert.ok(!text.includes(expectedText), `Response should NOT contain "${expectedText}" but it does.`);
321
+ } else {
322
+ assert.ok(text.includes(expectedText), `Response should contain "${expectedText}" but it does not.`);
323
+ }
324
+ });
325
+
326
+ /**
327
+ * Assert that the current page's response status is or is not a given code.
328
+ *
329
+ * Example #1: Then the response status code should be 200
330
+ * Example #2: And the response status code should not be 404
331
+ * Example #3: Then the response status code should be 301
332
+ *
333
+ */
334
+ Then(/^(the )*response status code should( not)* be (\d+)$/, async function (theCase, notCase, expectedStatusCode) {
335
+ const currentURL = this.page.url();
336
+ try {
337
+ const response = await axios.get(currentURL);
338
+ const status = response.status;
339
+ if (notCase) {
340
+ assert.notStrictEqual(status, parseInt(expectedStatusCode), `Status code should NOT be ${expectedStatusCode}`);
341
+ } else {
342
+ assert.strictEqual(status, parseInt(expectedStatusCode), `Expected status ${expectedStatusCode} but got ${status}`);
343
+ }
344
+ } catch (error) {
345
+ const status = error.response ? error.response.status : null;
346
+ if (notCase) {
347
+ if (status !== null) assert.notStrictEqual(status, parseInt(expectedStatusCode));
348
+ } else {
349
+ assert.strictEqual(status, parseInt(expectedStatusCode), `Expected status ${expectedStatusCode} but got ${status}`);
350
+ }
351
+ }
352
+ });
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const { friendly } = require('./varbase-e2e');
4
+
5
+ // Authentication-state helpers built on Playwright's `storageState` API.
6
+ //
7
+ // Save the cookies + localStorage of an authenticated context to a JSON file
8
+ // once, then restore that snapshot in later scenarios so each test does not
9
+ // have to walk through the login UI. This is the single biggest speedup
10
+ // available for suites that touch authenticated pages — typical savings of
11
+ // 1-3 seconds per scenario.
12
+ //
13
+ // Recommended layout:
14
+ //
15
+ // tests/auth/admin.json
16
+ // tests/auth/editor.json
17
+ // tests/auth/customer.json
18
+ //
19
+ // Recommended setup feature: tag a one-shot scenario `@auth-setup`, run it
20
+ // once via `npx cucumber-js --tags @auth-setup` after every credential
21
+ // rotation, and let everyday scenarios restore the snapshot.
22
+
23
+ const { Given, When } = require('@cucumber/cucumber');
24
+ const path = require('path');
25
+ const fs = require('fs');
26
+
27
+ function resolveStoragePath(p) {
28
+ return path.isAbsolute(p) ? p : path.join(process.cwd(), p);
29
+ }
30
+
31
+ /**
32
+ * Save the current browser context's cookies + localStorage to a JSON file.
33
+ *
34
+ * Run this AFTER a successful interactive login. The destination file is
35
+ * created (with parent directories) if it does not exist; existing files are
36
+ * overwritten.
37
+ *
38
+ * Example #1: When I save the auth state to "tests/auth/admin.json"
39
+ * Example #2: Given I am on "/login"
40
+ * When I fill in "admin@example.com" for "Email"
41
+ * And I fill in "secret" for "Password"
42
+ * And I press "Sign in"
43
+ * And I wait until the URL contains "/dashboard"
44
+ * Then I save the auth state to "tests/auth/admin.json"
45
+ * Example #3: When I save the auth state to "tests/auth/editor.json"
46
+ * Example #4: When we save the auth state to "tests/auth/customer.json"
47
+ * Example #5: When I save the auth state to "/tmp/admin-state.json"
48
+ *
49
+ */
50
+ When(/^(I |we )*save the auth state to "([^"]*)"$/, async function (pronoun, target) {
51
+ const file = resolveStoragePath(target);
52
+ fs.mkdirSync(path.dirname(file), { recursive: true });
53
+ await this.context.storageState({ path: file });
54
+ });
55
+
56
+ /**
57
+ * Restore cookies + localStorage from a previously saved JSON file.
58
+ *
59
+ * The current browser context is closed and a fresh one is opened with the
60
+ * saved state. The varbase-e2e init script (AJAX/timer/mutation tracker) is
61
+ * re-installed automatically so smart waits keep working. Subsequent steps
62
+ * proceed against the restored, authenticated session.
63
+ *
64
+ * Example #1: Given I restore the auth state from "tests/auth/admin.json"
65
+ * Example #2: Given I restore the auth state from "tests/auth/editor.json"
66
+ * And I am on "/admin/products"
67
+ * Example #3: Given I restore the auth state from "tests/auth/customer.json"
68
+ * And I am on "/account/orders"
69
+ * Example #4: Given we restore the auth state from "tests/auth/admin.json"
70
+ * Example #5: Given I restore the auth state from "tests/auth/admin.json"
71
+ * And I am on "/dashboard"
72
+ * Then "<#user-list>" should have a count of 5 within 5 seconds
73
+ *
74
+ */
75
+ Given(/^(I |we )*restore the auth state from "([^"]*)"$/, async function (pronoun, source) {
76
+ const file = resolveStoragePath(source);
77
+ if (!fs.existsSync(file)) {
78
+ throw friendly(`Auth state file not found: ${file}`);
79
+ }
80
+ const playwrightConfig = require(path.join(process.cwd(), 'playwright.config'));
81
+ const newContext = await this.playwrightBrowser.newContext({
82
+ ...playwrightConfig.contextOptions,
83
+ storageState: file,
84
+ });
85
+ await newContext.addInitScript(() => {
86
+ if (window.__varbaseE2eAjaxInstalled) return;
87
+ window.__varbaseE2eAjaxInstalled = true;
88
+ window.__varbaseE2eAjaxCount = 0;
89
+ window.__varbaseE2ePendingTimers = 0;
90
+ window.__varbaseE2eLastMutation = Date.now();
91
+ });
92
+ if (this.context) await this.context.close();
93
+ this.context = newContext;
94
+ this.page = await this.context.newPage();
95
+ });
96
+
97
+ /**
98
+ * Clear cookies and localStorage / sessionStorage for the current context.
99
+ *
100
+ * Use to anonymize a session mid-scenario, e.g. after asserting a logged-in
101
+ * action you may want to verify the same path returns 302 / login form for
102
+ * an anonymous visitor.
103
+ *
104
+ * Example #1: Given I clear the auth state
105
+ * Example #2: Given I restore the auth state from "tests/auth/admin.json"
106
+ * And I am on "/dashboard"
107
+ * Given I clear the auth state
108
+ * When I am on "/dashboard"
109
+ * Then I should see "Sign in"
110
+ * Example #3: Given we clear the auth state
111
+ * Example #4: When I press "Logout"
112
+ * And I clear the auth state
113
+ * Example #5: Given I clear the auth state
114
+ * When I am on the homepage
115
+ * Then I should see "Login"
116
+ *
117
+ */
118
+ Given(/^(I |we )*clear the auth state$/, async function () {
119
+ await this.context.clearCookies();
120
+ try {
121
+ await this.page.evaluate(() => {
122
+ try { localStorage.clear(); } catch { /* no-op */ }
123
+ try { sessionStorage.clear(); } catch { /* no-op */ }
124
+ });
125
+ } catch { /* page may not be on a real URL yet */ }
126
+ });
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const { friendly } = require('./varbase-e2e');
4
+
5
+ // Clock / time-mocking steps backed by Playwright's `page.clock` API.
6
+
7
+ const { Given, When } = require('@cucumber/cucumber');
8
+
9
+ const ISO_HINT = 'install the fake clock first via "Given the system time is \\"YYYY-MM-DDTHH:MM:SSZ\\"".';
10
+
11
+ /**
12
+ * Install a fake clock anchored at an ISO 8601 instant.
13
+ *
14
+ * Example #1: Given the system time is "2026-05-08T10:00:00Z"
15
+ * Example #2: Given the system time is "2026-01-01T00:00:00Z"
16
+ * Example #3: Given the system time is "2026-12-31T23:59:55Z"
17
+ * Example #4: Given the system time is "2026-05-08T10:00:00Z"
18
+ * Example #5: Given the system time is "2026-07-04T12:00:00-04:00"
19
+ *
20
+ */
21
+ Given(/^the system time is "([^"]*)"$/, async function (iso) {
22
+ const d = new Date(iso);
23
+ if (isNaN(d.getTime())) {
24
+ throw friendly({
25
+ action: `set the system time to "${iso}"`,
26
+ hint: 'use an ISO 8601 date like "2026-05-08T10:00:00Z".',
27
+ });
28
+ }
29
+ try { await this.page.clock.install({ time: d }); }
30
+ catch (e) { throw friendly({ action: `install a fake clock at "${iso}"`, cause: e, hint: 'open a page first via "Given I am on \\"/path\\"".' }); }
31
+ });
32
+
33
+ /**
34
+ * Advance the fake clock forward by N milliseconds.
35
+ *
36
+ * Example #1: When I advance the clock by 500 ms
37
+ * Example #2: When I advance the clock by 1500 ms
38
+ * Example #3: When we advance the clock by 250 ms
39
+ * Example #4: And I advance the clock by 100 ms
40
+ * Example #5: When I advance the clock by 60000 ms
41
+ *
42
+ */
43
+ When(/^(I |we )*advance the clock by (\d+) ?ms$/, async function (pronoun, ms) {
44
+ try { await this.page.clock.runFor(parseInt(ms, 10)); }
45
+ catch (e) { throw friendly({ action: `advance the clock by ${ms} ms`, cause: e, hint: ISO_HINT }); }
46
+ });
47
+
48
+ /**
49
+ * Advance the fake clock forward by N seconds.
50
+ *
51
+ * Example #1: When I advance the clock by 30 seconds
52
+ * Example #2: When I advance the clock by 5 seconds
53
+ * Example #3: When we advance the clock by 1 second
54
+ * Example #4: And I advance the clock by 10 seconds
55
+ * Example #5: When I advance the clock by 90 seconds
56
+ *
57
+ */
58
+ When(/^(I |we )*advance the clock by (\d+) seconds?$/, async function (pronoun, seconds) {
59
+ try { await this.page.clock.runFor(parseInt(seconds, 10) * 1000); }
60
+ catch (e) { throw friendly({ action: `advance the clock by ${seconds} second(s)`, cause: e, hint: ISO_HINT }); }
61
+ });
62
+
63
+ /**
64
+ * Advance the fake clock forward by N minutes.
65
+ *
66
+ * Example #1: When I advance the clock by 5 minutes
67
+ * Example #2: When I advance the clock by 1 minute
68
+ * Example #3: When we advance the clock by 14 minutes
69
+ * Example #4: And I advance the clock by 30 minutes
70
+ * Example #5: When I advance the clock by 60 minutes
71
+ *
72
+ */
73
+ When(/^(I |we )*advance the clock by (\d+) minutes?$/, async function (pronoun, minutes) {
74
+ try { await this.page.clock.runFor(parseInt(minutes, 10) * 60 * 1000); }
75
+ catch (e) { throw friendly({ action: `advance the clock by ${minutes} minute(s)`, cause: e, hint: ISO_HINT }); }
76
+ });
77
+
78
+ /**
79
+ * Pause the fake clock.
80
+ *
81
+ * Example #1: When I pause the clock
82
+ * Example #2: When I pause the clock
83
+ * Example #3: When we pause the clock
84
+ * Example #4: And I pause the clock
85
+ * Example #5: When I pause the clock
86
+ *
87
+ */
88
+ When(/^(I |we )*pause the clock$/, async function () {
89
+ try { await this.page.clock.pauseAt(await this.page.evaluate(() => Date.now())); }
90
+ catch (e) { throw friendly({ action: 'pause the clock', cause: e, hint: ISO_HINT }); }
91
+ });
92
+
93
+ /**
94
+ * Resume the fake clock.
95
+ *
96
+ * Example #1: When I resume the clock
97
+ * Example #2: When I resume the clock
98
+ * Example #3: When we resume the clock
99
+ * Example #4: And I resume the clock
100
+ * Example #5: When I resume the clock
101
+ *
102
+ */
103
+ When(/^(I |we )*resume the clock$/, async function () {
104
+ try { await this.page.clock.resume(); }
105
+ catch (e) { throw friendly({ action: 'resume the clock', cause: e, hint: 'pause the clock first via "When I pause the clock".' }); }
106
+ });
107
+
108
+ /**
109
+ * Set the fake clock to a specific time.
110
+ *
111
+ * Example #1: When I set the system time to "2026-05-08T12:30:00Z"
112
+ * Example #2: When I set the system time to "2026-12-31T23:59:55Z"
113
+ * Example #3: When we set the system time to "2026-01-01T00:00:00Z"
114
+ * Example #4: And I set the system time to "2026-07-04T08:00:00-04:00"
115
+ * Example #5: When I set the system time to "2027-01-01T00:00:00Z"
116
+ *
117
+ */
118
+ When(/^(I |we )*set the system time to "([^"]*)"$/, async function (pronoun, iso) {
119
+ const d = new Date(iso);
120
+ if (isNaN(d.getTime())) {
121
+ throw friendly({
122
+ action: `set the system time to "${iso}"`,
123
+ hint: 'use an ISO 8601 date like "2026-05-08T12:30:00Z".',
124
+ });
125
+ }
126
+ try { await this.page.clock.setSystemTime(d); }
127
+ catch (e) { throw friendly({ action: `set the system time to "${iso}"`, cause: e, hint: ISO_HINT }); }
128
+ });