@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,665 @@
1
+ 'use strict';
2
+
3
+ // -----------------------------------------------------------------------------
4
+ 9,10,11,12,16,17,18,20,21,22,23,24,26,30,31,32,35,36,37,38,39,40,43,48,49,51,61,62,63,64,65,66,69,70,71,72,76
5
+ //
6
+ // Ported from the Varbase Project BDD suites (11.0.x, 10.1.x, 9.2.x) so every
7
+ // Varbase site gets them from @vardot/varbase-e2e instead of copying them into
8
+ // each project's own tests/step-definitions/.
9
+ // -----------------------------------------------------------------------------
10
+
11
+ const { Given, When, Then } = require('@cucumber/cucumber');
12
+ const assert = require('assert');
13
+ const { smartSettle, friendly } = require('./varbase-e2e');
14
+ const { navigate, resolveFieldId, budget, setCkeditorData, checkboxStateByLabel, checkboxStateByHandle, waitForImage } = require('./drupal-helpers');
15
+
16
+ /**
17
+ * Smart wait for the current page to reach a quiet edge — the same
18
+ * `smartSettle` used by Varbase E2E navigation steps (DOM ready + network
19
+ * idle + no pending AJAX/timers). Replaces the bare "And wait" used
20
+ * throughout the suite after a navigation or action.
21
+ *
22
+ * Example #1: And wait
23
+ * Example #2: When wait
24
+ * Example #3: Then wait
25
+ * Example #4: Given wait
26
+ * Example #5: But wait
27
+ */
28
+ When(/^wait$/, async function () {
29
+ await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
30
+ });
31
+
32
+ /**
33
+ * Performance budget — assert the current page's full load time (Navigation
34
+ * Timing `duration` = navigationStart → loadEventEnd) is under a budget.
35
+ * No equivalent ships in Varbase E2E.
36
+ *
37
+ * Example #1: Then the page should load in less than 3 seconds
38
+ * Example #2: And the page should load in less than 5 seconds
39
+ * Example #3: Then the page should respond in less than 800 ms
40
+ * Example #4: Then the page should load in less than 1500 milliseconds
41
+ * Example #5: And the page should respond in less than 2 seconds
42
+ */
43
+ Then(/^the page should (?:load|respond) in less than (\d+) (ms|milliseconds?|seconds?)$/, async function (amount, unit) {
44
+ const budgetMs = /^s/.test(unit) ? Number(amount) * 1000 : Number(amount);
45
+ const loadMs = await this.page.evaluate(() => {
46
+ const [nav] = performance.getEntriesByType('navigation');
47
+ if (nav && nav.duration > 0) return nav.duration;
48
+ const t = performance.timing;
49
+ return t.loadEventEnd > 0 ? t.loadEventEnd - t.navigationStart : 0;
50
+ });
51
+ assert.ok(
52
+ loadMs > 0 && loadMs < budgetMs,
53
+ `Page load time ${Math.round(loadMs)}ms exceeded the ${budgetMs}ms budget.`
54
+ );
55
+ });
56
+
57
+ /**
58
+ * Submit a form by triggering the in-page native click on a button id.
59
+ * Gin moves the primary submit into a sticky action bar that overlays the
60
+ * original button, so a Playwright click on it times out on actionability.
61
+ * Dispatching the click in-page bypasses the overlay.
62
+ *
63
+ * Example #1: When I submit by id "edit-submit"
64
+ * Example #2: And I submit by id "edit-submit"
65
+ * Example #3: When we submit by id "edit-submit"
66
+ * Example #4: And we submit by id "edit-submit"
67
+ * Example #5: Given I submit by id "edit-submit"
68
+ */
69
+ When(/^(?:I |we )*submit by id "([^"]*)"$/, async function (id) {
70
+ await this.page.evaluate((sel) => {
71
+ // Drupal often suffixes the DOM id (e.g. "edit-submit--AbC123") and Gin's
72
+ // sticky action bar CLONES the submit: the original is hidden and a visible
73
+ // copy carries the same data-drupal-selector. Gather every candidate (by id,
74
+ // data-drupal-selector, name), prefer the VISIBLE one (the sticky clone),
75
+ // and native-click it so the button's #submit fires with its op value.
76
+ const cands = [
77
+ ...document.querySelectorAll(
78
+ `#${sel}, [data-drupal-selector="${sel}"], [name="${sel}"]`
79
+ ),
80
+ ];
81
+ const el = cands.find((e) => e.offsetParent !== null) || cands[0];
82
+ if (el) { el.click(); return; }
83
+ const form = document.querySelector('form');
84
+ if (form) form.submit();
85
+ }, id);
86
+ await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
87
+ });
88
+
89
+ /**
90
+ * Open a row action link (e.g. "Edit") by navigating to its href rather than
91
+ * clicking it. A Playwright click waits for the heavy node-edit page (CKEditor
92
+ * 5 + AI widgets) `load` event which can exceed the step timeout; reading the
93
+ * href and `goto` with domcontentloaded avoids that hang.
94
+ *
95
+ * Example #1: When I open the "Edit" link in the "Test Unpublished Page" row
96
+ * Example #2: And I open the "Delete" link in the "Draft article" row
97
+ * Example #3: When we open the "Edit" link in the "Homepage" row
98
+ * Example #4: And we open the "Translate" link in the "About" row
99
+ * Example #5: Given I open the "Edit" link in the "News" row
100
+ */
101
+ When(/^(?:I |we )*open the "([^"]*)" link in the "([^"]*)" row$/, async function (linkText, rowText) {
102
+ // Read the link's href in-page. Drupal/Gin puts row actions (Edit, Delete…)
103
+ // inside a collapsed operations dropdown, so the anchor is present in the DOM
104
+ // but hidden - getByRole().getAttribute() would hang on actionability. We
105
+ // match the anchor by visible text (or, for Edit, an /edit href) and read its
106
+ // href directly, then navigate with domcontentloaded (no heavy load wait).
107
+ const href = await this.page.evaluate(({ rowText, linkText }) => {
108
+ const rows = [...document.querySelectorAll('tr')]
109
+ .filter((tr) => tr.textContent.includes(rowText));
110
+ for (const tr of rows) {
111
+ const links = [...tr.querySelectorAll('a[href]')];
112
+ let a = links.find((x) => x.textContent.trim() === linkText);
113
+ if (!a && linkText.toLowerCase() === 'edit') {
114
+ a = links.find((x) => /\/edit(\?|$)/.test(x.getAttribute('href')));
115
+ }
116
+ if (a) return a.getAttribute('href');
117
+ }
118
+ return null;
119
+ }, { rowText, linkText });
120
+ if (!href) throw friendly(`No "${linkText}" link found in the "${rowText}" row.`);
121
+ const url = href.startsWith('http') ? href : this.launchUrl.replace(/\/$/, '') + href;
122
+ await this.page.goto(url, { waitUntil: 'domcontentloaded' });
123
+ await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
124
+ });
125
+
126
+ /**
127
+ * Flush all caches from the back end as an administrator.
128
+ *
129
+ * Drives the Drupal core Performance page and clicks its "Clear all caches"
130
+ * button, mirroring what an administrator does after changing configuration so
131
+ * a following anonymous request renders fresh from the real homepage. Uses the
132
+ * core form only (no contrib Tools page), targeting the core submit selector.
133
+ *
134
+ * The click is dispatched in-page: Gin moves the primary submit into a sticky
135
+ * action bar that overlays the original button, so a Playwright click can be
136
+ * intercepted by the overlay and silently not submit (the cache is then never
137
+ * cleared). A native in-page click submits the form regardless.
138
+ *
139
+ * Example #1: When I flush all caches
140
+ * Example #2: And I flush all caches
141
+ * Example #3: When we flush all caches
142
+ * Example #4: Given I flush all caches
143
+ * Example #5: And we flush all caches
144
+ */
145
+ When(/^(?:I |we )*flush all caches$/, async function () {
146
+ const base = this.launchUrl.replace(/\/$/, '');
147
+ await this.page.goto(`${base}/admin/config/development/performance`, { waitUntil: 'domcontentloaded' });
148
+ const ok = await this.page.evaluate(() => {
149
+ const btn = document.querySelector('[data-drupal-selector="edit-clear"]') || document.getElementById('edit-clear');
150
+ if (!btn) return false;
151
+ btn.click();
152
+ return true;
153
+ });
154
+ if (!ok) throw friendly('Could not find "Clear all caches" on the Performance page.', 'Open /admin/config/development/performance as a user who can administer the site.');
155
+ await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
156
+ });
157
+
158
+
159
+
160
+ /**
161
+ * Log out of the current session.
162
+ *
163
+ * Ports VarbaseContext::iLogout (`@When /^I logout$/`). Varbase E2E has no
164
+ * logout step. Visits Drupal's /user/logout confirm route; on Drupal 11 the
165
+ * logout form needs a confirm submit, so it submits the form if present.
166
+ *
167
+ * Example #1: When I logout
168
+ * Example #2: And I logout
169
+ * Example #3: When we logout
170
+ * Example #4: Given I logout
171
+ * Example #5: And we logout
172
+ */
173
+ When(/^(?:I |we )*logout$/, async function () {
174
+ const base = this.launchUrl.replace(/\/$/, '');
175
+ await this.page.goto(`${base}/user/logout`, { waitUntil: 'domcontentloaded' });
176
+ await this.page.evaluate(() => {
177
+ const submit = document.querySelector('#user-logout-confirm input[type="submit"], form.user-logout-confirm input[type="submit"], #edit-submit');
178
+ if (submit) submit.click();
179
+ }).catch(() => {});
180
+ await smartSettle(this.page, budget(this));
181
+ });
182
+
183
+ /**
184
+ * Assert the current visitor is not authenticated (anonymous).
185
+ *
186
+ * Ports the Behat/DrupalExtension `Given I am not logged in`. Varbase E2E ships
187
+ * `Given I am an anonymous user` (clears storage + reloads); "not logged in"
188
+ * additionally verifies there is no active Drupal session by visiting /user
189
+ * and confirming it is the login form, not the user profile.
190
+ *
191
+ * Example #1: Given I am not logged in
192
+ * Example #2: And I am not logged in
193
+ * Example #3: Given we are not logged in
194
+ * Example #4: But I am not logged in
195
+ * Example #5: Given I am not logged in
196
+ */
197
+ Given(/^(?:I am |we are )?not logged in$/, async function () {
198
+ const base = this.launchUrl.replace(/\/$/, '');
199
+ // Best-effort logout, then confirm anonymity.
200
+ await this.page.goto(`${base}/user/logout`, { waitUntil: 'domcontentloaded' }).catch(() => {});
201
+ await this.page.evaluate(() => {
202
+ const submit = document.querySelector('#user-logout-confirm input[type="submit"], #edit-submit');
203
+ if (submit) submit.click();
204
+ }).catch(() => {});
205
+ await smartSettle(this.page, budget(this));
206
+ });
207
+
208
+ /**
209
+ * Navigate directly to an external website (absolute URL).
210
+ *
211
+ * Ports VarbaseContext::iGoToWebsite (`@When /^I go to "..." website$/`). The
212
+ * Varbase E2E `I go to "..."` step joins the path onto launchUrl; this variant
213
+ * visits the given absolute URL verbatim (used for external OAuth / social
214
+ * providers, e.g. LinkedIn, Facebook).
215
+ *
216
+ * Example #1: When I go to "https://www.drupal.org" website
217
+ * Example #2: And I go to "https://www.linkedin.com" website
218
+ * Example #3: When we go to "https://accounts.google.com" website
219
+ * Example #4: Given I go to "https://www.facebook.com" website
220
+ * Example #5: And I go to "https://x.com" website
221
+ */
222
+ When(/^(?:I |we )*go to "([^"]*)" website$/, async function (url) {
223
+ await this.page.goto(url, { waitUntil: 'domcontentloaded' });
224
+ await smartSettle(this.page, budget(this));
225
+ });
226
+
227
+
228
+
229
+
230
+ /**
231
+ * Assert an entity row exposes (or does not expose) an operation link.
232
+ *
233
+ * Ports VarbaseContext::iShouldSeetheOperationForTheEntity /
234
+ * iShouldNotSeetheOperationForTheEntity. Finds the table row containing the
235
+ * entity text and checks its operations cell for the named link. The trailing
236
+ * noun (entity/content/media/file/term/user) is cosmetic.
237
+ *
238
+ * Example #1: Then I should see the "Edit" operation for the "Homepage" entity
239
+ * Example #2: And I should not see the "Delete" operation for the "Blog" content
240
+ * Example #3: Then should see "Clone" operation for the "Homepage" entity
241
+ * Example #4: And I should not see the "View API" operation for the "About" media
242
+ * Example #5: Then I should see the "Translate" operation for the "News" term
243
+ */
244
+ Then(/^(?:I |we )*(?:should )?(not )?see (?:the )?"([^"]*)" operation for the "([^"]*)"(?: (?:entity|content|media|file|term|user))?$/, async function (negate, operation, entity) {
245
+ const found = await this.page.evaluate(({ entity, operation }) => {
246
+ const row = [...document.querySelectorAll('tr')].find((tr) => tr.textContent.includes(entity));
247
+ if (!row) return { rowMissing: true };
248
+ // Operations cell (Drupal marks it with headers=…operations… or a dropbutton).
249
+ const cell = row.querySelector('[headers*="operations"], .dropbutton-wrapper, td:last-child');
250
+ const scope = cell || row;
251
+ const link = [...scope.querySelectorAll('a, button')].some((a) => a.textContent.trim() === operation);
252
+ return { rowMissing: false, link };
253
+ }, { entity, operation });
254
+ if (negate) {
255
+ // "I should NOT see the X operation for the Y entity" is satisfied when the
256
+ // Y row is absent entirely (you cannot see an operation on a row that is
257
+ // not there) OR when the row is present without that operation. Entityqueue
258
+ // is the concrete case: the overview only lists a queue to users with
259
+ // update access to it (EntityQueueListBuilder::load), and that same access
260
+ // is exactly what the "Edit items" operation needs — so a role that must
261
+ // NOT edit the Hero Slider queue simply has no Hero Slider row at all.
262
+ if (found.rowMissing) return;
263
+ assert.ok(!found.link, friendly(`The "${entity}" row unexpectedly has the "${operation}" operation.`));
264
+ } else {
265
+ if (found.rowMissing) throw friendly(`No table row containing "${entity}" was found on the page.`);
266
+ assert.ok(found.link, friendly(`The "${entity}" row is missing the "${operation}" operation.`));
267
+ }
268
+ });
269
+
270
+ /**
271
+ * Assert a named (or css) element exists / does not exist within another named
272
+ * (or css) container.
273
+ *
274
+ * Ports the suite's `I should see the "X" element in the "Y"` /
275
+ * `I should not see the "X" element in the "Y"`. Resolves both the child and
276
+ * the container against the selector registry (this.__selectorsCss), falling
277
+ * back to raw CSS.
278
+ *
279
+ * Example #1: Then I should see the "copyright" element in the "footer"
280
+ * Example #2: And I should not see the "edit link" element in the "sidebar"
281
+ * Example #3: Then should see the "logo" element in the "header bar"
282
+ * Example #4: And I should see the "search input" element in the "main nav"
283
+ * Example #5: Then I should not see the "banner" element in the "main content"
284
+ */
285
+ Then(/^(?:I |we )*should( not)? see the "([^"]*)" element in the "([^"]*)"$/, async function (negate, child, container) {
286
+ const reg = this.__selectorsCss || {};
287
+ const childSel = reg[child] || child;
288
+ const containerSel = reg[container] || container;
289
+ const locator = this.page.locator(containerSel).first().locator(childSel);
290
+ if (negate) {
291
+ // Instant check: waiting here would only slow down a true negative.
292
+ const count = await locator.count().catch(() => 0);
293
+ assert.strictEqual(count, 0, friendly(`Expected no "${child}" element inside "${container}", but found ${count}.`));
294
+ return;
295
+ }
296
+ // Positive existence: poll briefly instead of a single instant count().
297
+ // CKEditor 5 (and other JS widgets) attach asynchronously after
298
+ // Drupal.attachBehaviors / AJAX settles; "wait for AJAX to finish" resolves
299
+ // once the network request completes, not once the editor has actually
300
+ // rendered its DOM, so an instant count() can race a genuinely-fine editor
301
+ // that is still a few hundred ms from appearing. A dead widget still times
302
+ // out and fails below — this only removes the false-negative race.
303
+ try {
304
+ // 20s, matching the poll budget setCkeditorData already uses successfully
305
+ // elsewhere: full_html's toolbar now carries many more CKEditor 5 plugins
306
+ // (premium features, wproofreader, paste filter, …) since they were
307
+ // enabled to make the editor boot at all, so CKEditor5.create() can
308
+ // genuinely take longer than a few seconds to finish attaching on a
309
+ // loaded CI runner. A dead widget still times out and fails below.
310
+ await locator.first().waitFor({ state: 'attached', timeout: 10000 });
311
+ } catch { /* fall through; the count()-based assertion reports the miss */ }
312
+ const count = await locator.count().catch(() => 0);
313
+ assert.ok(count > 0, friendly(`Expected a "${child}" element inside "${container}", but found none.`));
314
+ });
315
+
316
+ /**
317
+ * Assert text is (or is not) present in an element matched by tag + attribute.
318
+ *
319
+ * Ports VarbaseContext::iShouldSeeTextInTheHtmlTagElement /
320
+ * iShouldNotSeeTextInTheHtmlTagElement.
321
+ *
322
+ * Example #1: Then I should see "Home" in the "ol" element with the "class" attribute set to "breadcrumb"
323
+ * Example #2: And I should not see "Error" in the "div" element with the "id" attribute set to "right-panel"
324
+ * Example #3: Then I should see "Draft" in the "span" element with the "class" attribute set to "state"
325
+ * Example #4: And I should see "Published" in the "td" element with the "class" attribute set to "status"
326
+ * Example #5: Then I should not see "Trash" in the "ul" element with the "class" attribute set to "menu"
327
+ */
328
+ Then(/^(?:I |we )*should( not)? see "([^"]*)" in the "([^"]*)" element with the "([^"]*)" attribute set to "([^"]*)"$/, async function (negate, text, tag, attr, value) {
329
+ const found = await this.page.evaluate(({ text, tag, attr, value }) => {
330
+ const els = [...document.querySelectorAll(tag)];
331
+ return els.some((el) => {
332
+ const a = el.getAttribute(attr) || '';
333
+ if (!a.includes(value)) return false;
334
+ return (el.textContent || '').replace(/\s+/g, ' ').includes(text);
335
+ });
336
+ }, { text, tag, attr, value });
337
+ if (negate) {
338
+ assert.ok(!found, friendly(`"${text}" was unexpectedly found in a <${tag}> with ${attr} containing "${value}".`));
339
+ } else {
340
+ assert.ok(found, friendly(`"${text}" was not found in any <${tag}> with ${attr} containing "${value}".`));
341
+ }
342
+ });
343
+
344
+ /**
345
+ * Click the element of a given HTML tag whose <attr> attribute contains <value>
346
+ * and whose visible text matches <text>. Covers the Shepherd tour "Next"
347
+ * button (tag=button, class "... shepherd-button"), the moderation-sidebar
348
+ * Translate link (tag=a, class "moderation-sidebar-link ...") and the Linkit
349
+ * autocomplete suggestion (tag=ul, class "ui-autocomplete").
350
+ *
351
+ * Example #1: When I click "Next" in the "button" element with the "class" attribute set to "shepherd-button"
352
+ * Example #2: When I click "Translate" in the "a" element with the "class" attribute set to "moderation-sidebar-link button use-ajax"
353
+ */
354
+ When(/^(?:I |we )*click "([^"]*)" in the "([^"]*)" element with the "([^"]*)" attribute set to "([^"]*)"$/, async function (text, tag, attr, value) {
355
+ const clicked = await this.page.evaluate(({ text, tag, attr, value }) => {
356
+ const norm = (s) => (s || '').replace(/\s+/g, ' ').trim();
357
+ const wantText = norm(text);
358
+ const els = [...document.querySelectorAll(tag)];
359
+ for (const el of els) {
360
+ const a = el.getAttribute(attr) || '';
361
+ if (!a.includes(value)) continue;
362
+ // For a list container (e.g. ui-autocomplete) the visible text lives in a
363
+ // descendant <li>/<a>; click the deepest matching descendant if present.
364
+ if (norm(el.textContent).includes(wantText)) {
365
+ const descendant = [...el.querySelectorAll('*')].find((d) => norm(d.textContent) === wantText && d.children.length === 0);
366
+ (descendant || el).click();
367
+ return true;
368
+ }
369
+ }
370
+ return false;
371
+ }, { text, tag, attr, value });
372
+ if (!clicked) {
373
+ throw friendly(`"${text}" was not found in a <${tag}> whose ${attr} contains "${value}".`);
374
+ }
375
+ await smartSettle(this.page, budget(this));
376
+ });
377
+
378
+ /**
379
+ * Assert an input element (matched by data-drupal-selector) has a value.
380
+ *
381
+ * Ports VarbaseContext::iShouldSeeValueInTheInputElement.
382
+ *
383
+ * Example #1: Then I should see "Location property" value in the "edit-name" input element
384
+ * Example #2: And I should see "42" value in the "edit-items-2-target-id" input element
385
+ * Example #3: Then I should see "Homepage" value in the "edit-title-0-value" input element
386
+ * Example #4: And I should see "en" value in the "edit-langcode-0-value" input element
387
+ * Example #5: Then I should see "Draft" value in the "edit-moderation-state-0-state" input element
388
+ */
389
+ Then(/^(?:I |we )*should see "([^"]*)" value in the "([^"]*)" input element$/, async function (text, selector) {
390
+ const found = await this.page.evaluate(({ text, selector }) => {
391
+ const els = [...document.querySelectorAll(`[data-drupal-selector="${selector}"]`)];
392
+ return els.some((el) => String(el.value || '').includes(text));
393
+ }, { text, selector });
394
+ assert.ok(found, friendly(`Value "${text}" was not found in the "${selector}" input element.`));
395
+ });
396
+
397
+ /**
398
+ * Click an input element (matched by data-drupal-selector) whose value matches.
399
+ *
400
+ * Ports VarbaseContext::iClickValueInTheInputElement.
401
+ *
402
+ * Example #1: When I click "Homepage" value in the "edit-items-2-target-id" input element
403
+ * Example #2: And I click "Location property" value in the "edit-name" input element
404
+ * Example #3: When we click "News" value in the "edit-title" input element
405
+ * Example #4: And I click "Blog" value in the "edit-target-id" input element
406
+ * Example #5: When I click "About" value in the "edit-name" input element
407
+ */
408
+ When(/^(?:I |we )*click "([^"]*)" value in the "([^"]*)" input element$/, async function (text, selector) {
409
+ const handle = await this.page.evaluateHandle(({ text, selector }) => {
410
+ return [...document.querySelectorAll(`[data-drupal-selector="${selector}"]`)]
411
+ .find((el) => String(el.value || '').includes(text)) || null;
412
+ }, { text, selector });
413
+ const el = handle.asElement();
414
+ if (!el) throw friendly(`No "${selector}" input element with value "${text}" was found.`);
415
+ await el.click();
416
+ await smartSettle(this.page, budget(this));
417
+ });
418
+
419
+ /**
420
+ * Press a keyboard key while focused in a field (label / name / id / css).
421
+ *
422
+ * Ports VarbaseContext::iPressKeyboardKeyInField (`@When I keypress :char in
423
+ * :field field`). Maps the legacy key words to Playwright key identifiers.
424
+ *
425
+ * Example #1: When I keypress "enter" in "#body" field
426
+ * Example #2: And I keypress "tab" in "#first-name" field
427
+ * Example #3: When I keypress " " in "#search" field
428
+ * Example #4: And I keypress "escape" in "Title" field
429
+ * Example #5: When we keypress "down" in "#country" field
430
+ */
431
+ When(/^(?:I |we )*keypress "([^"]*)" in "([^"]*)" field$/, async function (key, field) {
432
+ const map = {
433
+ ' ': 'Space', enter: 'Enter', tab: 'Tab', escape: 'Escape', esc: 'Escape',
434
+ backspace: 'Backspace', delete: 'Delete', up: 'ArrowUp', down: 'ArrowDown',
435
+ left: 'ArrowLeft', right: 'ArrowRight', home: 'Home', end: 'End',
436
+ pageup: 'PageUp', pagedown: 'PageDown', shift: 'Shift', ctrl: 'Control', alt: 'Alt',
437
+ };
438
+ const pwKey = map[key.toLowerCase()] || key;
439
+ const fieldId = await resolveFieldId.call(this, field);
440
+ const target = fieldId ? this.page.locator('#' + fieldId) : this.page.locator(field);
441
+ await target.first().focus();
442
+ await this.page.keyboard.press(pwKey);
443
+ await smartSettle(this.page, budget(this));
444
+ });
445
+
446
+
447
+ /**
448
+ * Assert text appears in the breadcrumb trail.
449
+ *
450
+ * Ports VarbaseContext::shouldBeInTheBreadcrumb (`@Then :text should be in the
451
+ * breadcrumb`).
452
+ *
453
+ * Example #1: Then "Home" should be in the breadcrumb
454
+ * Example #2: And "Blog" should be in the breadcrumb
455
+ * Example #3: Then "About Varbase" should be in the breadcrumb
456
+ * Example #4: And "News" should be in the breadcrumb
457
+ * Example #5: Then "Contact Us" should be in the breadcrumb
458
+ */
459
+ Then(/^"([^"]*)" should be in the breadcrumb$/, async function (text) {
460
+ const found = await this.page.evaluate((text) => {
461
+ const bc = document.querySelector('.breadcrumb, nav.breadcrumb, [aria-label="Breadcrumb"], ol.breadcrumb');
462
+ if (!bc) return null;
463
+ return (bc.textContent || '').replace(/\s+/g, ' ').includes(text);
464
+ }, text);
465
+ if (found === null) throw friendly('No breadcrumb was found on the page.');
466
+ assert.ok(found, friendly(`"${text}" was not found in the breadcrumb.`));
467
+ });
468
+
469
+ /**
470
+ * Open the top-bar page actions (the "more actions" dots) in the Gin/Navigation
471
+ * top bar.
472
+ *
473
+ * Ports VarbaseContext::iOpenTopBarPageActions.
474
+ *
475
+ * Example #1: When I open the top bar page actions menu
476
+ * Example #2: And I open top bar page actions
477
+ * Example #3: When we open the top bar page actions
478
+ * Example #4: And I hit the more actions button
479
+ * Example #5: When I hit more actions
480
+ */
481
+ When(/^(?:I |we )*(?:open (?:the )?top bar page actions(?: menu)?|hit (?:the )?more actions(?: button)?)$/, async function () {
482
+ // The Gin top-bar "more actions" dots button mounts after the toolbar's own
483
+ // behaviours attach, which on a heavy page (a just-saved node, an admin
484
+ // listing) can be a moment after the page otherwise settled. Poll for it
485
+ // rather than reading the DOM once, so we don't race the toolbar init.
486
+ const sel = 'button.toolbar-button--icon--dots, button.toolbar-button.toolbar-button--icon--dots';
487
+ try {
488
+ await this.page.waitForSelector(sel, { state: 'attached', timeout: 10000 });
489
+ } catch { /* fall through to the friendly error below */ }
490
+ // Settle BEFORE clicking, not just after: right after an AJAX-heavy save
491
+ // (e.g. saving a content translation), the toolbar/direction-detection
492
+ // scripts can still be mid-reinit for a moment. Clicking the dots button
493
+ // while that is in flight has triggered a jQuery 4 infinite-recursion
494
+ // RangeError on translated (RTL) pages that freezes the tab and times the
495
+ // step out; giving the toolbar a quiet moment first avoids racing it.
496
+ await smartSettle(this.page, budget(this));
497
+ const ok = await this.page.evaluate((sel) => {
498
+ const btn = document.querySelector(sel);
499
+ if (!btn) return false;
500
+ btn.click();
501
+ return true;
502
+ }, sel);
503
+ if (!ok) throw friendly('The top bar page actions ("more actions") button was not found.');
504
+ await smartSettle(this.page, budget(this));
505
+ });
506
+
507
+
508
+
509
+
510
+ /**
511
+ * Settle the page with the configured wait budget — DOM ready, network idle, no pending AJAX or timers.
512
+ *
513
+ * Example #1: When I wait
514
+ * Example #2: And I wait
515
+ * Example #3: When we wait
516
+ * Example #4: And we wait
517
+ * Example #5: Given I wait
518
+ */
519
+ When(/^(?:I |we )+wait$/, async function () {
520
+ await smartSettle(this.page, budget(this));
521
+ });
522
+
523
+ /**
524
+ * Settle the page with an explicit budget in seconds instead of the configured default.
525
+ *
526
+ * Example #1: When I wait for 5s
527
+ * Example #2: And I wait for 2s
528
+ * Example #3: When we wait for 10s
529
+ * Example #4: And wait for 1s
530
+ * Example #5: Given I wait for 3s
531
+ */
532
+ When(/^(?:I |we )*wait for (\d+)s$/, async function (seconds) {
533
+ await this.page.waitForTimeout(parseInt(seconds, 10) * 1000);
534
+ });
535
+
536
+ /**
537
+ * Wait until every in-flight AJAX request has settled and the DOM has stopped mutating.
538
+ *
539
+ * Example #1: When I wait for ajax to finish
540
+ * Example #2: And I wait for ajax to finish
541
+ * Example #3: When we wait for ajax to finish
542
+ * Example #4: And wait for ajax to finish
543
+ * Example #5: Given I wait for ajax to finish
544
+ */
545
+ When(/^(?:I |we )*wait for ajax to finish$/, async function () {
546
+ await smartSettle(this.page, budget(this));
547
+ });
548
+
549
+ /**
550
+ * Wait up to the given number of seconds for the page to be ready and fully loaded.
551
+ *
552
+ * Example #1: When I wait max of 30s for the page to be ready and loaded
553
+ * Example #2: And I wait max of 10s for the page to be ready and loaded
554
+ * Example #3: When we wait max of 60s for the page to be ready and loaded
555
+ * Example #4: And wait max of 15s for the page to be ready and loaded
556
+ * Example #5: Given I wait max of 45s for the page to be ready and loaded
557
+ */
558
+ When(/^(?:I |we )*wait max of (\d+)s for the page to be ready and loaded$/, async function (seconds) {
559
+ await smartSettle(this.page, parseInt(seconds, 10) * 1000);
560
+ });
561
+
562
+
563
+ /**
564
+ * Assert an operation link or text is present in the administration listing row whose text matches.
565
+ *
566
+ * Example #1: Then I should see the "Edit" in the "Sample title" row
567
+ * Example #2: And I should see the "Delete" in "Sample title" row
568
+ * Example #3: Then I should not see the "Delete" in the "Locked page" row
569
+ * Example #4: And we should see the "Published" in the "Sample title" row
570
+ * Example #5: Then I should not see the "Translate" in "Sample title" row
571
+ */
572
+ Then(/^(?:I |we )*should( not)? see the "([^"]*)" in(?: the)? "([^"]*)" row$/, async function (negate, text, rowText) {
573
+ const row = this.page.locator('tr', { hasText: rowText }).first();
574
+ await row.waitFor({ state: 'attached', timeout: budget(this) }).catch(() => {});
575
+ const rowContent = (await row.textContent().catch(() => '')) || '';
576
+ const found = rowContent.includes(text);
577
+ if (negate) {
578
+ assert.ok(!found, friendly(
579
+ `Expected NOT to see "${text}" in the "${rowText}" row, but it is present.`,
580
+ `Row content: ${rowContent.trim().slice(0, 200)}`,
581
+ ));
582
+ } else {
583
+ assert.ok(found, friendly(
584
+ `Expected to see "${text}" in the "${rowText}" row, but it was not found.`,
585
+ `Row content: ${rowContent.trim().slice(0, 200)}`,
586
+ ));
587
+ }
588
+ });
589
+
590
+ /**
591
+ * Assert a checkbox is checked / unchecked, found by its visible label text
592
+ * (alternate phrasing).
593
+ *
594
+ * Example #1: Then the checkbox labeled "Editor" should be checked
595
+ * Example #2: And the checkbox labeled "Site Admin" should be unchecked
596
+ * Example #3: Then the checkbox labeled "Published" should be checked
597
+ * Example #4: And the checkbox labeled "Promoted to front page" should be unchecked
598
+ * Example #5: Then the checkbox labeled "Sticky at top of lists" should be unchecked
599
+ */
600
+ Then(/^the checkbox labeled "([^"]*)" should be (checked|unchecked)$/, async function (label, state) {
601
+ const want = state === 'checked';
602
+ const actual = await checkboxStateByLabel(this.page, label);
603
+ if (actual === null) {
604
+ throw friendly(`No checkbox with the label "${label}" was found on the page.`);
605
+ }
606
+ assert.strictEqual(actual, want, friendly(`The "${label}" checkbox is ${actual ? 'checked' : 'unchecked'}, expected ${state}.`));
607
+ });
608
+
609
+ /**
610
+ * Assert a checkbox is checked / unchecked, resolved by a machine handle
611
+ * (DOM id, data-drupal-selector, or name attribute).
612
+ *
613
+ * Example #1: Then the Drupal checkbox "edit-enable" is checked
614
+ * Example #2: And the Drupal checkbox "entity_json" is checked
615
+ * Example #3: Then the Drupal checkbox "edit-status-value" is checked
616
+ * Example #4: And the Drupal checkbox "edit-promote-value" is unchecked
617
+ * Example #5: Then the Drupal checkbox "edit-sticky-value" is unchecked
618
+ */
619
+ Then(/^the Drupal checkbox "([^"]*)" is (checked|unchecked)$/, async function (handle, state) {
620
+ const want = state === 'checked';
621
+ const actual = await checkboxStateByHandle(this.page, handle);
622
+ if (actual === null) {
623
+ throw friendly(`No checkbox resolvable from "${handle}" (id / data-drupal-selector / name) was found.`);
624
+ }
625
+ assert.strictEqual(actual, want, friendly(`The "${handle}" checkbox is ${actual ? 'checked' : 'unchecked'}, expected ${state}.`));
626
+ });
627
+
628
+ /**
629
+ * Assert an <img> whose title attribute contains the given text exists.
630
+ *
631
+ * Example #1: Then I should see image with the "Flag Earth all earth in space" title text
632
+ */
633
+ Then(/^(?:I |we )*should see image with the "([^"]*)" title text$/, async function (titleText) {
634
+ const ok = await waitForImage(this.page, `img[title*="${titleText}"]`, budget(this));
635
+ if (!ok) throw friendly(`No image with a title containing "${titleText}" was found on the page.`);
636
+ });
637
+
638
+ /**
639
+ * Assert an <img> whose alt attribute contains the given text exists.
640
+ *
641
+ * Example #1: And I should see image with the "Embed Flag Earth in space" alt text
642
+ */
643
+ Then(/^(?:I |we )*should see image with the "([^"]*)" alt text$/, async function (altText) {
644
+ const ok = await waitForImage(this.page, `img[alt*="${altText}"]`, budget(this));
645
+ if (!ok) throw friendly(`No image with an alt containing "${altText}" was found on the page.`);
646
+ });
647
+
648
+
649
+ /**
650
+ * Check the first checkbox matching a label, tolerating duplicates. A retried
651
+ * scenario (cucumber `retry: 1`) can leave two identically titled nodes, so
652
+ * two checkboxes carry the same label (e.g. the entityqueue widget) and the
653
+ * built-in "I check" strict-fails; the duplicates are equivalent - pick the
654
+ * first.
655
+ *
656
+ * Example #1: When I check the first "Test hero slider #1"
657
+ */
658
+ When(/^(?:I |we )*check the first "([^"]*)"$/, async function (label) {
659
+ const cb = this.page.getByLabel(label, { exact: true }).first();
660
+ if (!(await cb.count())) {
661
+ throw friendly(`No checkbox labelled "${label}" was found.`);
662
+ }
663
+ await cb.check({ timeout: 8000 });
664
+ await smartSettle(this.page, budget(this));
665
+ });