@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,1003 @@
1
+ 'use strict';
2
+
3
+ const { friendly } = require('./varbase-e2e');
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Varbase E2E — Selectors step definitions
7
+ //
8
+ // Uses Playwright Locator API (locator.boundingBox, locator.isVisible,
9
+ // locator.isHidden, locator.click) to assert named page components are
10
+ // positioned relative to each other: above, below, left, right, inside,
11
+ // outside, over, not over. Also asserts visibility, focus, and click.
12
+ //
13
+ // Advanced selector system: named CSS and XPath selectors + layout components
14
+ // + viewport breakpoints — all configured under one `selectors` block, loaded
15
+ // from worldParameters, JSON files, or registered inline via steps.
16
+ //
17
+ // Configuration: resolved per-scenario with this priority:
18
+ // 1. process.env.VARBASE_E2E_SELECTORS_* (CI / shell — highest)
19
+ // 2. cucumber.js worldParameters.selectors.* (project-level defaults)
20
+ // 3. built-in defaults
21
+ //
22
+ // Supported env vars / worldParameters keys:
23
+ // VARBASE_E2E_SELECTORS_OFFSET offset scroll offset in px, default 60
24
+ // VARBASE_E2E_SELECTORS_BREAKPOINTS (JSON string) named breakpoints
25
+ //
26
+ // Unified config block (worldParameters.selectors):
27
+ // selectors: {
28
+ // filesPath: './tests/selectors/', // base path for selector files
29
+ // files: ['front-end-selectors.json', 'back-end-selectors.json'],
30
+ // css: {
31
+ // 'page header': 'header.page-header',
32
+ // 'main nav': 'nav[role="navigation"]',
33
+ // },
34
+ // xpath: {
35
+ // 'page title': '//h1[contains(@class,"page-title")]',
36
+ // },
37
+ // offset: 60, // scroll offset (px)
38
+ // breakpoints: {
39
+ // xs: { width: 375, height: 667 }, // phone portrait
40
+ // sm: { width: 576, height: 800 }, // large phone / phablet
41
+ // md: { width: 768, height: 1024 }, // tablet portrait
42
+ // lg: { width: 992, height: 768 }, // small laptop / tablet landscape
43
+ // xl: { width: 1200, height: 900, default: true }, // desktop
44
+ // xxl: { width: 1400, height: 900 }, // wide desktop / HD
45
+ // xxxl: { width: 1920, height: 1080 }, // Full HD / large monitor
46
+ // },
47
+ // }
48
+ //
49
+ // Selector files (JSON only) have the structure:
50
+ // {
51
+ // "css": { "breadcrumb": ".breadcrumb", "logo": "header .logo" },
52
+ // "xpath": { "page title": "//h1[contains(@class,'title')]" }
53
+ // }
54
+ //
55
+ // One unified CSS registry: layout components and element locators share
56
+ // worldParameters.selectors.css. Register in bulk via "Given I define css selectors:"
57
+ // or one-at-a-time via "When I add ... css selector". Same for XPath via `xpath`.
58
+ // ---------------------------------------------------------------------------
59
+
60
+ const { Given, When, Then, Before } = require('@cucumber/cucumber');
61
+ const fs = require('fs');
62
+ const path = require('path');
63
+
64
+ function parseFile(filePath) {
65
+ const ext = path.extname(filePath).toLowerCase();
66
+ if (ext !== '.json') {
67
+ throw friendly(`Selector files must be JSON (got "${ext}" for "${filePath}").`);
68
+ }
69
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Config
74
+ // ---------------------------------------------------------------------------
75
+ function pick(envVal, paramVal, fallback) {
76
+ if (envVal !== undefined && envVal !== '') return envVal;
77
+ if (paramVal !== undefined && paramVal !== '' && paramVal !== null) return paramVal;
78
+ return fallback;
79
+ }
80
+
81
+ function resolveConfig(parameters) {
82
+ const s = (parameters && parameters.selectors) || {};
83
+
84
+ let breakpoints = s.breakpoints || {
85
+ xs: { width: 375, height: 667 },
86
+ sm: { width: 576, height: 800 },
87
+ md: { width: 768, height: 1024 },
88
+ lg: { width: 992, height: 768 },
89
+ xl: { width: 1200, height: 900, default: true },
90
+ xxl: { width: 1400, height: 900 },
91
+ xxxl: { width: 1920, height: 1080 },
92
+ };
93
+
94
+ if (process.env.VARBASE_E2E_SELECTORS_BREAKPOINTS) {
95
+ try { breakpoints = JSON.parse(process.env.VARBASE_E2E_SELECTORS_BREAKPOINTS); } catch { /* ignore */ }
96
+ }
97
+
98
+ return {
99
+ offset: parseInt(pick(process.env.VARBASE_E2E_SELECTORS_OFFSET, s.offset, '60'), 10),
100
+ breakpoints,
101
+ filesPath: s.filesPath || '',
102
+ files: Array.isArray(s.files) ? s.files : [],
103
+ css: Object.assign({}, s.css || {}),
104
+ xpath: Object.assign({}, s.xpath || {}),
105
+ };
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Per-scenario registries
110
+ // ---------------------------------------------------------------------------
111
+ Before(function () {
112
+ const cfg = resolveConfig(this.parameters);
113
+ this.__selectorsConfig = cfg;
114
+ this.__selectorsCss = Object.assign({}, cfg.css);
115
+ this.__selectorsXpath = Object.assign({}, cfg.xpath);
116
+
117
+ // Auto-load selector files specified in worldParameters.
118
+ for (const file of cfg.files) {
119
+ const filePath = cfg.filesPath ? path.join(cfg.filesPath, file) : file;
120
+ try {
121
+ const parsed = parseFile(filePath);
122
+ if (parsed && parsed.css) Object.assign(this.__selectorsCss, parsed.css);
123
+ if (parsed && parsed.xpath) Object.assign(this.__selectorsXpath, parsed.xpath);
124
+ } catch (e) {
125
+ throw friendly(`Selectors: failed to load selector file "${filePath}": ${e.message}`);
126
+ }
127
+ }
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Selector resolution helpers
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * Resolve a named selector to a Playwright-compatible selector string.
136
+ * Priority: css registry → xpath registry.
137
+ */
138
+ function resolveSelector(world, name) {
139
+ const trimmed = name.trim();
140
+
141
+ if (world.__selectorsCss && world.__selectorsCss[trimmed]) {
142
+ return world.__selectorsCss[trimmed];
143
+ }
144
+
145
+ if (world.__selectorsXpath && world.__selectorsXpath[trimmed]) {
146
+ const xp = world.__selectorsXpath[trimmed];
147
+ return xp.startsWith('xpath=') ? xp : `xpath=${xp}`;
148
+ }
149
+
150
+ // Suggest the closest registered name when one exists. Cheap Levenshtein
151
+ // by character — good enough for short selector names.
152
+ const known = [
153
+ ...Object.keys(world.__selectorsCss || {}),
154
+ ...Object.keys(world.__selectorsXpath || {}),
155
+ ];
156
+ let best = null; let bestDist = Infinity;
157
+ for (const k of known) {
158
+ const dist = (function lev(a, b) {
159
+ const m = a.length, n = b.length;
160
+ if (!m) return n; if (!n) return m;
161
+ const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
162
+ for (let i = 0; i <= m; i++) d[i][0] = i;
163
+ for (let j = 0; j <= n; j++) d[0][j] = j;
164
+ for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) {
165
+ d[i][j] = a[i - 1] === b[j - 1] ? d[i - 1][j - 1] : 1 + Math.min(d[i - 1][j], d[i][j - 1], d[i - 1][j - 1]);
166
+ }
167
+ return d[m][n];
168
+ })(trimmed.toLowerCase(), k.toLowerCase());
169
+ if (dist < bestDist) { bestDist = dist; best = k; }
170
+ }
171
+ const suggestion = best && bestDist <= Math.max(2, Math.floor(trimmed.length / 3))
172
+ ? `\n Did you mean "${best}"?`
173
+ : '';
174
+ const examples = known.length
175
+ ? `\n Registered names: ${known.slice(0, 8).join(', ')}${known.length > 8 ? ', ...' : ''}`
176
+ : '\n No selectors registered yet.';
177
+
178
+ throw friendly(
179
+ `Unknown selector "${trimmed}".${suggestion}${examples}\n` +
180
+ ` Register one with:\n` +
181
+ ` Given I define css selectors:\n` +
182
+ ` | name | css |\n` +
183
+ ` When I add "${trimmed}" selector for "<css>" css selector\n` +
184
+ ` or load a JSON preset via worldParameters.selectors.files.`
185
+ );
186
+ }
187
+
188
+ function parseNames(text) {
189
+ return text.split(/\s*(?:,|and)\s*/).map(s => s.trim()).filter(Boolean);
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Geometry helpers using Playwright locator.boundingBox()
194
+ // ---------------------------------------------------------------------------
195
+ async function getGeometry(page, selector) {
196
+ const loc = page.locator(selector).first();
197
+ try { await loc.scrollIntoViewIfNeeded({ timeout: 3000 }); } catch { /* may already be visible */ }
198
+
199
+ const box = await loc.boundingBox();
200
+ if (!box) return null;
201
+
202
+ const scroll = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
203
+
204
+ // z-index still requires evaluate — no native Playwright API for computed style.
205
+ const zIndex = await loc.evaluate(el => {
206
+ let z = 0;
207
+ let node = el;
208
+ while (node && node !== document.body) {
209
+ const cs = window.getComputedStyle(node);
210
+ if (cs.position !== 'static' && cs.zIndex !== 'auto') {
211
+ const n = parseInt(cs.zIndex, 10);
212
+ if (!isNaN(n)) z = n;
213
+ }
214
+ node = node.parentElement;
215
+ }
216
+ return z;
217
+ });
218
+
219
+ return { top: box.y + scroll.y, left: box.x + scroll.x, width: box.width, height: box.height, zIndex };
220
+ }
221
+
222
+ async function assertPosition(page, position, c1Name, c1Sel, c2Name, c2Sel) {
223
+ const g1 = await getGeometry(page, c1Sel);
224
+ if (!g1) throw friendly(`Cannot get bounding box for "${c1Name}" (${c1Sel}) — element not visible`);
225
+ const g2 = await getGeometry(page, c2Sel);
226
+ if (!g2) throw friendly(`Cannot get bounding box for "${c2Name}" (${c2Sel}) — element not visible`);
227
+
228
+ let pass = false;
229
+ switch (position) {
230
+ case 'above': pass = g2.top >= g1.top + g1.height; break;
231
+ case 'below': pass = g1.top >= g2.top + g2.height; break;
232
+ case 'left': pass = g1.left + g1.width <= g2.left; break;
233
+ case 'right': pass = g1.left >= g2.left + g2.width; break;
234
+ case 'inside':
235
+ pass = g1.top >= g2.top && g1.top + g1.height <= g2.top + g2.height &&
236
+ g1.left >= g2.left && g1.left + g1.width <= g2.left + g2.width;
237
+ break;
238
+ case 'outside':
239
+ pass = g1.top <= g2.top && g1.top + g1.height >= g2.top + g2.height &&
240
+ g1.left <= g2.left && g1.left + g1.width >= g2.left + g2.width;
241
+ break;
242
+ case 'over': {
243
+ const intersects = !(g1.left >= g2.left + g2.width || g1.left + g1.width <= g2.left ||
244
+ g1.top >= g2.top + g2.height || g1.top + g1.height <= g2.top);
245
+ pass = intersects && g1.zIndex <= g2.zIndex;
246
+ break;
247
+ }
248
+ }
249
+ return pass;
250
+ }
251
+
252
+ async function dispatcher(world, position, subjectText, othersText, negate = false) {
253
+ const subjects = parseNames(subjectText);
254
+ const others = parseNames(othersText);
255
+ const errors = [];
256
+ for (const subjectName of subjects) {
257
+ const subjectSel = resolveSelector(world, subjectName);
258
+ for (const otherName of others) {
259
+ const otherSel = resolveSelector(world, otherName);
260
+ try {
261
+ const pass = await assertPosition(world.page, position, subjectName, subjectSel, otherName, otherSel);
262
+ if (!pass && !negate) errors.push(`"${subjectName}" is not ${position} "${otherName}"`);
263
+ else if (pass && negate) errors.push(`"${subjectName}" is ${position} "${otherName}" (expected not to be)`);
264
+ } catch (e) { errors.push(e.message); }
265
+ }
266
+ }
267
+ if (errors.length > 0) throw friendly(errors.join('\n'));
268
+ }
269
+
270
+ // ===========================================================================
271
+ // STEP DEFINITIONS — Advanced selector system
272
+ // ===========================================================================
273
+
274
+ /**
275
+ * Register a named CSS selector at runtime.
276
+ *
277
+ * Example #1: When I add "mobile logo" selector for "header img#logo" css selector
278
+ * Example #2: When I add "breadcrumb" selector for ".breadcrumb" css selector
279
+ * Example #3: When I add "breadcrumb first link" selector for ".breadcrumb li:nth-child(1) a" css selector
280
+ * Example #4: When I add "cta button" selector for ".cta .btn-primary" css selector
281
+ * Example #5: When I add "page header" selector for "header.page-header" css selector
282
+ * Example #6: When I add "main nav" selector for "nav[role='navigation']" css selector
283
+ * Example #7: When I add "hero image" selector for ".hero img" css selector
284
+ * Example #8: When I add "footer links" selector for "footer a" css selector
285
+ * Example #9: When I add "search field" selector for "input[type='search']" css selector
286
+ * Example #10: When I add "submit button" selector for "button[type='submit']" css selector
287
+ *
288
+ * Advanced:
289
+ * Example #11: Later override same name → latest wins:
290
+ * When I add "target" selector for "h1" css selector
291
+ * And I add "target" selector for "ul" css selector
292
+ * Example #12: Component (CSS) takes precedence over CSS registry with same name:
293
+ * When I add "shared" selector for "ul" css selector
294
+ * And I define css selectors:
295
+ * | shared | h1 |
296
+ * Example #13: Combine with XPath registration and use together:
297
+ * When I add "cta" selector for ".cta" css selector
298
+ * And I add "cta link" selector for "//a[contains(@class,'cta')]" xpath selector
299
+ */
300
+ When(/^(I |we )*add "([^"]*)" selector for "([^"]*)" css selector$/, function (pronounCase, selectorName, cssSelector) {
301
+ if (!selectorName || !cssSelector) throw friendly('Selectors: selector name and CSS selector must not be empty.');
302
+ this.__selectorsCss[selectorName.trim()] = cssSelector.trim();
303
+ });
304
+
305
+ /**
306
+ * Register a named XPath selector at runtime.
307
+ *
308
+ * Example #1: When I add "page title" selector for "//h1[contains(@class,'page-header')]" xpath selector
309
+ * Example #2: When I add "Dashboard" selector for "//*[@id='navbar-link-admin-dashboard']" xpath selector
310
+ * Example #3: When I add "Vertical orientation" selector for "//*[@id='navbar-item--2-tray']/div/div[2]/div/button" xpath selector
311
+ * Example #4: When I add "admin menu" selector for "//nav[contains(@class,'admin-menu')]" xpath selector
312
+ * Example #5: When I add "first table row" selector for "//table/tbody/tr[1]" xpath selector
313
+ * Example #6: When I add "active tab" selector for "//li[contains(@class,'active')]/a" xpath selector
314
+ * Example #7: When I add "error message" selector for "//*[contains(@class,'error')]" xpath selector
315
+ * Example #8: When I add "site name" selector for "//a[contains(@class,'site-name')]" xpath selector
316
+ * Example #9: When I add "user menu" selector for "//*[@id='user-menu']" xpath selector
317
+ * Example #10: When I add "language switcher" selector for "//ul[contains(@class,'language-switcher')]" xpath selector
318
+ *
319
+ * Advanced:
320
+ * Example #11: Axis predicates — first match only (locator.first() is applied):
321
+ * When I add "first row" selector for "//table//tr[1]" xpath selector
322
+ * Example #12: Text contains — name by rendered label:
323
+ * When I add "login link" selector for "//a[contains(normalize-space(.),'Login')]" xpath selector
324
+ * Example #13: Attribute + position — last nav item:
325
+ * When I add "last nav item" selector for "//nav[@role='navigation']//a[last()]" xpath selector
326
+ */
327
+ When(/^(I |we )*add "([^"]*)" selector for "([^"]*)" xpath selector$/, function (pronounCase, selectorName, xpathSelector) {
328
+ if (!selectorName || !xpathSelector) throw friendly('Selectors: selector name and XPath selector must not be empty.');
329
+ this.__selectorsXpath[selectorName.trim()] = xpathSelector.trim();
330
+ });
331
+
332
+ /**
333
+ * Load CSS and XPath selectors from a JSON file.
334
+ * File path is relative to worldParameters.selectors.filesPath.
335
+ *
336
+ * Example #1: When I add selectors from "selectors.json" file
337
+ * Example #2: When I add selectors from "admin-selectors.json" file
338
+ * Example #3: When I add selectors from "frontend.json" file
339
+ * Example #4: When I add selectors from "theme-selectors.json" file
340
+ * Example #5: When I add selectors from "components.json" file
341
+ * Example #6: When we add selectors from "shared.json" file
342
+ * Example #7: When add selectors from "backend.json" file
343
+ * Example #8: When I add selectors from "page-selectors.json" file
344
+ * Example #9: When I add selectors from "module-selectors.json" file
345
+ * Example #10: When we add selectors from "mobile.json" file
346
+ *
347
+ * Advanced:
348
+ * Example #11: Load a file then override a single entry inline:
349
+ * When I add selectors from "homepage-selectors.json" file
350
+ * And I add "homepage heading" selector for "header h1" css selector
351
+ * Example #12: Load multiple files — later files override earlier keys:
352
+ * When I add selectors from "front-end-selectors.json" file
353
+ * And I add selectors from "back-end-selectors.json" file
354
+ * Example #13: File + inline xpath + component — all three registries merged:
355
+ * When I add selectors from "homepage-selectors.json" file
356
+ * And I add "extra link" selector for "//a[@rel='canonical']" xpath selector
357
+ * And I define css selectors:
358
+ * | wrapper | #main |
359
+ */
360
+ When(/^(I |we )*add selectors from "([^"]*)" file$/, function (pronounCase, fileName) {
361
+ const cfg = this.__selectorsConfig;
362
+ if (!fileName) throw friendly('Selectors: file name must not be empty.');
363
+ if (!cfg.filesPath) throw friendly('Selectors: worldParameters.selectors.filesPath must be set to load selector files.');
364
+ const filePath = path.join(cfg.filesPath, fileName);
365
+ let parsed;
366
+ try { parsed = parseFile(filePath); } catch (e) {
367
+ throw friendly(`Selectors: failed to load selector file "${filePath}": ${e.message}`);
368
+ }
369
+ if (parsed && parsed.css) Object.assign(this.__selectorsCss, parsed.css);
370
+ if (parsed && parsed.xpath) Object.assign(this.__selectorsXpath, parsed.xpath);
371
+ });
372
+
373
+ /**
374
+ * Print all registered CSS selectors to console (debug/inspection).
375
+ *
376
+ * Example #1: Then I print css selectors
377
+ * Example #2: When I print css selectors
378
+ * Example #3: Then we print css selectors
379
+ * Example #4: When we print css selectors
380
+ * Example #5: Then print css selectors
381
+ * Example #6: When print css selectors
382
+ * Example #7: And I print css selectors
383
+ * Example #8: And we print css selectors
384
+ * Example #9: Given I print css selectors
385
+ * Example #10: But I print css selectors
386
+ */
387
+ Then(/^(I |we )*print css selectors$/, function (_pronounCase) {
388
+ console.log('\n--- CSS Selectors ---');
389
+ for (const [name, sel] of Object.entries(this.__selectorsCss || {})) {
390
+ console.log(` "${name}": ${sel}`);
391
+ }
392
+ });
393
+
394
+ /**
395
+ * Print all registered XPath selectors to console (debug/inspection).
396
+ *
397
+ * Example #1: Then I print xpath selectors
398
+ * Example #2: When I print xpath selectors
399
+ * Example #3: Then we print xpath selectors
400
+ * Example #4: When we print xpath selectors
401
+ * Example #5: Then print xpath selectors
402
+ * Example #6: When print xpath selectors
403
+ * Example #7: And I print xpath selectors
404
+ * Example #8: And we print xpath selectors
405
+ * Example #9: Given I print xpath selectors
406
+ * Example #10: But I print xpath selectors
407
+ */
408
+ Then(/^(I |we )*print xpath selectors$/, function (_pronounCase) {
409
+ console.log('\n--- XPath Selectors ---');
410
+ for (const [name, sel] of Object.entries(this.__selectorsXpath || {})) {
411
+ console.log(` "${name}": ${sel}`);
412
+ }
413
+ });
414
+
415
+ // ===========================================================================
416
+ // STEP DEFINITIONS — Layout components
417
+ // ===========================================================================
418
+
419
+ /**
420
+ * Define named CSS selectors in bulk for the current scenario.
421
+ *
422
+ * Example #1:
423
+ * Given I define css selectors:
424
+ * | header | #header |
425
+ * | nav | nav.primary |
426
+ * | footer | footer |
427
+ *
428
+ * Example #2:
429
+ * Given I define css selectors:
430
+ * | logo | .site-logo |
431
+ * | search | #search-input |
432
+ *
433
+ * Example #3:
434
+ * Given I define css selectors:
435
+ * | hero | .hero-banner |
436
+ * | content | .main-content |
437
+ * | sidebar | .sidebar |
438
+ *
439
+ * Example #4:
440
+ * Given I define css selectors:
441
+ * | menu | ul.menu |
442
+ * | button | .cta-button |
443
+ *
444
+ * Example #5:
445
+ * Given I define css selectors:
446
+ * | top bar | .top-bar |
447
+ * | page | #page-wrapper |
448
+ * | footer | .site-footer |
449
+ *
450
+ * Example #6:
451
+ * Given we define css selectors:
452
+ * | modal | .modal-dialog |
453
+ * | overlay | .modal-backdrop |
454
+ *
455
+ * Example #7:
456
+ * When I define css selectors:
457
+ * | input | input[name=q] |
458
+ * | button | button[type=submit] |
459
+ *
460
+ * Example #8:
461
+ * And I define css selectors:
462
+ * | card | .card |
463
+ * | title | .card-title |
464
+ *
465
+ * Example #9:
466
+ * Given define css selectors:
467
+ * | heading | h1 |
468
+ * | list | ul |
469
+ *
470
+ * Example #10:
471
+ * Given I define css selectors:
472
+ * | left | .col-left |
473
+ * | center | .col-center |
474
+ * | right | .col-right |
475
+ *
476
+ * Advanced:
477
+ * Example #11: Later entry with same name overrides earlier:
478
+ * When I add "shared" selector for "ul" css selector
479
+ * Given I define css selectors:
480
+ * | shared | h1 |
481
+ * Then I see visible shared # resolves to h1 (latest wins).
482
+ *
483
+ * Example #12: Define + use in position + visibility asserts:
484
+ * Given I define css selectors:
485
+ * | heading | h1 |
486
+ * | list | ul |
487
+ * Then I see visible heading, list
488
+ * And I see heading above list
489
+ *
490
+ * Example #13: Layered page-shell selectors:
491
+ * Given I define css selectors:
492
+ * | topbar | .topbar |
493
+ * | header | header.site |
494
+ * | nav | nav[role=navigation] |
495
+ * | main | main |
496
+ * | footer | footer |
497
+ */
498
+ Given(/^(I |we )*define css selectors:$/, function (pronounCase, dataTable) {
499
+ for (const [name, selector] of dataTable.raw()) {
500
+ if (name && selector) this.__selectorsCss[name.trim()] = selector.trim();
501
+ }
502
+ });
503
+
504
+ /**
505
+ * Define named XPath selectors in bulk for the current scenario.
506
+ *
507
+ * Example #1:
508
+ * Given I define xpath selectors:
509
+ * | first heading | //h1[1] |
510
+ * | first subline | //h3[1] |
511
+ * Example #2:
512
+ * Given we define xpath selectors:
513
+ * | active tab | //li[contains(@class,'active')]/a |
514
+ * Example #3:
515
+ * Given define xpath selectors:
516
+ * | last nav item | //nav//a[last()] |
517
+ * Example #4:
518
+ * And I define xpath selectors:
519
+ * | login link | //a[contains(normalize-space(.),'Login')] |
520
+ * Example #5:
521
+ * When I define xpath selectors:
522
+ * | error message | //*[contains(@class,'error')] |
523
+ * Example #6:
524
+ * Given I define xpath selectors:
525
+ * | first table row | //table/tbody/tr[1] |
526
+ * | first table cell| //table/tbody/tr[1]/td[1] |
527
+ * Example #7:
528
+ * Given I define xpath selectors:
529
+ * | page title | //h1[contains(@class,'page-header')] |
530
+ * Example #8:
531
+ * Given I define xpath selectors:
532
+ * | admin menu | //nav[contains(@class,'admin-menu')] |
533
+ * Example #9:
534
+ * Given I define xpath selectors:
535
+ * | language switcher | //ul[contains(@class,'language-switcher')] |
536
+ * Example #10:
537
+ * Given I define xpath selectors:
538
+ * | user menu | //*[@id='user-menu'] |
539
+ */
540
+ Given(/^(I |we )*define xpath selectors:$/, function (pronounCase, dataTable) {
541
+ for (const [name, selector] of dataTable.raw()) {
542
+ if (name && selector) this.__selectorsXpath[name.trim()] = selector.trim();
543
+ }
544
+ });
545
+
546
+ /**
547
+ * Resize viewport to a named breakpoint.
548
+ * Built-in names: xs, sm, md, lg, xl, xxl, xxxl.
549
+ *
550
+ * xs 375 x 667 phone portrait
551
+ * sm 576 x 800 large phone / phablet
552
+ * md 768 x 1024 tablet portrait
553
+ * lg 992 x 768 small laptop / tablet landscape
554
+ * xl 1200 x 900 desktop (default)
555
+ * xxl 1400 x 900 wide desktop / HD
556
+ * xxxl 1920 x 1080 Full HD / large monitor
557
+ *
558
+ * Example #1: Given I am viewing the site on a xl screen
559
+ * Example #2: Given I am viewing the site on a xs screen
560
+ * Example #3: Given I am viewing the site on a md screen
561
+ * Example #4: Given I am viewing the site on a "xl" screen
562
+ * Example #5: Given I am viewing the site on a lg device
563
+ * Example #6: Given I am viewing the site on a "sm" device
564
+ * Example #7: Given I am viewing the site on a xxl screen
565
+ * Example #8: Given I am viewing the site on a "xxxl" screen
566
+ * Example #9: Given I am viewing the site on a xxxl device
567
+ * Example #10: Given I am viewing the site on a "xxl" device
568
+ * Example #11: Given we are viewing the site on a xs screen
569
+ * Example #12: Given viewing the site on a lg screen
570
+ */
571
+ Given(/^(I am |we are )?viewing the site on a (?:"([^"]+)"|([a-zA-Z0-9 _,]+)) (?:screen|device)$/, async function (pronounCase, quoted, bare) {
572
+ const name = (quoted || bare || '').trim();
573
+ const cfg = this.__selectorsConfig;
574
+ if (!cfg.breakpoints[name]) throw friendly(`Selectors: breakpoint "${name}" not defined in worldParameters.selectors.breakpoints.`);
575
+ const { width, height } = cfg.breakpoints[name];
576
+ await this.page.setViewportSize({ width, height });
577
+ });
578
+
579
+ // ===========================================================================
580
+ // STEP DEFINITIONS — Relative position assertions
581
+ // ===========================================================================
582
+
583
+ /**
584
+ * Assert a subject locator is above one or more others (absolute page coordinates).
585
+ *
586
+ * Example #1: Then I see header above footer
587
+ * Example #2: Then I see logo above search
588
+ * Example #3: Then I see nav above content, footer
589
+ * Example #4: Then I see hero above sidebar and footer
590
+ * Example #5: Then I see top bar above nav, content and footer
591
+ * Example #6: Then I see heading above subline
592
+ * Example #7: Then I see nav above content
593
+ * Example #8: Then I see search above results
594
+ * Example #9: Then I see banner above body
595
+ * Example #10: Then I see menu above page
596
+ *
597
+ * Advanced — multi-subject / multi-target dispatch:
598
+ * Example #11: Single subject, many targets (comma + "and"):
599
+ * Then I see heading above subline, list and footer
600
+ * Example #12: Many subjects, single target:
601
+ * Then I see heading, subline above list
602
+ * Example #13: Cartesian — many subjects × many targets:
603
+ * Then I see heading, subline above list, footer
604
+ * Example #14: Combined with inline xpath registration:
605
+ * When I add "first heading xp" selector for "//h1[1]" xpath selector
606
+ * Then I see first heading xp above list
607
+ */
608
+ Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) above ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
609
+ await dispatcher(this, 'above', subject, others);
610
+ });
611
+
612
+ /**
613
+ * Assert a subject locator is below one or more others.
614
+ *
615
+ * Example #1: Then I see footer below header
616
+ * Example #2: Then I see content below nav
617
+ * Example #3: Then I see footer below header, nav and content
618
+ * Example #4: Then I see results below search
619
+ * Example #5: Then I see sidebar below hero
620
+ * Example #6: Then I see list below heading
621
+ * Example #7: Then I see nav below top bar
622
+ * Example #8: Then I see content below logo
623
+ * Example #9: Then I see page below menu
624
+ * Example #10: Then I see body below banner
625
+ */
626
+ Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) below ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
627
+ await dispatcher(this, 'below', subject, others);
628
+ });
629
+
630
+ /**
631
+ * Assert a subject locator is to the left of one or more others.
632
+ *
633
+ * Example #1: Then I see logo to the left of nav
634
+ * Example #2: Then I see sidebar to left of content
635
+ * Example #3: Then I see search to the left of search button
636
+ * Example #4: Then I see menu to the left of cta
637
+ * Example #5: Then I see label to left of input
638
+ * Example #6: Then I see icon to the left of title
639
+ * Example #7: Then I see thumbnail to the left of description
640
+ * Example #8: Then I see avatar to left of username
641
+ * Example #9: Then I see back button to the left of forward button
642
+ * Example #10: Then I see flag to left of country name
643
+ */
644
+ Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) to (?:the )?left of ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
645
+ await dispatcher(this, 'left', subject, others);
646
+ });
647
+
648
+ /**
649
+ * Assert a subject locator is to the right of one or more others.
650
+ *
651
+ * Example #1: Then I see nav to the right of logo
652
+ * Example #2: Then I see content to right of sidebar
653
+ * Example #3: Then I see search button to the right of search
654
+ * Example #4: Then I see cta to the right of menu
655
+ * Example #5: Then I see input to right of label
656
+ * Example #6: Then I see title to the right of icon
657
+ * Example #7: Then I see description to the right of thumbnail
658
+ * Example #8: Then I see username to right of avatar
659
+ * Example #9: Then I see forward button to the right of back button
660
+ * Example #10: Then I see country name to right of flag
661
+ */
662
+ Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) to (?:the )?right of ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
663
+ await dispatcher(this, 'right', subject, others);
664
+ });
665
+
666
+ /**
667
+ * Assert a subject locator's bounding box is fully inside another's.
668
+ *
669
+ * Example #1: Then I see logo inside of header
670
+ * Example #2: Then I see nav inside of header
671
+ * Example #3: Then I see search inside of header
672
+ * Example #4: Then I see cta inside of hero
673
+ * Example #5: Then I see label inside of form
674
+ * Example #6: Then I see icon inside of button
675
+ * Example #7: Then I see title inside of banner
676
+ * Example #8: Then I see content inside of page
677
+ * Example #9: Then I see sidebar inside of page
678
+ * Example #10: Then I see footer inside of page
679
+ */
680
+ Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) inside of ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
681
+ await dispatcher(this, 'inside', subject, others);
682
+ });
683
+
684
+ /**
685
+ * Assert a subject locator's bounding box fully contains another's.
686
+ *
687
+ * Example #1: Then I see header outside of logo
688
+ * Example #2: Then I see header outside of nav
689
+ * Example #3: Then I see hero outside of cta
690
+ * Example #4: Then I see form outside of label
691
+ * Example #5: Then I see button outside of icon
692
+ * Example #6: Then I see banner outside of title
693
+ * Example #7: Then I see page outside of content
694
+ * Example #8: Then I see page outside of sidebar
695
+ * Example #9: Then I see page outside of footer
696
+ * Example #10: Then I see wrapper outside of inner
697
+ */
698
+ Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) outside of ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
699
+ await dispatcher(this, 'outside', subject, others);
700
+ });
701
+
702
+ /**
703
+ * Assert a subject locator overlaps another via intersection and z-index.
704
+ *
705
+ * Example #1: Then I see modal over content
706
+ * Example #2: Then I see dropdown over nav
707
+ * Example #3: Then I see tooltip over label
708
+ * Example #4: Then I see overlay over page
709
+ * Example #5: Then I see popup over hero
710
+ * Example #6: Then I see sticky header over content
711
+ * Example #7: Then I see cookie banner over footer
712
+ * Example #8: Then I see dialog over sidebar
713
+ * Example #9: Then I see notification over header
714
+ * Example #10: Then I see lightbox over page
715
+ */
716
+ Then(/^(I |we )*see ((?:[a-zA-Z0-9 ,\-](?!not))+) over ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
717
+ await dispatcher(this, 'over', subject, others);
718
+ });
719
+
720
+ /**
721
+ * Assert a subject locator does NOT overlap another element.
722
+ *
723
+ * Example #1: Then I see header not over content
724
+ * Example #2: Then I see nav not over sidebar
725
+ * Example #3: Then I see footer not over content
726
+ * Example #4: Then I see logo not over search
727
+ * Example #5: Then I see sidebar not over footer
728
+ * Example #6: Then I see menu not over hero
729
+ * Example #7: Then I see label not over input
730
+ * Example #8: Then I see icon not over title
731
+ * Example #9: Then I see button not over form
732
+ * Example #10: Then I see nav not over page
733
+ */
734
+ Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) not over ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
735
+ await dispatcher(this, 'over', subject, others, true);
736
+ });
737
+
738
+ // ===========================================================================
739
+ // STEP DEFINITIONS — Visibility and focus (uses locator.isVisible / isHidden)
740
+ // ===========================================================================
741
+
742
+ /**
743
+ * Assert one or more named locators are visible.
744
+ *
745
+ * Example #1: Then I see visible header
746
+ * Example #2: Then I see visible nav, footer
747
+ * Example #3: Then I see visible logo and search
748
+ * Example #4: Then I see visible cta
749
+ * Example #5: Then I see visible hero, content and sidebar
750
+ * Example #6: Then I see visible menu
751
+ * Example #7: Then I see visible search button
752
+ * Example #8: Then I see visible banner
753
+ * Example #9: Then I see visible footer
754
+ * Example #10: Then I see visible top bar, nav and content
755
+ *
756
+ * Advanced:
757
+ * Example #11: Mix of components, inline CSS, inline XPath — all resolve:
758
+ * When I add "page heading" selector for "h1" css selector
759
+ * And I add "first para" selector for "//p[1]" xpath selector
760
+ * Then I see visible page heading, first para
761
+ * Example #12: After breakpoint resize — assert still visible:
762
+ * Given I am viewing the site on a xs screen
763
+ * Then I see visible heading, list
764
+ * Example #13: After loading a selector file — names from file are visible:
765
+ * When I add selectors from "homepage-selectors.json" file
766
+ * Then I see visible homepage heading, homepage first heading
767
+ */
768
+ Then(/^(I |we )*see visible ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subjectsText) {
769
+ const names = parseNames(subjectsText);
770
+ const errors = [];
771
+ for (const name of names) {
772
+ const sel = resolveSelector(this, name);
773
+ // Playwright native auto-retrying assertion — waits until the locator becomes visible.
774
+ try { await this.page.locator(sel).first().waitFor({ state: 'visible', timeout: 5000 }); }
775
+ catch { errors.push(`"${name}" (${sel}) is not visible`); }
776
+ }
777
+ if (errors.length > 0) throw friendly(errors.join('\n'));
778
+ });
779
+
780
+ /**
781
+ * Assert one or more named locators are hidden.
782
+ *
783
+ * Example #1: Then I don't see modal
784
+ * Example #2: Then I don't see dropdown, tooltip
785
+ * Example #3: Then I don't see overlay
786
+ * Example #4: Then I don't see cookie banner
787
+ * Example #5: Then I don't see popup
788
+ * Example #6: Then I don't see notification
789
+ * Example #7: Then I don't see lightbox
790
+ * Example #8: Then I don't see sticky header
791
+ * Example #9: Then I don't see dialog
792
+ * Example #10: Then I don't see sidebar, overlay and popup
793
+ */
794
+ Then(/^(I |we )*(don't|do not) see ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, negation, subjectsText) {
795
+ const names = parseNames(subjectsText);
796
+ const errors = [];
797
+ for (const name of names) {
798
+ const sel = resolveSelector(this, name);
799
+ // Playwright native auto-retrying assertion — waits until the locator is detached or hidden.
800
+ try { await this.page.locator(sel).first().waitFor({ state: 'hidden', timeout: 5000 }); }
801
+ catch { errors.push(`"${name}" (${sel}) is visible (expected hidden)`); }
802
+ }
803
+ if (errors.length > 0) throw friendly(errors.join('\n'));
804
+ });
805
+
806
+ /**
807
+ * Assert a named locator has keyboard focus.
808
+ *
809
+ * Example #1: Then I see search has focus
810
+ * Example #2: Then I see email input has focus
811
+ * Example #3: Then we see submit button has focus
812
+ * Example #4: Then I see username has focus
813
+ * Example #5: Then I see password has focus
814
+ * Example #6: Then we see search button has focus
815
+ * Example #7: Then I see first link has focus
816
+ * Example #8: Then I see skip link has focus
817
+ * Example #9: Then I see close button has focus
818
+ * Example #10: Then I see modal heading has focus
819
+ */
820
+ Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) has focus$/, async function (pronounCase, subjectText) {
821
+ const name = subjectText.trim();
822
+ const sel = resolveSelector(this, name);
823
+ const loc = this.page.locator(sel).first();
824
+ // Playwright native auto-retrying focus check via waitForFunction.
825
+ try {
826
+ await loc.waitFor({ state: 'attached', timeout: 5000 });
827
+ const handle = await loc.elementHandle();
828
+ await this.page.waitForFunction((el) => el === document.activeElement, handle, { timeout: 5000 });
829
+ } catch {
830
+ throw friendly(`"${name}" (${sel}) does not have focus`);
831
+ }
832
+ });
833
+
834
+ // ===========================================================================
835
+ // STEP DEFINITIONS — Focus and text selection
836
+ // ===========================================================================
837
+
838
+ /**
839
+ * Resolve a form field by label → placeholder → role=textbox → [name]/#id.
840
+ * Uses Playwright's ready-made semantic locators (getByLabel, getByPlaceholder,
841
+ * getByRole) with .or() fallbacks — matches the accessibility-first lookup
842
+ * chain recommended by https://playwright.dev/docs/locators.
843
+ */
844
+ function resolveField(page, field) {
845
+ return page.getByLabel(field).or(
846
+ page.getByPlaceholder(field)
847
+ ).or(
848
+ page.getByRole('textbox', { name: field })
849
+ ).or(
850
+ page.locator(`[name="${field}"], #${field}`)
851
+ ).first();
852
+ }
853
+
854
+ /**
855
+ * Move keyboard focus to a field by label, name, or id.
856
+ *
857
+ * Example #1: When I move focus to "Title" field
858
+ * Example #2: When I move focus to "Body" field
859
+ * Example #3: When I move focus to "Email" field
860
+ * Example #4: When I move focus to "Search" field
861
+ * Example #5: When I move focus to "Username" field
862
+ * Example #6: When I move focus to "Password" field
863
+ * Example #7: When I move focus to "First name" field
864
+ * Example #8: When I move focus to "Last name" field
865
+ * Example #9: When I move focus to "Description" field
866
+ * Example #10: When I move focus to "Phone" field
867
+ */
868
+ When(/^(I |we )*move focus to "([^"]*)" field$/, async function (pronounCase, fieldLabel) {
869
+ const loc = resolveField(this.page, fieldLabel);
870
+ await loc.focus();
871
+ });
872
+
873
+ /**
874
+ * Select all text inside a field by label, name, or id.
875
+ *
876
+ * Example #1: When I select all text in "Title" field
877
+ * Example #2: When I select all text in "Description" field
878
+ * Example #3: When I select all text in "Body" field
879
+ * Example #4: When I select all text in "Email" field
880
+ * Example #5: When I select all text in "Username" field
881
+ * Example #6: When I select all text in "Search" field
882
+ * Example #7: When I select all text in "Name" field
883
+ * Example #8: When I select all text in "Notes" field
884
+ * Example #9: When I select all text in "Address" field
885
+ * Example #10: When I select all text in "Message" field
886
+ */
887
+ When(/^(I |we )*select all text in "([^"]*)" field$/, async function (pronounCase, fieldLabel) {
888
+ const loc = resolveField(this.page, fieldLabel);
889
+ await loc.focus();
890
+ await loc.selectText();
891
+ });
892
+
893
+ /**
894
+ * Select a character range (from..to) inside a field.
895
+ *
896
+ * Example #1: When I select from 0 to 5 text in "Title" field
897
+ * Example #2: When I select from 0 to 10 text in "Body" field
898
+ * Example #3: When I select from 3 to 8 text in "Description" field
899
+ * Example #4: When I select from 0 to 5 text in "Email" field
900
+ * Example #5: When I select from 2 to 6 text in "Username" field
901
+ * Example #6: When we select from 0 to 4 text in "Name" field
902
+ * Example #7: When select from 1 to 7 text in "Search" field
903
+ * Example #8: When I select from 0 to 20 text in "Notes" field
904
+ * Example #9: When I select from 5 to 15 text in "Address" field
905
+ * Example #10: When we select from 0 to 3 text in "Phone" field
906
+ *
907
+ * Advanced:
908
+ * Example #11: Select first word after fill:
909
+ * And I fill in "Title" with "Release 2.0 notes"
910
+ * When I select from 0 to 7 text in "Title" field
911
+ * Example #12: Select middle substring by index range:
912
+ * And I fill in "Body" with "abcdefghij"
913
+ * When I select from 3 to 6 text in "Body" field
914
+ * Example #13: Chain — fill, focus, range-select, assert focus:
915
+ * And I fill in "Notes" with "hello world"
916
+ * When I move focus to "Notes" field
917
+ * And I select from 0 to 5 text in "Notes" field
918
+ * Then I see Notes has focus
919
+ */
920
+ When(/^(I |we )*select from (\d+) to (\d+) text in "([^"]*)" field$/, async function (pronounCase, from, to, fieldLabel) {
921
+ const start = parseInt(from, 10);
922
+ const end = parseInt(to, 10);
923
+ const loc = resolveField(this.page, fieldLabel);
924
+ await loc.focus();
925
+ await loc.evaluate((el, [s, e]) => {
926
+ if (typeof el.setSelectionRange === 'function') el.setSelectionRange(s, e);
927
+ }, [start, end]);
928
+ });
929
+
930
+ /**
931
+ * Select a specific substring of text inside a field.
932
+ *
933
+ * Example #1: When I select "title name" text in "Title" field
934
+ * Example #2: When I select "some content" text in "Description" field
935
+ * Example #3: When I select "hello" text in "Body" field
936
+ * Example #4: When I select "admin" text in "Username" field
937
+ * Example #5: When I select "example" text in "Email" field
938
+ * Example #6: When we select "varbase-e2e" text in "Search" field
939
+ * Example #7: When select "lorem" text in "Notes" field
940
+ * Example #8: When I select "street" text in "Address" field
941
+ * Example #9: When we select "555" text in "Phone" field
942
+ * Example #10: When I select "summary" text in "Message" field
943
+ */
944
+ When(/^(I |we )*select "([^"]*)" text in "([^"]*)" field$/, async function (pronounCase, selectedText, fieldLabel) {
945
+ const loc = resolveField(this.page, fieldLabel);
946
+
947
+ const value = await loc.inputValue();
948
+ const start = value.indexOf(selectedText);
949
+ if (start === -1) throw friendly(`Text "${selectedText}" not found in field "${fieldLabel}"`);
950
+ const end = start + selectedText.length;
951
+
952
+ await loc.focus();
953
+ await loc.evaluate((el, [s, e]) => {
954
+ if (typeof el.setSelectionRange === 'function') el.setSelectionRange(s, e);
955
+ }, [start, end]);
956
+ });
957
+
958
+ // ===========================================================================
959
+ // STEP DEFINITIONS — Click by named component/selector
960
+ // ===========================================================================
961
+
962
+ /**
963
+ * Click one or more named components — uses locator.click() which auto-scrolls.
964
+ *
965
+ * The negative lookahead keeps three phrasings out of this greedy pattern —
966
+ * "the delete button", "on tasks in the toolbar" and "next button in tour" —
967
+ * because drupal-core / drupal-moderation / varbase own those exact steps and
968
+ * cucumber-js has no notion of a more-specific match winning.
969
+ *
970
+ * Example #1: When I click nav
971
+ * Example #2: When I click search button
972
+ * Example #3: When click cta
973
+ * Example #4: When I click on logo
974
+ * Example #5: When I click menu
975
+ * Example #6: When I click on submit button
976
+ * Example #7: When I click close button
977
+ * Example #8: When click back button
978
+ * Example #9: When I click on forward button
979
+ * Example #10: When I click hero, cta
980
+ *
981
+ * Advanced:
982
+ * Example #11: Click via XPath-registered selector:
983
+ * When I add "login link xp" selector for "//a[contains(.,'Login')]" xpath selector
984
+ * And I click login link xp
985
+ * Example #12: Click a sequence — comma-separated names:
986
+ * Given I define css selectors:
987
+ * | tab 1 | [data-tab='1'] |
988
+ * | tab 2 | [data-tab='2'] |
989
+ * When I click tab 1, tab 2
990
+ * Example #13: Register with CSS then click — no components needed:
991
+ * When I add "cta primary" selector for ".cta .btn-primary" css selector
992
+ * And I click cta primary
993
+ */
994
+ When(/^(I |we )*click (?!the delete button$|on tasks in the toolbar$|next button in tour$)(?:on |a )?([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subjectsText) {
995
+ const names = parseNames(subjectsText);
996
+ const errors = [];
997
+ for (const name of names) {
998
+ const sel = resolveSelector(this, name);
999
+ try { await this.page.locator(sel).first().click(); }
1000
+ catch (e) { errors.push(`Cannot click "${name}" (${sel}): ${e.message}`); }
1001
+ }
1002
+ if (errors.length > 0) throw friendly(errors.join('\n'));
1003
+ });