@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,50 @@
1
+ 'use strict';
2
+
3
+ // -----------------------------------------------------------------------------
4
+ // Paragraphs step definitions: choose a paragraph component in the add-widget
5
+ // dialog of a paragraphs field.
6
+ //
7
+ // Ported from the Varbase Project BDD suites (11.0.x, 10.1.x, 9.2.x) so every
8
+ // Varbase site gets them from @vardot/varbase-e2e instead of copying them into
9
+ // each project's own tests/step-definitions/.
10
+ // -----------------------------------------------------------------------------
11
+
12
+ const { When } = require('@cucumber/cucumber');
13
+ const { smartSettle, friendly } = require('./varbase-e2e');
14
+ const { budget } = require('./drupal-helpers');
15
+
16
+ /**
17
+ * Select a paragraph component in the "Add component" dialog.
18
+ *
19
+ * Ports VarbaseContext::iSelectTheParagraphComponent: clicks the add button in
20
+ * the paragraphs-add-dialog whose name matches the component.
21
+ *
22
+ * Example #1: When I select the "Text" paragraph component
23
+ * Example #2: And I select the "Modal" paragraph component
24
+ * Example #3: When we select the "Drupal block" paragraph component
25
+ * Example #4: And I select the "Rich Text" paragraph component
26
+ * Example #5: Given I select the "Accordion" paragraph component
27
+ */
28
+ When(/^(?:I |we )*select the "([^"]*)" paragraph component$/, async function (component) {
29
+ const ok = await this.page.evaluate((component) => {
30
+ const dialog = document.querySelector('.paragraphs-add-dialog.ui-dialog-content, .paragraphs-add-dialog');
31
+ const scope = dialog || document;
32
+ const btn = [...scope.querySelectorAll('input, button')]
33
+ .find((b) => (b.getAttribute('name') || '').includes(component) || (b.value || '').includes(component) || (b.textContent || '').includes(component));
34
+ if (!btn) return false;
35
+ // The paragraphs "Add <type>" buttons are Drupal AJAX submit buttons whose
36
+ // handler is bound to the `mousedown` event (Drupal core ajax.js binds form
37
+ // buttons on mousedown), so a bare element.click() fires only a `click`
38
+ // event and the AJAX that inserts the paragraph subform never runs — the
39
+ // dialog just sits there and the subform (and its CKEditor field) never
40
+ // appears. Dispatch the full pointer sequence so the mousedown-bound AJAX
41
+ // fires and the subform is added.
42
+ btn.scrollIntoView({ block: 'center' });
43
+ for (const type of ['mousedown', 'mouseup', 'click']) {
44
+ btn.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
45
+ }
46
+ return true;
47
+ }, component);
48
+ if (!ok) throw friendly(`Could not find the "${component}" paragraph component in the add dialog.`);
49
+ await smartSettle(this.page, budget(this));
50
+ });
@@ -0,0 +1,356 @@
1
+ 'use strict';
2
+
3
+ const { friendly } = require('./varbase-e2e');
4
+
5
+ // Element interactions and visual / positional assertions using Playwright.
6
+
7
+ const { Given, When, Then } = require('@cucumber/cucumber');
8
+ const assert = require('assert');
9
+
10
+ /**
11
+ * Assert element A is positioned vertically below element B (greater Y).
12
+ *
13
+ * Example #1: Then the element "#footer" should appear after the element "#main"
14
+ * Example #2: Then the element ".checkout" should appear after the element ".cart"
15
+ * Example #3: And the element "#summary" should appear after the element "#details"
16
+ * Example #4: Then the element "h2" should appear after the element "h1"
17
+ * Example #5: Then the element "[data-testid=cta]" should appear after the element ".hero"
18
+ *
19
+ */
20
+ Then('the element {string} should appear after the element {string}', async function (sel1, sel2) {
21
+ const el1 = await this.page.locator(sel1).first();
22
+ const el2 = await this.page.locator(sel2).first();
23
+ const b1 = await el1.boundingBox();
24
+ const b2 = await el2.boundingBox();
25
+ assert.ok(b1 && b2, 'Both elements must be visible.');
26
+ assert.ok(b1.y > b2.y, `Element "${sel1}" should appear after "${sel2}".`);
27
+ });
28
+
29
+ /**
30
+ * Assert text A appears after text B in the document body's text order.
31
+ *
32
+ * Example #1: Then the text "Sign in" should appear after the text "Welcome"
33
+ * Example #2: Then the text "Total" should appear after the text "Subtotal"
34
+ * Example #3: And the text "Footer" should appear after the text "Header"
35
+ * Example #4: Then the text "Privacy" should appear after the text "Terms"
36
+ * Example #5: Then the text "Step 3" should appear after the text "Step 2"
37
+ *
38
+ */
39
+ Then('the text {string} should appear after the text {string}', async function (t1, t2) {
40
+ const body = await this.page.locator('body').innerText();
41
+ const i1 = body.indexOf(t1);
42
+ const i2 = body.indexOf(t2);
43
+ assert.ok(i1 !== -1, `Text "${t1}" not found.`);
44
+ assert.ok(i2 !== -1, `Text "${t2}" not found.`);
45
+ assert.ok(i1 > i2, `Text "${t1}" should appear after "${t2}".`);
46
+ });
47
+
48
+ /**
49
+ * Assert at least one element matching `<selector>[<attr>="<value>"]` exists.
50
+ *
51
+ * Example #1: Then the element "a" with the attribute "href" and the value "/about" should exist
52
+ * Example #2: Then the element "input" with the attribute "name" and the value "email" should exist
53
+ * Example #3: And the element "button" with the attribute "type" and the value "submit" should exist
54
+ * Example #4: Then the element "div" with the attribute "data-testid" and the value "user-list" should exist
55
+ * Example #5: Then the element "img" with the attribute "alt" and the value "Logo" should exist
56
+ *
57
+ */
58
+ Then('the element {string} with the attribute {string} and the value {string} should exist', async function (sel, attr, value) {
59
+ const count = await this.page.locator(`${sel}[${attr}="${value}"]`).count();
60
+ assert.ok(count > 0, `Element "${sel}" with ${attr}="${value}" not found.`);
61
+ });
62
+
63
+ /**
64
+ * Assert at least one element with attribute value containing a substring exists.
65
+ *
66
+ * Example #1: Then the element "a" with the attribute "href" and the value containing "/about" should exist
67
+ * Example #2: Then the element "input" with the attribute "class" and the value containing "is-valid" should exist
68
+ * Example #3: And the element "button" with the attribute "data-testid" and the value containing "cta" should exist
69
+ * Example #4: Then the element "img" with the attribute "src" and the value containing ".png" should exist
70
+ * Example #5: Then the element "div" with the attribute "aria-label" and the value containing "card" should exist
71
+ *
72
+ */
73
+ Then('the element {string} with the attribute {string} and the value containing {string} should exist', async function (sel, attr, value) {
74
+ const count = await this.page.locator(`${sel}[${attr}*="${value}"]`).count();
75
+ assert.ok(count > 0, `Element "${sel}" with ${attr} containing "${value}" not found.`);
76
+ });
77
+
78
+ /**
79
+ * Assert NO element matches `<selector>[<attr>="<value>"]`.
80
+ *
81
+ * Example #1: Then the element "a" with the attribute "href" and the value "/admin" should not exist
82
+ * Example #2: Then the element "input" with the attribute "type" and the value "hidden" should not exist
83
+ * Example #3: And the element "button" with the attribute "disabled" and the value "true" should not exist
84
+ * Example #4: Then the element "div" with the attribute "data-testid" and the value "error" should not exist
85
+ * Example #5: Then the element "img" with the attribute "alt" and the value "" should not exist
86
+ *
87
+ */
88
+ Then('the element {string} with the attribute {string} and the value {string} should not exist', async function (sel, attr, value) {
89
+ const count = await this.page.locator(`${sel}[${attr}="${value}"]`).count();
90
+ assert.strictEqual(count, 0, `Element "${sel}" with ${attr}="${value}" should not exist.`);
91
+ });
92
+
93
+ /**
94
+ * Assert NO element has attribute value containing a substring.
95
+ *
96
+ * Example #1: Then the element "a" with the attribute "href" and the value containing "/old" should not exist
97
+ * Example #2: Then the element "div" with the attribute "class" and the value containing "is-error" should not exist
98
+ * Example #3: And the element "button" with the attribute "data-testid" and the value containing "deprecated" should not exist
99
+ * Example #4: Then the element "img" with the attribute "src" and the value containing "tracking" should not exist
100
+ * Example #5: Then the element "input" with the attribute "name" and the value containing "_legacy" should not exist
101
+ *
102
+ */
103
+ Then('the element {string} with the attribute {string} and the value containing {string} should not exist', async function (sel, attr, value) {
104
+ const count = await this.page.locator(`${sel}[${attr}*="${value}"]`).count();
105
+ assert.strictEqual(count, 0, `Element "${sel}" with ${attr} containing "${value}" should not exist.`);
106
+ });
107
+
108
+ /**
109
+ * Assert an element is positioned within the top 100 px of the viewport.
110
+ *
111
+ * Example #1: Then the element "#header" should be at the top of the viewport
112
+ * Example #2: Then the element "h1" should be at the top of the viewport
113
+ * Example #3: And the element ".sticky-nav" should be at the top of the viewport
114
+ * Example #4: When I scroll to the top
115
+ * Then the element "#hero" should be at the top of the viewport
116
+ * Example #5: Then the element "[role=banner]" should be at the top of the viewport
117
+ *
118
+ */
119
+ Then('the element {string} should be at the top of the viewport', async function (sel) {
120
+ const box = await this.page.locator(sel).first().boundingBox();
121
+ assert.ok(box, `Element "${sel}" not found or not visible.`);
122
+ assert.ok(box.y >= 0 && box.y <= 100, `Element "${sel}" is not at top of viewport (y=${box.y}).`);
123
+ });
124
+
125
+ /**
126
+ * Assert an element is horizontally centered in the viewport (±10% tolerance).
127
+ *
128
+ * Example #1: Then the element ".hero" should be centered in the viewport
129
+ * Example #2: Then the element "#cta" should be centered in the viewport
130
+ * Example #3: And the element ".modal-dialog" should be centered in the viewport
131
+ * Example #4: Then the element "h1" should be centered in the viewport
132
+ * Example #5: Then the element ".loader" should be centered in the viewport
133
+ *
134
+ */
135
+ Then('the element {string} should be centered in the viewport', async function (sel) {
136
+ const box = await this.page.locator(sel).first().boundingBox();
137
+ assert.ok(box, `Element "${sel}" not found.`);
138
+ const vw = this.page.viewportSize().width;
139
+ const center = box.x + box.width / 2;
140
+ const tolerance = vw * 0.1;
141
+ assert.ok(Math.abs(center - vw / 2) <= tolerance, `Element "${sel}" not centered (center=${center}, vw=${vw}).`);
142
+ });
143
+
144
+ // Native browser dialog handlers (alert / confirm / prompt) live in
145
+ // tests/step-definitions/dialog.steps.js.
146
+
147
+ /**
148
+ * Click an element addressed by CSS selector.
149
+ *
150
+ * Example #1: When I click on the element "#sign-in"
151
+ * Example #2: When I click on the element ".btn-primary"
152
+ * Example #3: And we click on the element "[data-testid=cta]"
153
+ * Example #4: When I click on the element "li.nav-item:first-child a"
154
+ * Example #5: When I click on the element "button[aria-label='Close']"
155
+ *
156
+ */
157
+ When(/^(I |we )*click on the element "([^"]*)"$/, async function (pronoun, sel) {
158
+ try {
159
+ await this.page.locator(sel).first().click();
160
+ } catch (e) {
161
+ throw friendly({
162
+ action: `click on the element "${sel}"`,
163
+ cause: e,
164
+ hint: `check that the element exists on the page, is visible, and is clickable.`,
165
+ });
166
+ }
167
+ });
168
+
169
+ /**
170
+ * Dispatch a synthetic JavaScript event on a CSS-addressed element.
171
+ *
172
+ * Useful for components that listen for non-bubbling events or for events
173
+ * Playwright cannot synthesize through normal interactions.
174
+ *
175
+ * Example #1: When I trigger the JS event "click" on the element "#cta"
176
+ * Example #2: When I trigger the JS event "change" on the element "#country"
177
+ * Example #3: And I trigger the JS event "input" on the element "#search"
178
+ * Example #4: When I trigger the JS event "focus" on the element "#email"
179
+ * Example #5: When I trigger the JS event "blur" on the element "#name"
180
+ *
181
+ */
182
+ When(/^(I |we )*trigger the JS event "([^"]*)" on the element "([^"]*)"$/, async function (pronoun, event, sel) {
183
+ try {
184
+ await this.page.locator(sel).first().dispatchEvent(event);
185
+ } catch (e) {
186
+ throw friendly({
187
+ action: `trigger the "${event}" event on "${sel}"`,
188
+ cause: e,
189
+ hint: `use a standard event name like click, change, input, focus, or blur.`,
190
+ });
191
+ }
192
+ });
193
+
194
+ /**
195
+ * Scroll a CSS-addressed element into view (uses Playwright's auto-scroll).
196
+ *
197
+ * Example #1: When I scroll to the element "#footer"
198
+ * Example #2: When I scroll to the element ".pricing-table"
199
+ * Example #3: And we scroll to the element "[data-testid=signup-cta]"
200
+ * Example #4: When I scroll to the element "tbody tr:nth-child(20)"
201
+ * Example #5: When I scroll to the element ".testimonial:last-child"
202
+ *
203
+ */
204
+ When(/^(I |we )*scroll to the element "([^"]*)"$/, async function (pronoun, sel) {
205
+ try {
206
+ await this.page.locator(sel).first().scrollIntoViewIfNeeded();
207
+ } catch (e) {
208
+ throw friendly({
209
+ action: `scroll to "${sel}"`,
210
+ cause: e,
211
+ hint: `check the element exists on the page and was not removed before scrolling.`,
212
+ });
213
+ }
214
+ });
215
+
216
+ /**
217
+ * Hover the mouse pointer over a CSS-addressed element.
218
+ *
219
+ * Example #1: When I hover over the element ".tooltip-trigger"
220
+ * Example #2: When I hover over the element "#user-menu"
221
+ * Example #3: And we hover over the element "[data-testid=avatar]"
222
+ * Example #4: When I hover over the element "img.preview"
223
+ * Example #5: When I hover over the element "li.menu-item"
224
+ *
225
+ */
226
+ When(/^(I |we )*hover over the element "([^"]*)"$/, async function (pronoun, sel) {
227
+ try {
228
+ await this.page.locator(sel).first().hover();
229
+ } catch (e) {
230
+ throw friendly({
231
+ action: `hover over "${sel}"`,
232
+ cause: e,
233
+ hint: `check the element exists, is visible, and is not covered by another element.`,
234
+ });
235
+ }
236
+ });
237
+
238
+ /**
239
+ * Move keyboard focus to a CSS-addressed element.
240
+ *
241
+ * Example #1: When I focus on the element "#email"
242
+ * Example #2: When I focus on the element "input[type=search]"
243
+ * Example #3: And we focus on the element "[data-testid=combobox]"
244
+ * Example #4: When I focus on the element "textarea#message"
245
+ * Example #5: When I focus on the element "button.primary"
246
+ *
247
+ */
248
+ When(/^(I |we )*focus on the element "([^"]*)"$/, async function (pronoun, sel) {
249
+ try {
250
+ await this.page.locator(sel).first().focus();
251
+ } catch (e) {
252
+ throw friendly({
253
+ action: `focus on "${sel}"`,
254
+ cause: e,
255
+ hint: `the element must be focusable — an input, button, link, or have a tabindex.`,
256
+ });
257
+ }
258
+ });
259
+
260
+ /**
261
+ * Wait for a CSS-addressed element to become visible (display !== none, etc.).
262
+ *
263
+ * Example #1: Then the element "#dashboard" should be displayed
264
+ * Example #2: Then the element ".success-banner" should be displayed
265
+ * Example #3: And the element "[data-testid=user-list]" should be displayed
266
+ * Example #4: Then the element ".toast" should be displayed
267
+ * Example #5: When I press "Save"
268
+ * Then the element ".toast-success" should be displayed
269
+ *
270
+ */
271
+ Then('the element {string} should be displayed', async function (sel) {
272
+ await this.page.locator(sel).first().waitFor({ state: 'visible' });
273
+ });
274
+
275
+ /**
276
+ * Assert a CSS-addressed element is hidden or absent.
277
+ *
278
+ * Example #1: Then the element "#loading-spinner" should not be displayed
279
+ * Example #2: Then the element ".error-banner" should not be displayed
280
+ * Example #3: And the element "[data-testid=skeleton]" should not be displayed
281
+ * Example #4: Then the element ".modal" should not be displayed
282
+ * Example #5: When I press "Submit"
283
+ * Then the element ".pending-badge" should not be displayed
284
+ *
285
+ */
286
+ Then('the element {string} should not be displayed', async function (sel) {
287
+ const loc = this.page.locator(sel).first();
288
+ const count = await loc.count();
289
+ if (count === 0) return;
290
+ assert.ok(!(await loc.isVisible()), `Element "${sel}" should not be displayed.`);
291
+ });
292
+
293
+ async function inViewport(page, sel, topOffset = 0) {
294
+ return await page.locator(sel).first().evaluate((el, off) => {
295
+ const r = el.getBoundingClientRect();
296
+ return r.top >= off && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth;
297
+ }, topOffset);
298
+ }
299
+
300
+ /**
301
+ * Assert an element is fully inside the current viewport rectangle.
302
+ *
303
+ * Example #1: Then the element "#hero" should be displayed within a viewport
304
+ * Example #2: Then the element ".cta" should be displayed within a viewport
305
+ * Example #3: And the element "[data-testid=summary]" should be displayed within a viewport
306
+ * Example #4: Then the element "h1" should be displayed within a viewport
307
+ * Example #5: When I scroll to the element "#footer"
308
+ * Then the element "#footer" should be displayed within a viewport
309
+ *
310
+ */
311
+ Then('the element {string} should be displayed within a viewport', async function (sel) {
312
+ assert.ok(await inViewport(this.page, sel, 0), `Element "${sel}" is not within viewport.`);
313
+ });
314
+
315
+ /**
316
+ * Assert an element is fully inside the viewport, allowing a top offset.
317
+ * Useful for sticky headers — pass the header height as the offset.
318
+ *
319
+ * Example #1: Then the element "#hero" should be displayed within a viewport with a top offset of 60 pixels
320
+ * Example #2: Then the element ".main" should be displayed within a viewport with a top offset of 80 pixels
321
+ * Example #3: And the element ".cta" should be displayed within a viewport with a top offset of 50 pixels
322
+ * Example #4: Then the element "h1" should be displayed within a viewport with a top offset of 100 pixels
323
+ * Example #5: Then the element "#summary" should be displayed within a viewport with a top offset of 64 pixels
324
+ *
325
+ */
326
+ Then('the element {string} should be displayed within a viewport with a top offset of {int} pixels', async function (sel, off) {
327
+ assert.ok(await inViewport(this.page, sel, off), `Element "${sel}" is not within viewport (offset ${off}).`);
328
+ });
329
+
330
+ /**
331
+ * Assert an element is NOT inside the viewport (top-offset variant).
332
+ *
333
+ * Example #1: Then the element "#footer" should not be displayed within a viewport with a top offset of 60 pixels
334
+ * Example #2: Then the element ".off-screen" should not be displayed within a viewport with a top offset of 80 pixels
335
+ * Example #3: And the element ".pending" should not be displayed within a viewport with a top offset of 50 pixels
336
+ * Example #4: Then the element "#cookies-banner" should not be displayed within a viewport with a top offset of 100 pixels
337
+ * Example #5: Then the element ".low-priority" should not be displayed within a viewport with a top offset of 64 pixels
338
+ *
339
+ */
340
+ Then('the element {string} should not be displayed within a viewport with a top offset of {int} pixels', async function (sel, off) {
341
+ assert.ok(!(await inViewport(this.page, sel, off)), `Element "${sel}" should not be in viewport (offset ${off}).`);
342
+ });
343
+
344
+ /**
345
+ * Assert an element is NOT inside the viewport rectangle.
346
+ *
347
+ * Example #1: Then the element "#footer" should not be displayed within a viewport
348
+ * Example #2: Then the element ".off-screen" should not be displayed within a viewport
349
+ * Example #3: And the element ".pending" should not be displayed within a viewport
350
+ * Example #4: Then the element "#cookies-banner" should not be displayed within a viewport
351
+ * Example #5: Then the element ".low-priority" should not be displayed within a viewport
352
+ *
353
+ */
354
+ Then('the element {string} should not be displayed within a viewport', async function (sel) {
355
+ assert.ok(!(await inViewport(this.page, sel, 0)), `Element "${sel}" should not be in viewport.`);
356
+ });