@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,400 @@
1
+ 'use strict';
2
+
3
+ // All wait/settle step definitions live in this file.
4
+ //
5
+ // Behavior-Based Robotics (BBR) principle: react to the environment, not the
6
+ // clock. A wait step never blocks for a fixed duration — it returns as soon
7
+ // as the page is at the *edge* of activity, bounded by a budget so a runaway
8
+ // page cannot stall the run.
9
+ //
10
+ // Shared smartSettle and modal probes are imported from varbase-e2e.js
11
+ // so this file never duplicates BBR plumbing.
12
+
13
+ const { When } = require('@cucumber/cucumber');
14
+ const { smartSettle, waitForModalState } = require('./varbase-e2e');
15
+
16
+ /**
17
+ * Wait UP TO N seconds for the page to settle. Returns early on idle.
18
+ *
19
+ * "Settle" = DOM ready, no in-flight fetch/XHR, no pending setTimeout
20
+ * callbacks, and no MutationObserver activity for at least 250 ms.
21
+ *
22
+ * Example #1: When I wait for 1 second
23
+ * Example #2: When I wait for 5 seconds
24
+ * Example #3: And I wait for 3 seconds
25
+ * Example #4: When we wait for 2 seconds
26
+ * Example #5: And we wait for 10 seconds
27
+ * Example #6: When I press "Save"
28
+ * And I wait for 2 seconds
29
+ * Then I should see "Saved"
30
+ *
31
+ */
32
+ When(/^(I |we )*wait for (\d+) seconds?$/, async function (pronoun, seconds) {
33
+ await smartSettle(this.page, parseInt(seconds, 10) * 1000);
34
+ });
35
+
36
+ /**
37
+ * Wait UP TO N seconds for the page (and AJAX) to settle. Returns early.
38
+ *
39
+ * Same probe as `wait for N seconds` — kept for legacy phrasing parity with
40
+ * older feature files that say "for AJAX to finish" explicitly.
41
+ *
42
+ * Example #1: When I wait for 1 second for AJAX to finish
43
+ * Example #2: When I wait for 3 seconds for AJAX to finish
44
+ * Example #3: And I wait for 5 seconds for AJAX to finish
45
+ * Example #4: When we wait for 2 seconds for AJAX to finish
46
+ * Example #5: When I press "Search"
47
+ * And I wait for 5 seconds for AJAX to finish
48
+ * Then I should see "Results"
49
+ *
50
+ */
51
+ When(/^(I |we )*wait for (\d+) seconds? for AJAX to finish$/, async function (pronoun, seconds) {
52
+ await smartSettle(this.page, parseInt(seconds, 10) * 1000);
53
+ });
54
+
55
+ /**
56
+ * Wait UP TO a number of seconds for the page to settle.
57
+ *
58
+ * Example #1: When I wait 1 second
59
+ * Example #2: When I wait 5 seconds
60
+ * Example #3: When we wait 3s
61
+ * Example #4: And wait 2s
62
+ * Example #5: And wait 2 seconds
63
+ * Example #6: When we wait 1 second
64
+ * Example #7: When we wait 5 seconds
65
+ * Example #8: When we wait 4s
66
+ *
67
+ */
68
+ When(/^(I |we )*wait (\d+)( second| seconds|s)?$/, async function (pronounCase, number, withSecondWord) {
69
+ await smartSettle(this.page, parseInt(number, 10) * 1000);
70
+ });
71
+
72
+ /**
73
+ * Wait UP TO a maximum number of seconds for the page to settle.
74
+ *
75
+ * Example #1: When I wait max of 1 second
76
+ * Example #2: When I wait max of 5 seconds
77
+ * Example #3: When we wait max of 3s
78
+ * Example #4: And wait max of 2s
79
+ * Example #5: And wait max of 2 seconds
80
+ * Example #6: When we wait max of 1 second
81
+ * Example #7: When we wait max of 5 seconds
82
+ * Example #8: When we wait max of 4s
83
+ *
84
+ */
85
+ When(/^(I |we )*wait max of (\d+)( second| seconds|s)?$/, async function (pronounCase, number, withSecondWord) {
86
+ await smartSettle(this.page, parseInt(number, 10) * 1000);
87
+ });
88
+
89
+ /**
90
+ * Wait UP TO a number of minutes for the page to settle.
91
+ *
92
+ * Example #1: When I wait 1 minute
93
+ * Example #2: When I wait 10 minutes
94
+ * Example #3: When we wait 1m
95
+ * Example #4: And wait 2m
96
+ * Example #5: And wait 2 minutes
97
+ *
98
+ */
99
+ When(/^(I |we )*wait (\d+)( minute| minutes|m)?$/, async function (pronounCase, number, withMinuteWord) {
100
+ await smartSettle(this.page, parseInt(number, 10) * 1000 * 60);
101
+ });
102
+
103
+ /**
104
+ * Wait UP TO a maximum number of minutes for the page to settle.
105
+ *
106
+ * Example #1: When I wait max of 1 minute
107
+ * Example #2: When I wait max of 10 minutes
108
+ * Example #3: When we wait max of 1m
109
+ * Example #4: And wait max of 2m
110
+ * Example #5: And wait max of 2 minutes
111
+ *
112
+ */
113
+ When(/^(I |we )*wait max of (\d+)( minute| minutes|m)?$/, async function (pronounCase, number, withMinuteWord) {
114
+ await smartSettle(this.page, parseInt(number, 10) * 1000 * 60);
115
+ });
116
+
117
+ /**
118
+ * Wait until the page is loaded.
119
+ *
120
+ * Example #1: When I wait until the page is loaded
121
+ * Example #2: When we wait until the page is loaded
122
+ * Example #3: When wait until page loaded
123
+ *
124
+ */
125
+ When(/^(I |we )*wait until( the)* page( is)* loaded*$/, async function (pronounCase, theCase, withIs) {
126
+ await smartSettle(this.page, 10000);
127
+ });
128
+
129
+ /**
130
+ * Wait for active XHR/fetch requests to complete.
131
+ *
132
+ * Example #1: When I wait for AJAX to finish
133
+ * Example #2: And I wait for AJAX to finish
134
+ * Example #3: When we wait for AJAX to finish
135
+ * Example #4: And wait for AJAX to finish
136
+ *
137
+ */
138
+ When(/^(I |we )*wait for AJAX to finish$/, async function (pronounCase) {
139
+ await smartSettle(this.page, 10000);
140
+ });
141
+
142
+ /**
143
+ * Wait for a modal dialog to appear or disappear.
144
+ *
145
+ * Example #1: When I wait for the modal to appear
146
+ * Example #2: When I wait for the modal to disappear
147
+ * Example #3: When we wait for modal to appear
148
+ * Example #4: And I wait for the modal dialog to disappear
149
+ *
150
+ */
151
+ When(/^(I |we )*wait for( the)* modal( dialog)* to (appear|disappear)$/, async function (pronounCase, theCase, dialogCase, appearOrDisappear) {
152
+ const state = appearOrDisappear === 'appear' ? 'visible' : 'hidden';
153
+ await waitForModalState(this.page, state, 10000, this);
154
+ });
155
+
156
+ /**
157
+ * Wait until a CSS selector becomes visible. Returns the moment the
158
+ * matching element is on screen (display !== 'none', visibility !== 'hidden',
159
+ * has a non-zero box). This is the canonical "wait until X appears" probe.
160
+ *
161
+ * Example #1: When I wait for "#dashboard" to appear
162
+ * Example #2: When I wait for ".success-banner" to appear
163
+ * Example #3: And I wait for "[data-testid=user-list]" to appear
164
+ * Example #4: When we wait for ".modal.show" to appear
165
+ * Example #5: When I press "Save"
166
+ * And I wait for ".toast-success" to appear
167
+ *
168
+ */
169
+ When(/^(I |we )*wait for "([^"]*)" to appear$/, async function (pronounCase, selector) {
170
+ await this.page.waitForSelector(selector, { state: 'visible', timeout: 10000 });
171
+ });
172
+
173
+ /**
174
+ * Wait until a CSS selector becomes hidden (display:none, visibility:hidden,
175
+ * detached, or zero-size).
176
+ *
177
+ * Example #1: When I wait for "#loading-spinner" to disappear
178
+ * Example #2: When I wait for ".overlay" to disappear
179
+ * Example #3: And I wait for ".modal.show" to disappear
180
+ * Example #4: When we wait for "[data-testid=skeleton]" to disappear
181
+ * Example #5: When I press "Submit"
182
+ * And I wait for ".loader" to disappear
183
+ * Then I should see "Saved"
184
+ *
185
+ */
186
+ When(/^(I |we )*wait for "([^"]*)" to disappear$/, async function (pronounCase, selector) {
187
+ await this.page.waitForSelector(selector, { state: 'hidden', timeout: 10000 });
188
+ });
189
+
190
+ /**
191
+ * Wait until visible text appears anywhere on the page.
192
+ *
193
+ * Polls `document.body.innerText` every 100 ms for up to 10 s. Match is
194
+ * case-sensitive substring.
195
+ *
196
+ * Example #1: When I wait for the text "Dashboard" to appear
197
+ * Example #2: When I wait for the text "Loading complete" to appear
198
+ * Example #3: And I wait for text "Welcome" to appear
199
+ * Example #4: When we wait for the text "Order #1234" to appear
200
+ * Example #5: When I press "Submit"
201
+ * And I wait for the text "Thank you" to appear
202
+ *
203
+ */
204
+ When(/^(I |we )*wait for( the)* text "([^"]*)" to appear$/, async function (pronounCase, theCase, text) {
205
+ await this.page.waitForFunction(
206
+ (t) => document.body && document.body.innerText.includes(t),
207
+ text, { timeout: 10000, polling: 100 }
208
+ );
209
+ });
210
+
211
+ /**
212
+ * Wait until visible text is gone from the page.
213
+ *
214
+ * Example #1: When I wait for the text "Loading…" to disappear
215
+ * Example #2: When I wait for the text "Saving" to disappear
216
+ * Example #3: And I wait for text "Pending" to disappear
217
+ * Example #4: When we wait for the text "Connecting" to disappear
218
+ * Example #5: When I press "Submit"
219
+ * And I wait for the text "Validating" to disappear
220
+ * Then I should see "Done"
221
+ *
222
+ */
223
+ When(/^(I |we )*wait for( the)* text "([^"]*)" to disappear$/, async function (pronounCase, theCase, text) {
224
+ await this.page.waitForFunction(
225
+ (t) => !document.body || !document.body.innerText.includes(t),
226
+ text, { timeout: 10000, polling: 100 }
227
+ );
228
+ });
229
+
230
+ /**
231
+ * Wait until the current URL contains a fragment.
232
+ *
233
+ * Useful after a redirect, push-state navigation, or hash-based routing.
234
+ *
235
+ * Example #1: When I wait until the URL contains "/dashboard"
236
+ * Example #2: When I wait until the URL contains "?step=2"
237
+ * Example #3: And I wait until the URL contains "#confirmation"
238
+ * Example #4: When we wait until the URL contains "/orders/"
239
+ * Example #5: When I follow "Sign in"
240
+ * And I wait until the URL contains "/auth/callback"
241
+ *
242
+ */
243
+ When(/^(I |we )*wait until( the)* URL contains "([^"]*)"$/, async function (pronounCase, theCase, fragment) {
244
+ await this.page.waitForFunction(
245
+ (frag) => window.location.href.includes(frag),
246
+ fragment, { timeout: 10000, polling: 100 }
247
+ );
248
+ });
249
+
250
+ /**
251
+ * Wait until `document.title` equals (`is`) or contains the given value.
252
+ *
253
+ * Example #1: When I wait until the page title is "Dashboard - MyApp"
254
+ * Example #2: When I wait until the page title contains "Dashboard"
255
+ * Example #3: And I wait until the page title contains "(3 unread)"
256
+ * Example #4: When we wait until the page title is "Order Placed"
257
+ * Example #5: When I follow "Inbox"
258
+ * And I wait until the page title contains "Inbox"
259
+ *
260
+ */
261
+ When(/^(I |we )*wait until( the)* page title (is|contains) "([^"]*)"$/, async function (pronounCase, theCase, op, value) {
262
+ await this.page.waitForFunction(
263
+ ({ op, value }) => op === 'is' ? document.title === value : document.title.includes(value),
264
+ { op, value }, { timeout: 10000, polling: 100 }
265
+ );
266
+ });
267
+
268
+ /**
269
+ * Wait until exactly N elements match a CSS selector.
270
+ *
271
+ * Use 0 to wait for elements to disappear, or a positive count to wait for
272
+ * a specific number to render.
273
+ *
274
+ * Example #1: When I wait until 5 elements match ".product-card"
275
+ * Example #2: When I wait until 0 elements match ".loading-skeleton"
276
+ * Example #3: And I wait until 1 element matches ".active-row"
277
+ * Example #4: When we wait until 10 elements match "tr.user"
278
+ * Example #5: When I press "Load more"
279
+ * And I wait until 20 elements match ".feed-item"
280
+ *
281
+ */
282
+ When(/^(I |we )*wait until (\d+) elements? match(?:es)? "([^"]*)"$/, async function (pronounCase, count, selector) {
283
+ const expected = parseInt(count, 10);
284
+ await this.page.waitForFunction(
285
+ ({ sel, n }) => document.querySelectorAll(sel).length === n,
286
+ { sel: selector, n: expected }, { timeout: 10000, polling: 100 }
287
+ );
288
+ });
289
+
290
+ /**
291
+ * Wait until at least N elements match a CSS selector.
292
+ *
293
+ * Useful for infinite-scroll / pagination tests where the lower bound matters
294
+ * but the exact count may vary.
295
+ *
296
+ * Example #1: When I wait until at least 3 elements match ".item"
297
+ * Example #2: When I wait until at least 1 element matches ".notification"
298
+ * Example #3: And I wait until at least 10 elements match ".feed-item"
299
+ * Example #4: When we wait until at least 5 elements match "tr"
300
+ * Example #5: When I scroll to the bottom
301
+ * And I wait until at least 25 elements match ".product-card"
302
+ *
303
+ */
304
+ When(/^(I |we )*wait until at least (\d+) elements? match(?:es)? "([^"]*)"$/, async function (pronounCase, count, selector) {
305
+ const expected = parseInt(count, 10);
306
+ await this.page.waitForFunction(
307
+ ({ sel, n }) => document.querySelectorAll(sel).length >= n,
308
+ { sel: selector, n: expected }, { timeout: 10000, polling: 100 }
309
+ );
310
+ });
311
+
312
+ /**
313
+ * Wait until the network goes quiet (full smart settle, 10 s budget).
314
+ *
315
+ * Three equivalent phrasings.
316
+ *
317
+ * Example #1: When I wait until the network is idle
318
+ * Example #2: And I wait until requests are complete
319
+ * Example #3: When we wait until network goes quiet
320
+ * Example #4: When I press "Search"
321
+ * And I wait until the network is idle
322
+ * Then I should see "Results"
323
+ * Example #5: When I press "Refresh"
324
+ * And I wait until requests are complete
325
+ *
326
+ */
327
+ When(/^(I |we )*wait until (the network is idle|requests are complete|network goes quiet)$/, async function () {
328
+ await smartSettle(this.page, 10000);
329
+ });
330
+
331
+ /**
332
+ * Wait until the page is interactive (DOMContentLoaded + body attached).
333
+ *
334
+ * Lighter than `wait until the network is idle` — does not wait for late
335
+ * fetch/XHR. Useful when you need to interact with the document early.
336
+ *
337
+ * Example #1: When I wait until the page is interactive
338
+ * Example #2: Given I am on "/heavy-page"
339
+ * When I wait until the page is interactive
340
+ * Example #3: And I wait until the page is interactive
341
+ * Example #4: When we wait until the page is interactive
342
+ * Example #5: Given I am on "/spa"
343
+ * When I wait until the page is interactive
344
+ * Then "<#root>" should be attached
345
+ *
346
+ */
347
+ When(/^(I |we )*wait until( the)* page is interactive$/, async function () {
348
+ await this.page.waitForSelector('body', { state: 'attached', timeout: 10000 });
349
+ await this.page.waitForLoadState('domcontentloaded', { timeout: 10000 });
350
+ });
351
+
352
+ /**
353
+ * Wait until every tracked `setTimeout` callback has fired.
354
+ *
355
+ * Catches "fade-out then display:none" close transitions, debounced renders,
356
+ * and any UI flow that schedules a delayed update without making a network
357
+ * request. Reads `window.__varbaseE2ePendingTimers`, the counter installed by
358
+ * varbase-e2e.js's init script.
359
+ *
360
+ * Example #1: When I wait until pending timers settle
361
+ * Example #2: When I press "Close"
362
+ * And I wait until pending timers settle
363
+ * Then I should not see the modal
364
+ * Example #3: And I wait until pending timers settle
365
+ * Example #4: When we wait until pending timers settle
366
+ * Example #5: When I press "Toast"
367
+ * And I wait until pending timers settle
368
+ * Then I should not see "Saved"
369
+ *
370
+ */
371
+ When(/^(I |we )*wait until pending timers settle$/, async function () {
372
+ await this.page.waitForFunction(
373
+ () => {
374
+ const t = window.__varbaseE2ePendingTimers;
375
+ return typeof t !== 'number' || t <= 0;
376
+ },
377
+ null, { timeout: 10000, polling: 100 }
378
+ );
379
+ });
380
+
381
+ /**
382
+ * Pollable text assertion — retries the inner step until passing or timeout.
383
+ *
384
+ * Use sparingly; prefer explicit edge waits (`wait for "X" to appear`).
385
+ *
386
+ * Example #1: Then eventually I should see "Done"
387
+ * Example #2: Then eventually I should see "Loaded" within 5 seconds
388
+ * Example #3: And eventually we should see "Saved"
389
+ * Example #4: Then eventually I should see "Order Confirmed" within 30 seconds
390
+ * Example #5: When I press "Submit"
391
+ * Then eventually I should see "Submitted" within 8 seconds
392
+ *
393
+ */
394
+ When(/^eventually (I |we )*should see "([^"]*)"(?: within (\d+) seconds?)?$/, async function (pronoun, text, seconds) {
395
+ const total = (seconds ? parseInt(seconds, 10) : 10) * 1000;
396
+ await this.page.waitForFunction(
397
+ (t) => document.body && document.body.innerText.includes(t),
398
+ text, { timeout: total, polling: 100 }
399
+ );
400
+ });
@@ -0,0 +1,298 @@
1
+ 'use strict';
2
+
3
+ const { friendly } = require('./varbase-e2e');
4
+
5
+ // Web-first assertion steps — each matcher polls the live page until the
6
+ // condition holds or the per-step timeout elapses. The assertion IS the
7
+ // wait, so no `wait for AJAX` is required between an action and its check.
8
+ //
9
+ // Pattern (all steps): "<selector>" should [not] be <state> [within N seconds]
10
+ // Default budget is 5s; override with the trailing "within N seconds" clause.
11
+
12
+ const { Then, When } = require('@cucumber/cucumber');
13
+ const assert = require('assert');
14
+
15
+ function parseTimeout(secondsStr) {
16
+ const n = secondsStr ? parseInt(secondsStr, 10) : 0;
17
+ return n > 0 ? n * 1000 : 5000;
18
+ }
19
+
20
+ // Poll fn() until it returns truthy, or timeout. fn() returns a value;
21
+ // `predicate` decides pass/fail. Returns the last value seen.
22
+ async function poll(fn, predicate, timeout, message) {
23
+ const deadline = Date.now() + timeout;
24
+ let last;
25
+ while (Date.now() < deadline) {
26
+ try {
27
+ last = await fn();
28
+ if (predicate(last)) return last;
29
+ } catch (e) {
30
+ last = e;
31
+ }
32
+ await new Promise((r) => setTimeout(r, 100));
33
+ }
34
+ throw friendly(`${message} (last seen: ${typeof last === 'object' ? JSON.stringify(last) : String(last)})`);
35
+ }
36
+
37
+ async function checkState(locator, state) {
38
+ switch (state) {
39
+ case 'visible': return locator.first().isVisible();
40
+ case 'hidden': return !(await locator.first().isVisible().catch(() => false));
41
+ case 'attached': return (await locator.count()) > 0;
42
+ case 'focused': return locator.first().evaluate((el) => el === document.activeElement);
43
+ case 'enabled': return locator.first().isEnabled();
44
+ case 'disabled': return locator.first().isDisabled();
45
+ case 'editable': return locator.first().isEditable();
46
+ }
47
+ return false;
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // State matchers
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * Auto-retrying state assertion. Polls the live page until the matcher passes
56
+ * or the budget elapses. Default budget 5 s.
57
+ *
58
+ * Example #1: Then "#dashboard" should be visible
59
+ * Example #2: Then ".success-banner" should be visible within 10 seconds
60
+ * Example #3: And "[data-testid=signup-cta]" should be focused
61
+ * Example #4: Then "button.submit" should be enabled
62
+ * Example #5: Then "input#email" should be editable
63
+ *
64
+ */
65
+ Then(/^"([^"]*)" should be (visible|hidden|attached|focused|enabled|disabled|editable)(?: within (\d+) seconds?)?$/, async function (selector, state, sec) {
66
+ const loc = this.page.locator(selector);
67
+ await poll(
68
+ () => checkState(loc, state),
69
+ (v) => v === true,
70
+ parseTimeout(sec),
71
+ `Expected "${selector}" to be ${state}`
72
+ );
73
+ });
74
+
75
+ /**
76
+ * Auto-retrying negated state assertion.
77
+ *
78
+ * Example #1: Then "#loading-spinner" should not be visible
79
+ * Example #2: Then ".error" should not be visible within 5 seconds
80
+ * Example #3: And "button.submit" should not be disabled
81
+ * Example #4: Then "input#email" should not be editable
82
+ * Example #5: Then "[data-testid=password]" should not be focused
83
+ *
84
+ */
85
+ Then(/^"([^"]*)" should not be (visible|hidden|attached|focused|enabled|disabled|editable)(?: within (\d+) seconds?)?$/, async function (selector, state, sec) {
86
+ const loc = this.page.locator(selector);
87
+ await poll(
88
+ () => checkState(loc, state),
89
+ (v) => v === false,
90
+ parseTimeout(sec),
91
+ `Expected "${selector}" to not be ${state}`
92
+ );
93
+ });
94
+
95
+ /**
96
+ * Assert an element's bounding box overlaps the visible viewport rectangle.
97
+ *
98
+ * Example #1: Then "#hero" should be in the viewport
99
+ * Example #2: Then "[data-testid=cta]" should be in the viewport within 3 seconds
100
+ * Example #3: When I scroll to the element "#footer"
101
+ * Then "#footer" should be in the viewport
102
+ * Example #4: Then "h1" should be in the viewport
103
+ * Example #5: Then ".banner" should be in the viewport
104
+ *
105
+ */
106
+ Then(/^"([^"]*)" should be in the viewport(?: within (\d+) seconds?)?$/, async function (selector, sec) {
107
+ const loc = this.page.locator(selector).first();
108
+ await poll(
109
+ async () => loc.evaluate((el) => {
110
+ const r = el.getBoundingClientRect();
111
+ return r.bottom > 0 && r.top < (window.innerHeight || 0)
112
+ && r.right > 0 && r.left < (window.innerWidth || 0);
113
+ }),
114
+ (v) => v === true,
115
+ parseTimeout(sec),
116
+ `Expected "${selector}" to be in viewport`
117
+ );
118
+ });
119
+
120
+ /**
121
+ * Assert an element is OUTSIDE the visible viewport rectangle.
122
+ *
123
+ * Example #1: Then "#footer" should not be in the viewport
124
+ * Example #2: Then "[data-testid=hidden]" should not be in the viewport within 3 seconds
125
+ * Example #3: And ".off-screen" should not be in the viewport
126
+ * Example #4: Then "section.below-fold" should not be in the viewport
127
+ * Example #5: When I scroll to the top
128
+ * Then "#footer" should not be in the viewport
129
+ *
130
+ */
131
+ Then(/^"([^"]*)" should not be in the viewport(?: within (\d+) seconds?)?$/, async function (selector, sec) {
132
+ const loc = this.page.locator(selector).first();
133
+ await poll(
134
+ async () => loc.evaluate((el) => {
135
+ const r = el.getBoundingClientRect();
136
+ return r.bottom > 0 && r.top < (window.innerHeight || 0)
137
+ && r.right > 0 && r.left < (window.innerWidth || 0);
138
+ }).catch(() => false),
139
+ (v) => v === false,
140
+ parseTimeout(sec),
141
+ `Expected "${selector}" to not be in viewport`
142
+ );
143
+ });
144
+
145
+ /**
146
+ * Assert exactly N elements match a selector (auto-retry).
147
+ *
148
+ * Example #1: Then ".product-card" should have a count of 12
149
+ * Example #2: Then ".error" should have a count of 0 within 5 seconds
150
+ * Example #3: And "tr" should have a count of 21
151
+ * Example #4: Then "[data-testid=item]" should have a count of 5
152
+ * Example #5: Then ".feed-item" should have a count of 25 within 10 seconds
153
+ *
154
+ */
155
+ Then(/^"([^"]*)" should have a count of (\d+)(?: within (\d+) seconds?)?$/, async function (selector, count, sec) {
156
+ const expected = parseInt(count, 10);
157
+ const loc = this.page.locator(selector);
158
+ await poll(
159
+ () => loc.count(),
160
+ (v) => v === expected,
161
+ parseTimeout(sec),
162
+ `Expected "${selector}" count to be ${expected}`
163
+ );
164
+ });
165
+
166
+ /**
167
+ * Assert an element's textContent equals an expected string (trimmed).
168
+ *
169
+ * Example #1: Then "h1" should have text "Welcome"
170
+ * Example #2: Then ".badge" should have text "12" within 3 seconds
171
+ * Example #3: And ".total" should have text "$99.00"
172
+ * Example #4: Then "[data-testid=subtitle]" should have text "Premium"
173
+ * Example #5: Then ".alert" should have text "Saved"
174
+ *
175
+ */
176
+ Then(/^"([^"]*)" should have text "([^"]*)"(?: within (\d+) seconds?)?$/, async function (selector, text, sec) {
177
+ const loc = this.page.locator(selector).first();
178
+ await poll(
179
+ () => loc.textContent(),
180
+ (v) => (v || '').trim() === text,
181
+ parseTimeout(sec),
182
+ `Expected "${selector}" text to be "${text}"`
183
+ );
184
+ });
185
+
186
+ /**
187
+ * Assert an element's textContent contains an expected substring.
188
+ *
189
+ * Example #1: Then "h1" should contain text "Welcome"
190
+ * Example #2: Then ".badge" should contain text "unread" within 3 seconds
191
+ * Example #3: And ".total" should contain text "$99"
192
+ * Example #4: Then "[data-testid=subtitle]" should contain text "Premium"
193
+ * Example #5: Then ".alert" should contain text "Saved"
194
+ *
195
+ */
196
+ Then(/^"([^"]*)" should contain text "([^"]*)"(?: within (\d+) seconds?)?$/, async function (selector, text, sec) {
197
+ const loc = this.page.locator(selector).first();
198
+ await poll(
199
+ () => loc.textContent(),
200
+ (v) => (v || '').includes(text),
201
+ parseTimeout(sec),
202
+ `Expected "${selector}" to contain text "${text}"`
203
+ );
204
+ });
205
+
206
+ /**
207
+ * Assert an input's `value` property equals an expected string.
208
+ *
209
+ * Example #1: Then "#email" should have value "alice@example.com"
210
+ * Example #2: Then "input[name=q]" should have value "laptops"
211
+ * Example #3: And "[data-testid=phone]" should have value "0790000000"
212
+ * Example #4: Then "#bio" should have value "" within 3 seconds
213
+ * Example #5: Then "select#country" should have value "JO"
214
+ *
215
+ */
216
+ Then(/^"([^"]*)" should have value "([^"]*)"(?: within (\d+) seconds?)?$/, async function (selector, value, sec) {
217
+ const loc = this.page.locator(selector).first();
218
+ await poll(
219
+ () => loc.inputValue(),
220
+ (v) => v === value,
221
+ parseTimeout(sec),
222
+ `Expected "${selector}" value to be "${value}"`
223
+ );
224
+ });
225
+
226
+ /**
227
+ * Assert an element's attribute equals an expected value.
228
+ *
229
+ * Example #1: Then "#tab-1" should have attribute "aria-selected" with value "true"
230
+ * Example #2: Then "[data-testid=cta]" should have attribute "data-state" with value "open"
231
+ * Example #3: And "img.logo" should have attribute "alt" with value "Vardot"
232
+ * Example #4: Then "a.signup" should have attribute "href" with value "/signup"
233
+ * Example #5: Then "input#email" should have attribute "type" with value "email"
234
+ *
235
+ */
236
+ Then(/^"([^"]*)" should have attribute "([^"]*)" with value "([^"]*)"(?: within (\d+) seconds?)?$/, async function (selector, attr, value, sec) {
237
+ const loc = this.page.locator(selector).first();
238
+ await poll(
239
+ () => loc.getAttribute(attr),
240
+ (v) => v === value,
241
+ parseTimeout(sec),
242
+ `Expected "${selector}" attribute "${attr}" to be "${value}"`
243
+ );
244
+ });
245
+
246
+ /**
247
+ * Assert an element's `class` attribute contains a class token.
248
+ *
249
+ * Example #1: Then "#tab-1" should have class "is-active"
250
+ * Example #2: Then ".modal" should have class "show" within 3 seconds
251
+ * Example #3: And ".dropdown" should have class "open"
252
+ * Example #4: Then "tr.row-1" should have class "selected"
253
+ * Example #5: Then "[data-testid=card]" should have class "highlighted"
254
+ *
255
+ */
256
+ Then(/^"([^"]*)" should have class "([^"]*)"(?: within (\d+) seconds?)?$/, async function (selector, cls, sec) {
257
+ const loc = this.page.locator(selector).first();
258
+ await poll(
259
+ () => loc.getAttribute('class'),
260
+ (v) => (v || '').split(/\s+/).includes(cls),
261
+ parseTimeout(sec),
262
+ `Expected "${selector}" to have class "${cls}"`
263
+ );
264
+ });
265
+
266
+ /**
267
+ * Click an element by its accessible role + name (Playwright `getByRole`).
268
+ *
269
+ * Example #1: When I click the "Sign in" button
270
+ * Example #2: When I click the "Profile" link
271
+ * Example #3: And we click the "Notifications" tab
272
+ * Example #4: When I click the "Subscribe" checkbox
273
+ * Example #5: When I click the "Premium" radio
274
+ *
275
+ */
276
+ When(/^(I |we )*click the "([^"]*)" (button|link|tab|menuitem|checkbox|radio|option)$/, async function (pronoun, name, role) {
277
+ await this.page.getByRole(role, { name }).first().click();
278
+ });
279
+
280
+ /**
281
+ * Assert a role-addressed element is visible (auto-retry).
282
+ *
283
+ * Example #1: Then the "Save changes" button should be visible
284
+ * Example #2: Then the "Profile" link should be visible within 5 seconds
285
+ * Example #3: And the "Privacy" tab should be visible
286
+ * Example #4: Then the "I agree" checkbox should be visible
287
+ * Example #5: Then the "Premium" radio should be visible
288
+ *
289
+ */
290
+ Then(/^the "([^"]*)" (button|link|tab|menuitem|checkbox|radio|option) should be visible(?: within (\d+) seconds?)?$/, async function (name, role, sec) {
291
+ const loc = this.page.getByRole(role, { name }).first();
292
+ await poll(
293
+ () => loc.isVisible(),
294
+ (v) => v === true,
295
+ parseTimeout(sec),
296
+ `Expected ${role} "${name}" to be visible`
297
+ );
298
+ });