@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,211 @@
1
+ 'use strict';
2
+
3
+ const { friendly } = require('./varbase-e2e');
4
+
5
+ // Validate file downloads triggered via URL or link click.
6
+
7
+ const { When, Then } = require('@cucumber/cucumber');
8
+ const assert = require('assert');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ function tmpDownloadDir() {
14
+ const d = path.join(os.tmpdir(), `varbase-e2e-dl-${process.pid}`);
15
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
16
+ return d;
17
+ }
18
+
19
+ /**
20
+ * Download the file at a URL using the browser's request context. The result
21
+ * is captured on `this._lastDownload` for follow-up assertions.
22
+ *
23
+ * Relative paths are joined to `launchUrl`. Absolute URLs (`http(s)://...`)
24
+ * are used as-is.
25
+ *
26
+ * Example #1: When I download the file from the URL "/exports/users.csv"
27
+ * Example #2: When I download the file from the URL "https://example.com/doc.pdf"
28
+ * Example #3: And we download the file from the URL "/api/report?format=pdf"
29
+ * Example #4: When I download the file from the URL "/files/logo.svg"
30
+ * Example #5: When I download the file from the URL "/exports/data-2026.json"
31
+ *
32
+ */
33
+ When(/^(I |we )*download the file from the URL "([^"]*)"$/, async function (pronoun, url) {
34
+ const target = url.startsWith('http') ? url : (this.launchUrl || '') + url;
35
+ try {
36
+ const resp = await this.page.request.get(target);
37
+ assert.strictEqual(resp.status(), 200, `Download failed with status ${resp.status()}.`);
38
+ const buffer = await resp.body();
39
+ const cd = resp.headers()['content-disposition'] || '';
40
+ const m = /filename="?([^"]+)"?/.exec(cd);
41
+ const filename = m ? m[1] : path.basename(new URL(target).pathname) || 'download';
42
+ const filePath = path.join(tmpDownloadDir(), filename);
43
+ fs.writeFileSync(filePath, buffer);
44
+ this._lastDownload = { path: filePath, filename, content: buffer };
45
+ } catch (e) {
46
+ throw friendly({
47
+ action: `download "${target}"`,
48
+ cause: e,
49
+ hint: `check the URL is reachable and returns a successful response.`,
50
+ });
51
+ }
52
+ });
53
+
54
+ /**
55
+ * Click a link by visible text and capture the resulting download.
56
+ *
57
+ * Example #1: When I download the file from the link "Download report"
58
+ * Example #2: When I download the file from the link "Export CSV"
59
+ * Example #3: And we download the file from the link "Get logo"
60
+ * Example #4: When I download the file from the link "Download archive"
61
+ * Example #5: When I download the file from the link "Export JSON"
62
+ *
63
+ */
64
+ When(/^(I |we )*download the file from the link "([^"]*)"$/, async function (pronoun, linkText) {
65
+ try {
66
+ const [download] = await Promise.all([
67
+ this.page.waitForEvent('download'),
68
+ this.page.getByRole('link', { name: linkText }).first().click().catch(() =>
69
+ this.page.locator(`a:has-text("${linkText}")`).first().click()
70
+ ),
71
+ ]);
72
+ const filename = download.suggestedFilename();
73
+ const filePath = path.join(tmpDownloadDir(), filename);
74
+ await download.saveAs(filePath);
75
+ const content = fs.readFileSync(filePath);
76
+ this._lastDownload = { path: filePath, filename, content };
77
+ } catch (e) {
78
+ throw friendly({
79
+ action: `download from link "${linkText}"`,
80
+ cause: e,
81
+ hint: `the link must offer a downloadable file (not open in a new tab). If the link only navigates, use "When I download the file from the URL ..." instead.`,
82
+ });
83
+ }
84
+ });
85
+
86
+ /**
87
+ * Assert the most recent download's body contains an expected substring.
88
+ *
89
+ * Body is decoded as UTF-8.
90
+ *
91
+ * Example #1: Then the downloaded file should contain:
92
+ * """
93
+ * Order #1234
94
+ * """
95
+ * Example #2: Then the downloaded file should contain:
96
+ * """
97
+ * name,email
98
+ * """
99
+ * Example #3: And the downloaded file should contain:
100
+ * """
101
+ * <svg
102
+ * """
103
+ * Example #4: Then the downloaded file should contain:
104
+ * """
105
+ * "version": "1.0"
106
+ * """
107
+ * Example #5: Then the downloaded file should contain:
108
+ * """
109
+ * %PDF-
110
+ * """
111
+ *
112
+ */
113
+ Then('the downloaded file should contain:', async function (docString) {
114
+ assert.ok(this._lastDownload, 'No download has been performed.');
115
+ const text = this._lastDownload.content.toString('utf-8');
116
+ assert.ok(text.indexOf(docString) !== -1, `Downloaded file does not contain expected text.`);
117
+ });
118
+
119
+ /**
120
+ * Assert the most recent download's filename equals an expected value.
121
+ *
122
+ * Example #1: Then the downloaded file name should be "report.pdf"
123
+ * Example #2: Then the downloaded file name should be "users.csv"
124
+ * Example #3: And the downloaded file name should be "logo.svg"
125
+ * Example #4: Then the downloaded file name should be "archive.zip"
126
+ * Example #5: Then the downloaded file name should be "data-2026.json"
127
+ *
128
+ */
129
+ Then('the downloaded file name should be {string}', async function (name) {
130
+ assert.ok(this._lastDownload, 'No download has been performed.');
131
+ assert.strictEqual(this._lastDownload.filename, name);
132
+ });
133
+
134
+ /**
135
+ * Assert the most recent download's filename contains a substring.
136
+ *
137
+ * Useful when filenames embed timestamps or hashes.
138
+ *
139
+ * Example #1: Then the downloaded file name should contain "report"
140
+ * Example #2: Then the downloaded file name should contain ".pdf"
141
+ * Example #3: And the downloaded file name should contain "2026"
142
+ * Example #4: Then the downloaded file name should contain "users"
143
+ * Example #5: Then the downloaded file name should contain "export"
144
+ *
145
+ */
146
+ Then('the downloaded file name should contain {string}', async function (part) {
147
+ assert.ok(this._lastDownload, 'No download has been performed.');
148
+ assert.ok(this._lastDownload.filename.indexOf(part) !== -1, `Filename "${this._lastDownload.filename}" does not contain "${part}".`);
149
+ });
150
+
151
+ // ZIP archive assertions: pending. Install `yauzl` or `adm-zip` to enable.
152
+
153
+ /**
154
+ * (Pending) Assert the downloaded ZIP archive contains the listed filenames exactly.
155
+ *
156
+ * Example #1: Then the downloaded file should be a zip archive containing the following files named:
157
+ * | report.pdf |
158
+ * | summary.csv |
159
+ * Example #2: Then the downloaded file should be a zip archive containing the following files named:
160
+ * | a.txt | b.txt |
161
+ * Example #3: Then the downloaded file should be a zip archive containing the following files named:
162
+ * | data.json |
163
+ * Example #4: Then the downloaded file should be a zip archive containing the following files named:
164
+ * | logo.svg | favicon.ico |
165
+ * Example #5: Then the downloaded file should be a zip archive containing the following files named:
166
+ * | manifest.json |
167
+ *
168
+ */
169
+ Then('the downloaded file should be a zip archive containing the following files named:', async function () {
170
+ return 'pending';
171
+ });
172
+
173
+ /**
174
+ * (Pending) Assert the downloaded ZIP contains files whose names contain the listed substrings.
175
+ *
176
+ * Example #1: Then the downloaded file should be a zip archive containing the following files partially named:
177
+ * | report |
178
+ * | csv |
179
+ * Example #2: Then the downloaded file should be a zip archive containing the following files partially named:
180
+ * | 2026 |
181
+ * Example #3: Then the downloaded file should be a zip archive containing the following files partially named:
182
+ * | json |
183
+ * Example #4: Then the downloaded file should be a zip archive containing the following files partially named:
184
+ * | logo | favicon |
185
+ * Example #5: Then the downloaded file should be a zip archive containing the following files partially named:
186
+ * | manifest |
187
+ *
188
+ */
189
+ Then('the downloaded file should be a zip archive containing the following files partially named:', async function () {
190
+ return 'pending';
191
+ });
192
+
193
+ /**
194
+ * (Pending) Assert the downloaded ZIP does NOT contain files whose names contain the listed substrings.
195
+ *
196
+ * Example #1: Then the downloaded file should be a zip archive not containing the following files partially named:
197
+ * | secret |
198
+ * | .env |
199
+ * Example #2: Then the downloaded file should be a zip archive not containing the following files partially named:
200
+ * | private |
201
+ * Example #3: Then the downloaded file should be a zip archive not containing the following files partially named:
202
+ * | tmp |
203
+ * Example #4: Then the downloaded file should be a zip archive not containing the following files partially named:
204
+ * | DS_Store |
205
+ * Example #5: Then the downloaded file should be a zip archive not containing the following files partially named:
206
+ * | __MACOSX |
207
+ *
208
+ */
209
+ Then('the downloaded file should be a zip archive not containing the following files partially named:', async function () {
210
+ return 'pending';
211
+ });
@@ -0,0 +1,333 @@
1
+ 'use strict';
2
+
3
+ // All form interaction step definitions live here:
4
+ // fill in (label, attribute, table), select / additionally select,
5
+ // check / uncheck, radio buttons.
6
+
7
+ const { When } = require('@cucumber/cucumber');
8
+ const { buildSelector, fillField, friendly } = require('./varbase-e2e');
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Fill — by label, attribute, table, reverse syntax
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Fill an input field located by its label, placeholder, or name.
16
+ *
17
+ * Example #1: When I fill in "Username" with "John Smith"
18
+ * Example #2: When I fill in "Email" with "jon@example.com"
19
+ * Example #3: And we fill in "Organization" with "Vardot"
20
+ * Example #4: And I fill in "Password" with "1234"
21
+ *
22
+ */
23
+ When(/^(I |we )*fill in "([^"]*)?" with "([^"]*)?"$/, async function (pronounCase, field, value) {
24
+ await fillField(this.page, field, value);
25
+ });
26
+
27
+ /**
28
+ * Fill an input field located by its attribute.
29
+ *
30
+ * Example #1: When I fill in "#uname" with "John Smith" by attr
31
+ * Example #2: When I fill in "uname" with "John Smith" by attr
32
+ * Example #3: And I fill in "pwordcss" with "1234" by "class" attr
33
+ * Example #4: And I fill in "Your full name" with "John Smith" by its "placeholder" attribute
34
+ *
35
+ */
36
+ When(/^(I |we )*fill in "([^"]*)?" with "([^"]*)?" by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (pronounCase, attrValue, txtValue, itsCase, attr, attrWord) {
37
+ await this.page.locator(buildSelector(attrValue, attr)).first().fill(txtValue);
38
+ });
39
+
40
+ /**
41
+ * Clear an input field located by its label.
42
+ *
43
+ * Example #1: When I fill in "Username" with:
44
+ * Example #2: And we fill in "Email" with:
45
+ * Example #3: And I fill in "Password" with:
46
+ *
47
+ */
48
+ When(/^(I |we )*fill in "([^"]*)?" with:$/, async function (pronounCase, field) {
49
+ await fillField(this.page, field, '');
50
+ });
51
+
52
+ /**
53
+ * Clear an input field located by its attribute.
54
+ *
55
+ * Example #1: When I fill in "#uname" with: by attr
56
+ * Example #2: When I fill in "uname" with: by attr
57
+ * Example #3: And I fill in "pwordcss" with: by "class" attr
58
+ * Example #4: And I fill in "Your full name" with: by its "placeholder" attribute
59
+ *
60
+ */
61
+ When(/^(I |we )*fill in "([^"]*)?" with: by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (pronounCase, attrValue, itsCase, attr, attrWord) {
62
+ await this.page.locator(buildSelector(attrValue, attr)).first().fill('');
63
+ });
64
+
65
+ /**
66
+ * Fill an input field located by label (reverse syntax).
67
+ *
68
+ * Example #1: When I fill in "jon-smith" for "Username"
69
+ * Example #2: When we fill in "Testing" for "Organization options"
70
+ * Example #3: And I fill in "1234" for "Password"
71
+ *
72
+ */
73
+ When(/^(I |we )*fill in "([^"]*)?" for "([^"]*)?"$/, async function (pronounCase, value, field) {
74
+ await fillField(this.page, field, value);
75
+ });
76
+
77
+ /**
78
+ * Fill an input field located by attribute (reverse syntax).
79
+ *
80
+ * Example #1: When I fill in "John Smith" for "#uname" by attr
81
+ * Example #2: When I fill in "John Smith" for "uname" by attr
82
+ * Example #3: And I fill in "1234" for "password" by "class" attr
83
+ * Example #4: And I fill in "John Smith" for "Your full name" by its "placeholder" attribute
84
+ *
85
+ */
86
+ When(/^(I |we )*fill in "([^"]*)?" for "([^"]*)?" by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (pronounCase, txtValue, attrValue, itsCase, attr, attrWord) {
87
+ await this.page.locator(buildSelector(attrValue, attr)).first().fill(txtValue);
88
+ });
89
+
90
+ /**
91
+ * Fills multiple form fields from a data table, located by their labels.
92
+ *
93
+ * Each row is `| label | value |`. Labels resolve via `<label for=...>`,
94
+ * `placeholder`, or `[name]` — whichever matches first.
95
+ *
96
+ * Example #1: When I fill in the following:
97
+ * | Username | vardot |
98
+ * | Password | s3cret |
99
+ * Example #2: When we fill in the following:
100
+ * | Email | jon@example.com |
101
+ * | Organization | Vardot |
102
+ * Example #3: And I fill in the following:
103
+ * | First name | Alice |
104
+ * | Last name | Smith |
105
+ * | Country | Jordan |
106
+ * Example #4: When I fill in the following:
107
+ * | Subject | Bug report |
108
+ * | Message | The submit button is hidden |
109
+ * Example #5: When I fill in the following:
110
+ * | Card number | 4242 4242 4242 4242 |
111
+ * | Expiry | 12/30 |
112
+ * | CVV | 123 |
113
+ *
114
+ */
115
+ When(/^(I |we )*fill in( the)* following:$/, async function (pronounCase, theCase, table) {
116
+ for (const [field, value] of table.raw()) {
117
+ await fillField(this.page, field, value);
118
+ }
119
+ });
120
+
121
+ /**
122
+ * Fills multiple form fields from a data table, located by attribute.
123
+ *
124
+ * Without an explicit attribute, each key is matched against id / class /
125
+ * name / data-testid / data-test-id / data-test / data-cy / aria-label /
126
+ * value / placeholder / title.
127
+ *
128
+ * Example #1: When I fill in the following: by attr
129
+ * | #uname | John Smith |
130
+ * | password | s3cret |
131
+ * Example #2: When I fill in the following: by its "placeholder" attribute
132
+ * | Your full name | John Smith |
133
+ * | Your Password | s3cret |
134
+ * Example #3: When I fill in the following: by attribute
135
+ * | [data-testid=email] | a@b.c |
136
+ * | [data-testid=username] | alice |
137
+ * Example #4: When we fill in the following: by attr
138
+ * | first-name | Alice |
139
+ * | last-name | Smith |
140
+ * Example #5: When I fill in the following: by its "name" attribute
141
+ * | card_number | 4242 4242 4242 4242 |
142
+ * | exp_date | 12/30 |
143
+ *
144
+ */
145
+ When(/^(I |we )*fill in( the)* following: by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (pronounCase, theCase, itsCase, attr, attrWord, table) {
146
+ for (const [attrValue, value] of table.raw()) {
147
+ await this.page.locator(buildSelector(attrValue, attr)).first().fill(value);
148
+ }
149
+ });
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Select dropdown
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /**
156
+ * Select an option from a dropdown by label, id, class, or name.
157
+ *
158
+ * Example #1: When I select "Mercedes" from "Cars"
159
+ * Example #2: When I select "saab" from "#cars"
160
+ * Example #3: When I select "Mercedes" from "cars"
161
+ * Example #4: And we select "English" from "Language"
162
+ *
163
+ */
164
+ When(/^(I |we )*select "([^"]*)?" from "([^"]*)?"$/, async function (pronounCase, option, selectList) {
165
+ let loc;
166
+ if (selectList.startsWith('#') || selectList.startsWith('.')) {
167
+ loc = this.page.locator(selectList);
168
+ } else if (!selectList.includes(' ')) {
169
+ loc = this.page.locator(`[name="${selectList}"], #${selectList}`).first();
170
+ } else {
171
+ loc = this.page.getByLabel(selectList, { exact: true });
172
+ }
173
+ try {
174
+ await loc.selectOption({ label: option }, { timeout: 3000 });
175
+ return;
176
+ } catch { /* fall through to value-based selection */ }
177
+ try {
178
+ await loc.selectOption(option);
179
+ } catch (e) {
180
+ throw friendly(
181
+ `Could not select "${option}" from "${selectList}".\n` +
182
+ ` ${(e.message || '').split('\n')[0]}\n` +
183
+ ` Hints:\n` +
184
+ ` - Confirm the option text matches exactly (case-sensitive).\n` +
185
+ ` - For value-based selection pass the option's "value" attribute.\n` +
186
+ ` - For multi-select, use "additionally select" instead.`
187
+ );
188
+ }
189
+ });
190
+
191
+ /**
192
+ * Add an option to a <select multiple> without clearing the existing selection.
193
+ * Resolves the select by label, then by [name]/#id fallback.
194
+ *
195
+ * Example #1: When I additionally select "Red" from "Colors"
196
+ * Example #2: When I additionally select "Blue" from "Colors"
197
+ * Example #3: When we additionally select "Green" from "Colors"
198
+ * Example #4: And additionally select "Yellow" from "Colors"
199
+ * Example #5: When I additionally select "Admin" from "Roles"
200
+ * Example #6: When we additionally select "Editor" from "Roles"
201
+ * Example #7: When I additionally select "Viewer" from "Roles"
202
+ * Example #8: And I additionally select "EN" from "Languages"
203
+ * Example #9: When I additionally select "FR" from "Languages"
204
+ * Example #10: When we additionally select "ES" from "Languages"
205
+ *
206
+ * Advanced:
207
+ * Example #11: Build a full multi-selection in sequence:
208
+ * When I select "Red" from "Colors"
209
+ * And I additionally select "Blue" from "Colors"
210
+ * And I additionally select "Green" from "Colors"
211
+ * Example #12: Works against a [name] select:
212
+ * When I additionally select "tag-a" from "tags"
213
+ * And I additionally select "tag-b" from "tags"
214
+ * Example #13: Confirm the select stays multi-valued after adds:
215
+ * When I select "One" from "Items"
216
+ * And I additionally select "Two" from "Items"
217
+ */
218
+ When(/^(I |we )*additionally select "([^"]*)" from "([^"]*)"$/, async function (pronounCase, option, select) {
219
+ const loc = this.page.getByLabel(select).or(
220
+ this.page.locator(`select[name="${select}"], select#${select}`)
221
+ ).first();
222
+
223
+ const currentValues = await loc.evaluate((el) => {
224
+ if (!el || !el.selectedOptions) return [];
225
+ return Array.from(el.selectedOptions).map((o) => o.value);
226
+ });
227
+
228
+ const nextValueForLabel = await loc.evaluate((el, label) => {
229
+ const opt = Array.from(el.options || []).find((o) => o.label === label || o.text === label);
230
+ return opt ? opt.value : null;
231
+ }, option);
232
+
233
+ if (nextValueForLabel === null) {
234
+ throw friendly(`Option "${option}" not found in select "${select}".`);
235
+ }
236
+
237
+ const combined = Array.from(new Set([...currentValues, nextValueForLabel]));
238
+ await loc.selectOption(combined);
239
+ });
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Checkbox / radio
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * Checks the specified checkbox by label, id, class, or name.
247
+ *
248
+ * Example #1: When I check "Remember me"
249
+ * Example #2: When we check "Put site into maintenance mode"
250
+ * Example #3: And I check "#newsletter"
251
+ * Example #4: And we check ".terms-and-conditions"
252
+ *
253
+ */
254
+ When(/^(I |we )*check "([^"]*)?"$/, async function (pronounCase, item) {
255
+ try {
256
+ if (item.startsWith('#') || item.startsWith('.')) {
257
+ await this.page.locator(item).check();
258
+ } else {
259
+ const byLabel = this.page.getByLabel(item, { exact: true });
260
+ if (await byLabel.count() > 0) {
261
+ await byLabel.check();
262
+ } else {
263
+ await this.page.locator(`input[type="checkbox"][id="${item}"], input[type="checkbox"][name="${item}"], input[type="checkbox"][value="${item}"]`).first().check();
264
+ }
265
+ }
266
+ } catch (e) {
267
+ throw friendly(
268
+ `Could not check "${item}".\n` +
269
+ ` ${(e.message || '').split('\n')[0]}\n` +
270
+ ` Hints: confirm the label / id / class matches; ensure the input is visible and not disabled.`
271
+ );
272
+ }
273
+ });
274
+
275
+ /**
276
+ * Unchecks the specified checkbox by label, id, class, or name.
277
+ *
278
+ * Example #1: When I uncheck "Remember me"
279
+ * Example #2: When we uncheck "Put site into maintenance mode"
280
+ * Example #3: And I uncheck "#newsletter"
281
+ * Example #4: And we uncheck ".terms-and-conditions"
282
+ *
283
+ */
284
+ When(/^(I |we )*uncheck "([^"]*)?"$/, async function (pronounCase, item) {
285
+ try {
286
+ if (item.startsWith('#') || item.startsWith('.')) {
287
+ await this.page.locator(item).uncheck();
288
+ } else {
289
+ const byLabel = this.page.getByLabel(item, { exact: true });
290
+ if (await byLabel.count() > 0) {
291
+ await byLabel.uncheck();
292
+ } else {
293
+ await this.page.locator(`input[type="checkbox"][id="${item}"], input[type="checkbox"][name="${item}"], input[type="checkbox"][value="${item}"]`).first().uncheck();
294
+ }
295
+ }
296
+ } catch (e) {
297
+ throw friendly(
298
+ `Could not uncheck "${item}".\n` +
299
+ ` ${(e.message || '').split('\n')[0]}\n` +
300
+ ` Hints: confirm the label / id / class matches; ensure the input is visible and not disabled.`
301
+ );
302
+ }
303
+ });
304
+
305
+ /**
306
+ * Selects a radio button by label, value, id, or class.
307
+ *
308
+ * Example #1: When I select radio button "Male"
309
+ * Example #2: When I select radio button "female"
310
+ * Example #3: When I select radio button "#gender-male"
311
+ * Example #4: When we select radio button "option1"
312
+ *
313
+ */
314
+ When(/^(I |we )*select radio button "([^"]*)?"$/, async function (pronounCase, item) {
315
+ try {
316
+ if (item.startsWith('#') || item.startsWith('.')) {
317
+ await this.page.locator(item).check();
318
+ } else {
319
+ const byValue = this.page.locator(`input[type="radio"][value="${item}"]`);
320
+ if (await byValue.count() > 0) {
321
+ await byValue.first().check();
322
+ } else {
323
+ await this.page.getByLabel(item, { exact: true }).check();
324
+ }
325
+ }
326
+ } catch (e) {
327
+ throw friendly(
328
+ `Could not select radio button "${item}".\n` +
329
+ ` ${(e.message || '').split('\n')[0]}\n` +
330
+ ` Hints: try matching by [value=...], by label text, or by #id / .class.`
331
+ );
332
+ }
333
+ });