@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,371 @@
1
+ 'use strict';
2
+
3
+ // -----------------------------------------------------------------------------
4
+ // Shared helpers for the Drupal and Varbase step packs.
5
+ //
6
+ // Not a step file: it registers no step definitions. It holds the plumbing the
7
+ // drupal-*.steps.js and varbase.steps.js files share - field resolution, the
8
+ // per-page settle budget, robust clicking, CKEditor data writing - plus the two
9
+ // Before hooks that point the asset folder and the named-selector registry at
10
+ // the consuming project rather than at this package.
11
+ // -----------------------------------------------------------------------------
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const { Before } = require('@cucumber/cucumber');
16
+ const { smartSettle, friendly, gotoUrl, waitForPageLoad } = require('./varbase-e2e');
17
+
18
+ // Resolve the assets folder to the repo's own tests/assets/ (Varbase E2E
19
+ // defaults `this.assetsFolder` to its own package dir, so `I attach the file
20
+ // "flag-earth.jpg"` looked for node_modules/Varbase E2E/tests/assets/…).
21
+ Before({ order: 1 }, function () {
22
+ this.assetsFolder = path.resolve(process.cwd(), 'tests/assets') + path.sep;
23
+ });
24
+
25
+ // Populate the named-selector registry (`this.__selectorsCss`) that the custom
26
+ // steps resolve container / child names against. Varbase E2E loads
27
+ // worldParameters.selectors.files into its OWN registry, but the custom steps
28
+ // here read `this.__selectorsCss`, which nothing ever filled — so a named
29
+ // container like "field body" fell back to the literal string "field body"
30
+ // (an invalid CSS descendant combinator that matches nothing), making
31
+ // `I should see the ".ck.ck-editor__main" element in the "field body"` fail
32
+ // with "found none" even though CKEditor had booted correctly. Load the same
33
+ // JSON selector files Varbase E2E uses and flatten their `css` maps here so the
34
+ // registry the custom steps consult is actually populated.
35
+ Before({ order: 2 }, function () {
36
+ // Varbase E2E's OWN Before hook (selectors.steps.js) already populated
37
+ // this.__selectorsCss from the JSON preset files. We must NOT early-return on
38
+ // that being non-empty — the previous guard did, so the "field body" default
39
+ // below was never merged, `field body` resolved to the literal (invalid) CSS
40
+ // string, and every `... in the "field body"` assertion false-failed with
41
+ // "found none" AFTER a 30s wait even though CKEditor had booted correctly.
42
+ // Start from whatever Varbase E2E populated (fall back to loading the same files
43
+ // ourselves only if it is somehow empty), then always merge our defaults.
44
+ let map = this.__selectorsCss;
45
+ if (!map || !Object.keys(map).length) {
46
+ map = {};
47
+ const cfg = (this.parameters && this.parameters.selectors) || {};
48
+ const dir = cfg.filesPath || './tests/selectors/';
49
+ for (const file of (cfg.files || [])) {
50
+ try {
51
+ const full = path.resolve(process.cwd(), dir, file);
52
+ const json = JSON.parse(fs.readFileSync(full, 'utf8'));
53
+ Object.assign(map, json.css || {}, json.selectors || {});
54
+ } catch (e) { /* a missing/invalid file simply contributes nothing */ }
55
+ }
56
+ }
57
+ // Named selectors the ported feature files reference that are not (or not
58
+ // reliably) present in the shared JSON presets. Kept here so the resolution
59
+ // is self-contained and does not silently regress if a preset changes.
60
+ // "field body" — the Body field wrapper on the node add/edit form. Drupal
61
+ // renders each field wrapper with a `field--name-<field>` class; the
62
+ // text-format widget (and the CKEditor 5 instance it attaches) lives
63
+ // inside it, so `.ck.ck-editor__main` / `#edit-body-0-value` are children.
64
+ const defaults = {
65
+ // The Body field widget wrapper on the node add/edit form. The CKEditor 5
66
+ // instance (.ck.ck-editor__main) and the raw textarea (#edit-body-0-value)
67
+ // are attached inside the textarea's own form-item wrapper
68
+ // (.form-item--body-0-value / .js-form-item-body-0-value), which sits inside
69
+ // the field wrapper (.field--name-body). On some form renders the outer
70
+ // .field--name-body wrapper is not emitted, so match the widget form-item
71
+ // wrapper too (both contain the editor); Playwright's .first() picks the
72
+ // outermost present. Diagnosed via the CK boot state: the editor boots fine
73
+ // but .field--name-body alone did not always contain it.
74
+ 'field body': '.field--name-body, .form-item--body-0-value, .js-form-item-body-0-value, [data-drupal-selector="edit-body-wrapper"]',
75
+ };
76
+ for (const [k, v] of Object.entries(defaults)) {
77
+ if (!(k in map)) map[k] = v;
78
+ }
79
+ this.__selectorsCss = map;
80
+ });
81
+
82
+ // When I fill in "value" for "field".
83
+ // Delegates to Varbase E2E's fillField, then adds Drupal-friendly fallbacks:
84
+ // • a bare id ("edit-name") -> #edit-name
85
+ // • a known label alias ("Email") -> "Email address"
86
+ // • a unique partial <label> match.
87
+ const FIELD_LABEL_ALIASES = {
88
+ 'Email': 'Email address',
89
+ };
90
+
91
+ const budget = (world) => (world.minWaitTime && world.minWaitTime.page) || 8000;
92
+
93
+ // Join launchUrl + path with exactly one slash so a path with OR without a
94
+ // leading slash resolves correctly (Varbase E2E uses a bare `launchUrl + url`).
95
+ function joinUrl(launchUrl, url) {
96
+ if (!url) return launchUrl;
97
+ if (/^https?:\/\//i.test(url)) return url; // absolute URL, use as-is.
98
+ return String(launchUrl).replace(/\/+$/, '') + '/' + String(url).replace(/^\/+/, '');
99
+ }
100
+
101
+ async function navigate(page, launchUrl, url, budget) {
102
+ await gotoUrl(page, joinUrl(launchUrl, url));
103
+ await page.waitForSelector('body', { state: 'attached', timeout: budget });
104
+ await waitForPageLoad(page, budget);
105
+ }
106
+
107
+ // Resolve a Drupal form field id from a label, name, or id/css.
108
+ async function resolveFieldId(locator) {
109
+ return this.page.evaluate((locator) => {
110
+ // by id
111
+ if (document.getElementById(locator)) return locator;
112
+ // by label text. Try an exact match first, then a normalised one: Drupal
113
+ // appends "(Edit summary)" to a Body/text field that has a summary and a
114
+ // trailing " *" to required fields, so the visible label for the "Body"
115
+ // field is actually "Body (Edit summary)". Strip those so "Body" resolves.
116
+ const norm = (s) => s.trim().replace(/\s*\(Edit summary\)\s*$/i, '').replace(/\s*\*\s*$/, '').trim();
117
+ let lbl = [...document.querySelectorAll('label')].find((l) => l.textContent.trim() === locator);
118
+ if (!lbl) lbl = [...document.querySelectorAll('label')].find((l) => norm(l.textContent) === locator);
119
+ if (lbl && lbl.getAttribute('for')) return lbl.getAttribute('for');
120
+ // by name
121
+ const byName = document.querySelector(`[name="${locator}"]`);
122
+ if (byName && byName.id) return byName.id;
123
+ // by css
124
+ try { const el = document.querySelector(locator); if (el && el.id) return el.id; } catch (e) { /* not a selector */ }
125
+ return null;
126
+ }, locator);
127
+ }
128
+
129
+ /**
130
+ * Click a Playwright locator robustly: try a normal click first (fast path,
131
+ * unaffected pages), then fall back to a force-click (skips the
132
+ * actionability/visibility checks, still a real pointer event), then to a
133
+ * native in-page click dispatch (bypasses hit-testing entirely). A
134
+ * fixed-position overlay (the AI chatbot widget most commonly, also sticky
135
+ * Gin/paragraphs action bars) sits on top of an otherwise-correctly-resolved
136
+ * button on some pages, so Playwright's pointer-based click hits a 30s
137
+ * actionability timeout even though the element itself is right — the same
138
+ * class of problem the login step already works around with an in-page
139
+ * dispatch. A genuinely missing/disabled element still fails all three.
140
+ */
141
+ async function robustClick(locator) {
142
+ // 1) Real, trusted pointer click at the element's coordinates (fires the full
143
+ // mousedown→mouseup→click). This is the path that submits Gin CONFIG forms:
144
+ // their sticky action-bar "Save configuration" clone is actionable and its
145
+ // Gin handler only forwards the submit from a genuine pointer click — a
146
+ // synthetic dispatch does NOT save a config form.
147
+ try {
148
+ await locator.click({ timeout: 5000 });
149
+ return;
150
+ } catch { /* fall through */ }
151
+ // 2) Not actionable — a fixed overlay covers it (the AI chatbot widget) or the
152
+ // element is a sticky clone the browser will not point-click on a tall form
153
+ // (the Landing page paragraphs node form). A coordinate force-click would
154
+ // land on whatever overlays it, so dispatch the pointer events DIRECTLY on
155
+ // the element. Include `mousedown`: Gin/Drupal bind the node-form sticky
156
+ // "Save as" submit and the paragraphs add-more buttons to mousedown, so a
157
+ // plain click() alone would not trigger them.
158
+ await locator.scrollIntoViewIfNeeded().catch(() => {});
159
+ await locator.evaluate((el) => {
160
+ for (const type of ['mousedown', 'mouseup', 'click']) {
161
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
162
+ }
163
+ });
164
+ }
165
+
166
+ async function setCheckbox(label, want) {
167
+ // Try Playwright's label association first.
168
+ const byLabel = this.page.getByLabel(label, { exact: true });
169
+ if (await byLabel.count().catch(() => 0)) {
170
+ if ((await byLabel.first().isChecked().catch(() => null)) !== want) {
171
+ await byLabel.first().click();
172
+ }
173
+ return;
174
+ }
175
+ // Fall back: id / name / raw css, or a <label> text -> for=id lookup.
176
+ const done = await this.page.evaluate(({ label, want }) => {
177
+ let cb = document.getElementById(label)
178
+ || document.querySelector(`input[type="checkbox"][name="${label}"]`)
179
+ || (() => { try { return document.querySelector(label); } catch (e) { return null; } })();
180
+ if (!cb) {
181
+ const lbl = [...document.querySelectorAll('label')].find((l) => l.textContent.trim() === label);
182
+ if (lbl) {
183
+ const forId = lbl.getAttribute('for');
184
+ cb = forId ? document.getElementById(forId) : lbl.querySelector('input[type="checkbox"]');
185
+ }
186
+ }
187
+ if (!cb) return false;
188
+ if (cb.checked !== want) cb.click();
189
+ return true;
190
+ }, { label, want });
191
+ if (!done) throw friendly(`Could not find a checkbox for "${label}".`, 'Pass the visible label, the input id/name, or a CSS selector.');
192
+ }
193
+
194
+ async function setCkeditorData(locator, value, mode) {
195
+ const fieldId = await resolveFieldId.call(this, locator);
196
+ if (!fieldId) return false;
197
+ // CKEditor 5 boots asynchronously after Drupal.attachBehaviors; on a fresh
198
+ // node-add form the instance may not be registered yet when this step runs.
199
+ // Poll for the instance (up to 15s) before reading it so we don't race the
200
+ // editor init. A genuinely dead editor still times out and returns false.
201
+ try {
202
+ await this.page.waitForFunction((id) => {
203
+ const el = document.getElementById(id);
204
+ return !!(el && window.Drupal && Drupal.CKEditor5Instances
205
+ && Drupal.CKEditor5Instances.get(el.dataset.ckeditor5Id));
206
+ }, fieldId, { timeout: 15000 });
207
+ } catch {
208
+ // TEMP DIAGNOSTIC: the editor never instantiated. Dump the page's editor
209
+ // state + raw console/page errors to the CI trace so we can see WHY.
210
+ try {
211
+ const diag = await this.page.evaluate((id) => {
212
+ const el = document.getElementById(id);
213
+ const sel = document.querySelector('select[name*="[format]"]');
214
+ return {
215
+ fieldFound: !!el,
216
+ tag: el ? el.tagName : null,
217
+ ck5id: el ? el.dataset.ckeditor5Id : null,
218
+ formatValue: sel ? sel.value : null,
219
+ registry: (window.Drupal && Drupal.CKEditor5Instances) ? Drupal.CKEditor5Instances.size : 'no-registry',
220
+ editors: (window.Drupal && Drupal.editors) ? Object.keys(Drupal.editors) : 'no-Drupal.editors',
221
+ hasCKEDITORglobal: typeof window.CKEDITOR,
222
+ textareaHasEditorClass: el ? el.className : null,
223
+ drupalSettingsEditorFormats: (window.drupalSettings && drupalSettings.editor && drupalSettings.editor.formats) ? Object.keys(drupalSettings.editor.formats) : 'none',
224
+ };
225
+ }, fieldId);
226
+ // eslint-disable-next-line no-console
227
+ console.log('[CK-DIAG] ' + JSON.stringify(diag) + ' rawErrors=' + JSON.stringify((this.__rawJsErrors || []).slice(0, 15)));
228
+ } catch (e) { /* diagnostic best-effort */ }
229
+ }
230
+ return this.page.evaluate(({ fieldId, value, mode }) => {
231
+ const el = document.getElementById(fieldId);
232
+ if (!el || !window.Drupal || !Drupal.CKEditor5Instances) return false;
233
+ const inst = Drupal.CKEditor5Instances.get(el.dataset.ckeditor5Id);
234
+ if (!inst) return false;
235
+ inst.setData(mode === 'append' ? inst.getData() + value : value);
236
+ // Write the editor data straight back to the source <textarea> now. CKEditor
237
+ // 5's Drupal integration normally syncs to the textarea on the form's native
238
+ // `submit` event, but the Varbase node form is submitted through the Gin
239
+ // sticky action bar's forwarded submit, which does not always fire that
240
+ // native submit handler — so the textarea can still be empty at POST time
241
+ // and a REQUIRED rich-text field (the Landing page paragraphs "Text" field)
242
+ // fails server validation, silently keeping the form on the add page. Force
243
+ // the sync here so the value is persisted regardless of how the form is
244
+ // ultimately submitted.
245
+ if (typeof inst.updateSourceElement === 'function') {
246
+ inst.updateSourceElement();
247
+ }
248
+ return true;
249
+ }, { fieldId, value, mode });
250
+ }
251
+
252
+ /**
253
+ * Resolve the default theme's settings path (e.g. /admin/appearance/settings/THEME)
254
+ * from the Appearance page, so tests stay theme-agnostic — a site may run a
255
+ * custom subtheme as its default theme.
256
+ */
257
+ async function defaultThemeSettingsPath(page, launchUrl) {
258
+ await page.goto(`${launchUrl.replace(/\/$/, '')}/admin/appearance`, { waitUntil: 'domcontentloaded' });
259
+ return page.evaluate(() => {
260
+ const link = [...document.querySelectorAll('a[href*="/admin/appearance/settings/"]')]
261
+ .map((a) => new URL(a.href, location.origin).pathname)
262
+ .find((p) => /\/admin\/appearance\/settings\/[a-z0-9_]+$/.test(p));
263
+ return link || null;
264
+ });
265
+ }
266
+
267
+ // Open a styles-tab settings sub-menu (Background / Typography / Spacing /
268
+ // Border / Animation / Blocks alignment) so its controls are in the DOM.
269
+ async function openSectionMenu(page, menu) {
270
+ await page.evaluate(() => {
271
+ const tab = document.querySelector("a[data-target*='appearance']");
272
+ if (tab) tab.click();
273
+ });
274
+ await page.evaluate((menu) => {
275
+ const span = [...document.querySelectorAll('span')].find((s) => (s.textContent || '').includes(menu));
276
+ if (span) { const d = span.closest('details'); if (d) d.setAttribute('open', ''); }
277
+ }, menu);
278
+ }
279
+
280
+ /**
281
+ * Read a checkbox's checked state, resolving it by its visible label text.
282
+ * Handles all common Drupal/Gin/Claro markups: label[for=id], wrapping label,
283
+ * and sibling label. Returns true / false / null (not found).
284
+ */
285
+ async function checkboxStateByLabel(page, label) {
286
+ return page.evaluate((labelText) => {
287
+ const norm = (s) => (s || '').replace(/\s+/g, ' ').trim();
288
+ const labels = [...document.querySelectorAll('label')]
289
+ .filter((l) => norm(l.textContent).includes(norm(labelText)));
290
+ for (const l of labels) {
291
+ // label[for] -> input#id
292
+ const forId = l.getAttribute('for');
293
+ if (forId) {
294
+ const byFor = document.getElementById(forId);
295
+ if (byFor && byFor.type === 'checkbox') return byFor.checked;
296
+ }
297
+ // wrapping label
298
+ const inner = l.querySelector('input[type="checkbox"]');
299
+ if (inner) return inner.checked;
300
+ // sibling input (preceding or following)
301
+ const prev = l.previousElementSibling;
302
+ if (prev && prev.matches && prev.matches('input[type="checkbox"]')) return prev.checked;
303
+ const next = l.nextElementSibling;
304
+ if (next && next.matches && next.matches('input[type="checkbox"]')) return next.checked;
305
+ }
306
+ return null;
307
+ }, label);
308
+ }
309
+
310
+ /**
311
+ * Read a checkbox's checked state, resolving it by a machine handle that may be
312
+ * a DOM id, a data-drupal-selector, or a name attribute. Returns true / false /
313
+ * null (not found).
314
+ */
315
+ async function checkboxStateByHandle(page, handle) {
316
+ return page.evaluate((h) => {
317
+ const el =
318
+ document.getElementById(h) ||
319
+ document.querySelector(`[data-drupal-selector="${h}"]`) ||
320
+ document.querySelector(`input[name="${h}"]`) ||
321
+ document.querySelector(`input[name="${h}[value]"]`);
322
+ if (!el) return null;
323
+ return !!el.checked;
324
+ }, handle);
325
+ }
326
+
327
+ /** Poll until at least one element matches the locator, or time out. */
328
+ async function waitForImage(page, selector, timeout) {
329
+ const start = Date.now();
330
+ do {
331
+ if (await page.locator(selector).count()) return true;
332
+ await page.waitForTimeout(150);
333
+ } while (Date.now() - start < timeout);
334
+ return false;
335
+ }
336
+
337
+ /**
338
+ * Assert an action IS / IS NOT offered in the open moderation sidebar
339
+ * (#drupal-off-canvas). Scoped to the sidebar so it is not confused by hidden
340
+ * Layout Builder / block contextual "Delete" links elsewhere on the page (the
341
+ * page-wide "I should not see" reads textContent, which includes those hidden
342
+ * links). Case-insensitive substring, so "Revisions" matches "Show revisions"
343
+ * and "Delete" matches "Delete content".
344
+ *
345
+ * Example #1: Then the moderation sidebar should show "Edit content"
346
+ * Example #2: And the moderation sidebar should not show "Delete"
347
+ */
348
+ async function moderationSidebarText(page) {
349
+ const oc = page.locator('#drupal-off-canvas');
350
+ if (!(await oc.count())) {
351
+ throw friendly('The moderation sidebar off-canvas is not open.');
352
+ }
353
+ return ((await oc.first().innerText()) || '').toLowerCase();
354
+ }
355
+
356
+ module.exports = {
357
+ FIELD_LABEL_ALIASES,
358
+ budget,
359
+ joinUrl,
360
+ navigate,
361
+ resolveFieldId,
362
+ robustClick,
363
+ setCheckbox,
364
+ setCkeditorData,
365
+ defaultThemeSettingsPath,
366
+ openSectionMenu,
367
+ checkboxStateByLabel,
368
+ checkboxStateByHandle,
369
+ waitForImage,
370
+ moderationSidebarText,
371
+ };