@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,423 @@
1
+ 'use strict';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Varbase E2E — Screenshot step definitions
5
+ //
6
+ // Saves PNG (and matching .html) screenshots to a configurable directory.
7
+ // Supports manual steps, automatic capture on failed step, and per-step
8
+ // capture for scenarios tagged @screenshots.
9
+ //
10
+ // Configuration: resolved per-scenario with this priority:
11
+ // 1. process.env.VARBASE_E2E_SCREENSHOT_* (CI / shell — highest)
12
+ // 2. cucumber.js worldParameters.screenshot.* (project-level defaults)
13
+ // 3. built-in defaults
14
+ //
15
+ // Supported env vars / worldParameters keys:
16
+ // VARBASE_E2E_SCREENSHOT_DIR dir default "./screenshots"
17
+ // VARBASE_E2E_SCREENSHOT_PURGE purge "true"/"false" — default false
18
+ // VARBASE_E2E_SCREENSHOT_ON_FAILED onFailed default true
19
+ // VARBASE_E2E_SCREENSHOT_ON_EVERY_STEP onEveryStep default false
20
+ // VARBASE_E2E_SCREENSHOT_FULLSCREEN alwaysFullscreen default false
21
+ // VARBASE_E2E_SCREENSHOT_FAILED_PREFIX failedPrefix default "failed_"
22
+ // VARBASE_E2E_SCREENSHOT_PATTERN filenamePattern default "{datetime}.{feature_file}.feature_{step_line}.{ext}"
23
+ // VARBASE_E2E_SCREENSHOT_PATTERN_FAIL filenamePatternFailed default "{failed_prefix}{datetime}.{feature_file}.feature_{step_line}.{ext}"
24
+ // VARBASE_E2E_SCREENSHOT_INFO_TYPES infoTypes comma list: url,feature,step,datetime
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const { When, Then, Before, After, AfterStep, BeforeAll } = require('@cucumber/cucumber');
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+ const { pad } = require('./varbase-e2e');
31
+
32
+ function pick(envVal, paramVal, fallback) {
33
+ if (envVal !== undefined && envVal !== '') return envVal;
34
+ if (paramVal !== undefined && paramVal !== '' && paramVal !== null) return paramVal;
35
+ return fallback;
36
+ }
37
+
38
+ function asBool(v, fallback) {
39
+ if (v === undefined || v === null || v === '') return fallback;
40
+ if (typeof v === 'boolean') return v;
41
+ const s = String(v).toLowerCase();
42
+ return s === '1' || s === 'true' || s === 'yes';
43
+ }
44
+
45
+ function resolveConfig(parameters) {
46
+ const p = (parameters && parameters.screenshot) || {};
47
+ return {
48
+ dir: pick(process.env.VARBASE_E2E_SCREENSHOT_DIR, p.dir, './screenshots'),
49
+ purge: asBool(pick(process.env.VARBASE_E2E_SCREENSHOT_PURGE, p.purge, undefined), false),
50
+ onFailed: asBool(pick(process.env.VARBASE_E2E_SCREENSHOT_ON_FAILED, p.onFailed, undefined), true),
51
+ onEveryStep: asBool(pick(process.env.VARBASE_E2E_SCREENSHOT_ON_EVERY_STEP, p.onEveryStep, undefined), false),
52
+ alwaysFullscreen: asBool(pick(process.env.VARBASE_E2E_SCREENSHOT_FULLSCREEN, p.alwaysFullscreen, undefined), false),
53
+ failedPrefix: pick(process.env.VARBASE_E2E_SCREENSHOT_FAILED_PREFIX, p.failedPrefix, 'failed_'),
54
+ filenamePattern: pick(
55
+ process.env.VARBASE_E2E_SCREENSHOT_PATTERN,
56
+ p.filenamePattern,
57
+ '{datetime}.{feature_file}.feature_{step_line}.{ext}'
58
+ ),
59
+ filenamePatternFailed: pick(
60
+ process.env.VARBASE_E2E_SCREENSHOT_PATTERN_FAIL,
61
+ p.filenamePatternFailed,
62
+ '{failed_prefix}{datetime}.{feature_file}.feature_{step_line}.{ext}'
63
+ ),
64
+ infoTypes: String(pick(process.env.VARBASE_E2E_SCREENSHOT_INFO_TYPES, p.infoTypes, ''))
65
+ .split(',')
66
+ .map(s => s.trim())
67
+ .filter(Boolean),
68
+ };
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Filename tokenizer
73
+ // ---------------------------------------------------------------------------
74
+ function formatDate(d, format) {
75
+ const yyyy = d.getFullYear();
76
+ const MM = pad(d.getMonth() + 1, 2);
77
+ const dd = pad(d.getDate(), 2);
78
+ const HH = pad(d.getHours(), 2);
79
+ const mm = pad(d.getMinutes(), 2);
80
+ const ss = pad(d.getSeconds(), 2);
81
+ if (!format || format === 'Ymd_His') return `${yyyy}${MM}${dd}_${HH}${mm}${ss}`;
82
+ return format
83
+ .replace(/YYYY/g, yyyy)
84
+ .replace(/MM/g, MM)
85
+ .replace(/DD/g, dd)
86
+ .replace(/HH/g, HH)
87
+ .replace(/mm/g, mm)
88
+ .replace(/ss/g, ss);
89
+ }
90
+
91
+ function urlToken(url, qualifier) {
92
+ if (!url) return '';
93
+ let parsed;
94
+ try { parsed = new URL(url); } catch { return url; }
95
+ let out;
96
+ switch (qualifier) {
97
+ case 'origin': out = `${parsed.protocol}//${parsed.host}`; break;
98
+ case 'domain': out = parsed.host; break;
99
+ case 'path': out = parsed.pathname.replace(/^\/+|\/+$/g, ''); break;
100
+ case 'query': out = parsed.search.replace(/^\?/, ''); break;
101
+ case 'fragment': out = parsed.hash.replace(/^#/, ''); break;
102
+ case 'relative':
103
+ out = parsed.pathname.replace(/^\/+|\/+$/g, '') + parsed.search + parsed.hash;
104
+ break;
105
+ default: out = url;
106
+ }
107
+ return out.replace(/[^\w\-]+/g, '_');
108
+ }
109
+
110
+ function replaceTokens(pattern, data) {
111
+ return pattern.replace(/\{([^}]+)\}/g, (match, raw) => {
112
+ const [head, format] = raw.split(':');
113
+ const parts = head.split('_');
114
+ const name = parts.shift();
115
+ const qualifier = parts.join('_') || null;
116
+
117
+ if (name === 'ext') return data.ext || 'html';
118
+ if (name === 'failed' && qualifier === 'prefix') return data.failed_prefix || match;
119
+ if (name === 'feature' && qualifier === 'file') {
120
+ return data.feature_file ? path.basename(data.feature_file, '.feature') : match;
121
+ }
122
+ if (name === 'step') {
123
+ if (qualifier === 'line') {
124
+ const n = Number(data.step_line);
125
+ if (Number.isNaN(n)) return match;
126
+ // sprintf-like: {step_line:%03d}
127
+ if (format && /^%\d*d$/.test(format)) {
128
+ const w = parseInt(format.match(/\d+/)?.[0] || '0', 10);
129
+ return pad(n, w);
130
+ }
131
+ return String(n);
132
+ }
133
+ if (qualifier === 'name' && data.step_name) {
134
+ return String(data.step_name).replace(/[ "]+/g, c => (c === '"' ? '' : '_'));
135
+ }
136
+ return match;
137
+ }
138
+ if (name === 'url') return urlToken(data.url, qualifier);
139
+ if (name === 'datetime') return data.timestamp ? formatDate(new Date(data.timestamp), format) : match;
140
+ return match;
141
+ });
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // State tracked per-scenario
146
+ // ---------------------------------------------------------------------------
147
+ let purgedOnce = false;
148
+
149
+ function ensureDir(dir) {
150
+ fs.mkdirSync(dir, { recursive: true });
151
+ }
152
+
153
+ function purgeDir(dir) {
154
+ if (!fs.existsSync(dir)) return;
155
+ for (const entry of fs.readdirSync(dir)) {
156
+ const full = path.join(dir, entry);
157
+ const stat = fs.lstatSync(full);
158
+ if (stat.isDirectory()) fs.rmSync(full, { recursive: true, force: true });
159
+ else fs.unlinkSync(full);
160
+ }
161
+ }
162
+
163
+ function buildInfo(cfg, ctx) {
164
+ const lines = [];
165
+ for (const type of cfg.infoTypes) {
166
+ if (type === 'url' && ctx.url) lines.push(`Current URL: ${ctx.url}`);
167
+ if (type === 'feature' && ctx.featureTitle) lines.push(`Feature: ${ctx.featureTitle}`);
168
+ if (type === 'step' && ctx.stepText) lines.push(`Step: ${ctx.stepText} (line ${ctx.stepLine || '?'})`);
169
+ if (type === 'datetime') lines.push(`Datetime: ${formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')}`);
170
+ }
171
+ return lines.join('\n');
172
+ }
173
+
174
+ function makeFileName(cfg, ctx, ext, explicitName, isFailed) {
175
+ let pattern;
176
+ if (isFailed) pattern = cfg.filenamePatternFailed;
177
+ else if (explicitName) pattern = explicitName;
178
+ else pattern = cfg.filenamePattern;
179
+
180
+ if (!pattern.endsWith('.{ext}')) pattern += '.{ext}';
181
+
182
+ return replaceTokens(pattern, {
183
+ ext,
184
+ failed_prefix: cfg.failedPrefix,
185
+ feature_file: ctx.featureFile,
186
+ step_line: ctx.stepLine,
187
+ step_name: ctx.stepText,
188
+ timestamp: Date.now(),
189
+ url: ctx.url,
190
+ });
191
+ }
192
+
193
+ async function captureScreenshot(world, options = {}) {
194
+ const cfg = resolveConfig(world.parameters);
195
+ if (!world.page) return;
196
+
197
+ const fullscreen = Boolean(options.fullscreen || cfg.alwaysFullscreen);
198
+ const ctx = world.__screenshotCtx || {};
199
+
200
+ let url = '';
201
+ try { url = world.page.url(); } catch { /* page closed */ }
202
+ ctx.url = url;
203
+
204
+ ensureDir(cfg.dir);
205
+
206
+ // HTML capture (works for all drivers, Playwright content()).
207
+ try {
208
+ let html = await world.page.content();
209
+ const info = buildInfo(cfg, ctx);
210
+ if (info) html = info.replace(/\n/g, '<br/>\n') + '<hr/>\n' + html;
211
+ const htmlName = makeFileName(cfg, ctx, 'html', options.filename, options.isFailed);
212
+ fs.writeFileSync(path.join(cfg.dir, htmlName), html);
213
+ } catch { /* no content yet */ }
214
+
215
+ // PNG capture.
216
+ try {
217
+ const pngName = makeFileName(cfg, ctx, 'png', options.filename, options.isFailed);
218
+ await world.page.screenshot({
219
+ path: path.join(cfg.dir, pngName),
220
+ fullPage: fullscreen,
221
+ });
222
+ } catch { /* driver without screenshot support */ }
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Hooks
227
+ // ---------------------------------------------------------------------------
228
+ BeforeAll(function () {
229
+ const cfg = resolveConfig(this && this.parameters);
230
+ if (cfg.purge && !purgedOnce) {
231
+ purgeDir(cfg.dir);
232
+ purgedOnce = true;
233
+ }
234
+ });
235
+
236
+ Before(function (scenario) {
237
+ const pickledTags = (scenario.pickle && scenario.pickle.tags) || [];
238
+ this.__screenshotCtx = {
239
+ hasScreenshotsTag: pickledTags.some(t => t.name === '@screenshots'),
240
+ featureFile: (scenario.gherkinDocument && scenario.gherkinDocument.uri) || '',
241
+ featureTitle: (scenario.gherkinDocument && scenario.gherkinDocument.feature && scenario.gherkinDocument.feature.name) || '',
242
+ stepText: '',
243
+ stepLine: 0,
244
+ url: '',
245
+ };
246
+ });
247
+
248
+ AfterStep(async function (step) {
249
+ if (!this.__screenshotCtx) return;
250
+ this.__screenshotCtx.stepText = (step.pickleStep && step.pickleStep.text) || '';
251
+ this.__screenshotCtx.stepLine = step.pickleStep && step.pickleStep.astNodeIds ? 0 : 0;
252
+
253
+ if (step.gherkinDocument && step.pickleStep) {
254
+ const astIds = step.pickleStep.astNodeIds || [];
255
+ const steps = [];
256
+ (step.gherkinDocument.feature.children || []).forEach(child => {
257
+ const container = child.scenario || child.background;
258
+ if (container && container.steps) steps.push(...container.steps);
259
+ });
260
+ const match = steps.find(s => astIds.includes(s.id));
261
+ if (match && match.location) this.__screenshotCtx.stepLine = match.location.line;
262
+ }
263
+
264
+ const cfg = resolveConfig(this.parameters);
265
+ const passed = step.result && step.result.status === 'PASSED';
266
+
267
+ if (!passed && cfg.onFailed) {
268
+ await captureScreenshot(this, { isFailed: true, fullscreen: cfg.alwaysFullscreen });
269
+ return;
270
+ }
271
+ if (passed && (cfg.onEveryStep || this.__screenshotCtx.hasScreenshotsTag)) {
272
+ await captureScreenshot(this, { fullscreen: cfg.alwaysFullscreen });
273
+ }
274
+ });
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Step definitions
278
+ // ---------------------------------------------------------------------------
279
+
280
+ /**
281
+ * Save a screenshot at the current viewport size.
282
+ *
283
+ * Example #1: Then I save screenshot
284
+ * Example #2: When I save screenshot
285
+ * Example #3: Then save screenshot
286
+ * Example #4: Given I am on "/about-us"
287
+ * Then I save screenshot
288
+ * Example #5: When I go to homepage
289
+ * Then I save screenshot
290
+ * Example #6: Given I am on "/news"
291
+ * When I scroll to "#latest"
292
+ * Then I save screenshot
293
+ * Example #7: Given I am on "/contact"
294
+ * When I fill in "email" with "info@vardot.com"
295
+ * Then I save screenshot
296
+ * Example #8: Given I am on "/search?q=UN"
297
+ * Then I save screenshot
298
+ * Example #9: Then I reload page
299
+ * Then I save screenshot
300
+ * Example #10: Given I am on "/partners/un.org"
301
+ * Then I save screenshot
302
+ */
303
+ When(/^(I |we )*save screenshot$/, async function (pronounCase) {
304
+ await captureScreenshot(this, {});
305
+ });
306
+
307
+ /**
308
+ * Save a full-page screenshot (entire scrollable area).
309
+ *
310
+ * Example #1: Then I save fullscreen screenshot
311
+ * Example #2: When I save fullscreen screenshot
312
+ * Example #3: Then save fullscreen screenshot
313
+ * Example #4: Given I am on "/news"
314
+ * Then I save fullscreen screenshot
315
+ * Example #5: Given I am on "/about-us"
316
+ * Then I save fullscreen screenshot
317
+ * Example #6: Given I am on homepage
318
+ * Then I save fullscreen screenshot
319
+ * Example #7: Given I am on "/partners"
320
+ * When I wait 2 seconds
321
+ * Then I save fullscreen screenshot
322
+ * Example #8: Given I am on "/varbase-e2e"
323
+ * Then I save fullscreen screenshot
324
+ * Example #9: Given I am on "/un.org/reports"
325
+ * Then I save fullscreen screenshot
326
+ * Example #10: Given I am on "/long-article"
327
+ * Then I save fullscreen screenshot
328
+ */
329
+ When(/^(I |we )*save fullscreen screenshot$/, async function (pronounCase) {
330
+ await captureScreenshot(this, { fullscreen: true });
331
+ });
332
+
333
+ /**
334
+ * Resize the viewport to <width> x <height> then save a screenshot.
335
+ *
336
+ * Example #1: Then I save 1440 x 900 screenshot
337
+ * Example #2: Then I save 1200 x 800 screenshot
338
+ * Example #3: Then I save 375 x 667 screenshot
339
+ * Example #4: Then I save 768 x 1024 screenshot
340
+ * Example #5: Then I save 1920 x 1080 screenshot
341
+ * Example #6: Given I am on "/about-us"
342
+ * Then I save 1440 x 900 screenshot
343
+ * Example #7: Given I am on homepage
344
+ * Then I save 375 x 812 screenshot
345
+ * Example #8: Given I am on "/news"
346
+ * Then I save 1024 x 768 screenshot
347
+ * Example #9: Given I am on "/about"
348
+ * Then I save 1366 x 768 screenshot
349
+ * Example #10: Given I am on "/un.org"
350
+ * Then I save 414 x 896 screenshot
351
+ */
352
+ When(/^(I |we )*save (\d+) x (\d+) screenshot$/, async function (pronounCase, width, height) {
353
+ await this.page.setViewportSize({ width: parseInt(width, 10), height: parseInt(height, 10) });
354
+ await captureScreenshot(this, {});
355
+ });
356
+
357
+ /**
358
+ * Resize the viewport to <width> x <height> then save a full-page screenshot.
359
+ *
360
+ * Example #1: Then I save fullscreen 1440 x 900 screenshot
361
+ * Example #2: Then I save fullscreen 1200 x 800 screenshot
362
+ * Example #3: Then I save fullscreen 375 x 667 screenshot
363
+ * Example #4: Then I save fullscreen 768 x 1024 screenshot
364
+ * Example #5: Then I save fullscreen 1920 x 1080 screenshot
365
+ * Example #6: Given I am on "/about-us"
366
+ * Then I save fullscreen 1440 x 900 screenshot
367
+ * Example #7: Given I am on homepage
368
+ * Then I save fullscreen 375 x 812 screenshot
369
+ * Example #8: Given I am on "/about"
370
+ * Then I save fullscreen 1366 x 768 screenshot
371
+ * Example #9: Given I am on "/un.org"
372
+ * Then I save fullscreen 414 x 896 screenshot
373
+ * Example #10: Given I am on "/long-article"
374
+ * Then I save fullscreen 1440 x 900 screenshot
375
+ */
376
+ When(/^(I |we )*save fullscreen (\d+) x (\d+) screenshot$/, async function (pronounCase, width, height) {
377
+ await this.page.setViewportSize({ width: parseInt(width, 10), height: parseInt(height, 10) });
378
+ await captureScreenshot(this, { fullscreen: true });
379
+ });
380
+
381
+ /**
382
+ * Save a screenshot using an explicit filename (tokens supported).
383
+ *
384
+ * Example #1: Then I save screenshot with name "homepage.png"
385
+ * Example #2: Then I save screenshot with name "varbase-e2e-home"
386
+ * Example #3: Then I save screenshot with name "un-landing-{datetime}.png"
387
+ * Example #4: Then I save screenshot with name "about-us.png"
388
+ * Example #5: Then I save screenshot with name "{feature_file}_{step_line}"
389
+ * Example #6: Then I save screenshot with name "{url_path}.png"
390
+ * Example #7: Given I am on "/news"
391
+ * Then I save screenshot with name "news-latest.png"
392
+ * Example #8: Given I am on "/about"
393
+ * Then I save screenshot with name "varbase-e2e-index.png"
394
+ * Example #9: Given I am on "/un.org"
395
+ * Then I save screenshot with name "un-home.png"
396
+ * Example #10: Then I save screenshot with name "contact-form-before-submit.png"
397
+ */
398
+ When(/^(I |we )*save screenshot with name "([^"]*)"$/, async function (pronounCase, filename) {
399
+ await captureScreenshot(this, { filename });
400
+ });
401
+
402
+ /**
403
+ * Save a full-page screenshot using an explicit filename (tokens supported).
404
+ *
405
+ * Example #1: Then I save fullscreen screenshot with name "homepage-full.png"
406
+ * Example #2: Then I save fullscreen screenshot with name "varbase-e2e-home-full"
407
+ * Example #3: Then I save fullscreen screenshot with name "news-{datetime}.png"
408
+ * Example #4: Then I save fullscreen screenshot with name "about-us-full.png"
409
+ * Example #5: Then I save fullscreen screenshot with name "{feature_file}_full.png"
410
+ * Example #6: Then I save fullscreen screenshot with name "{url_path}-full.png"
411
+ * Example #7: Given I am on "/news"
412
+ * Then I save fullscreen screenshot with name "news-full.png"
413
+ * Example #8: Given I am on "/about"
414
+ * Then I save fullscreen screenshot with name "varbase-e2e-full.png"
415
+ * Example #9: Given I am on "/un.org"
416
+ * Then I save fullscreen screenshot with name "un-home-full.png"
417
+ * Example #10: Then I save fullscreen screenshot with name "long-article-full.png"
418
+ */
419
+ When(/^(I |we )*save fullscreen screenshot with name "([^"]*)"$/, async function (pronounCase, filename) {
420
+ await captureScreenshot(this, { filename, fullscreen: true });
421
+ });
422
+
423
+ module.exports = { resolveConfig, replaceTokens, makeFileName };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ // All scroll step definitions live in this file.
4
+ //
5
+ // Vertical: down / up / to top / to bottom (page or scoped element).
6
+ // Horizontal: right / left / to start / to end (page or scoped element).
7
+ //
8
+ // Default delta when no number is supplied is 350 px.
9
+
10
+ const { When } = require('@cucumber/cucumber');
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Vertical scrolling — page
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Scrolls the page down by a custom number of pixels (default 350).
18
+ *
19
+ * Example #1: And I scroll down
20
+ * Example #2: When I scroll down 800
21
+ * Example #3: And we scroll down 500
22
+ * Example #4: When scrolling down 1200
23
+ *
24
+ */
25
+ When(/^(I scroll|we scroll|scrolling)? down(?: (\d+))?$/, async function (pronounCase, numValue) {
26
+ await this.page.evaluate((v) => window.scrollBy(0, v), numValue ? parseInt(numValue, 10) : 350);
27
+ });
28
+
29
+ /**
30
+ * Scrolls the page up by a custom number of pixels (default 350).
31
+ *
32
+ * Example #1: And I scroll up
33
+ * Example #2: When I scroll up 1000
34
+ * Example #3: And we scroll up 300
35
+ * Example #4: When scrolling up 750
36
+ *
37
+ */
38
+ When(/^(I scroll|we scroll|scrolling)? up(?: (\d+))?$/, async function (pronounCase, numValue) {
39
+ await this.page.evaluate((v) => window.scrollBy(0, -v), numValue ? parseInt(numValue, 10) : 350);
40
+ });
41
+
42
+ /**
43
+ * Scrolls to the very top of the current page.
44
+ *
45
+ * Example #1: When I scroll to top
46
+ * Example #2: And we scroll to the top
47
+ * Example #3: When scrolling to the top of the page
48
+ *
49
+ */
50
+ When(/^(I scroll|we scroll|scrolling)? to( the)* top( of the page)*$/, async function (pronounCase, theCase, pageCase) {
51
+ await this.page.evaluate(() => window.scrollTo(0, 0));
52
+ });
53
+
54
+ /**
55
+ * Scrolls to the bottom of the current page.
56
+ *
57
+ * Example #1: When I scroll to the bottom
58
+ * Example #2: And we scroll to bottom
59
+ * Example #3: When scrolling to the bottom of the page
60
+ *
61
+ */
62
+ When(/^(I scroll|we scroll|scrolling)? to( the)* bottom( of the page)*$/, async function (pronounCase, theCase, pageCase) {
63
+ await this.page.evaluate(() => {
64
+ window.scrollTo(0, document.body.scrollHeight);
65
+ document.querySelectorAll('*').forEach(el => {
66
+ const s = window.getComputedStyle(el);
67
+ if ((s.overflowY === 'scroll' || s.overflowY === 'auto') && el.scrollHeight > el.clientHeight) {
68
+ el.scrollTop = el.scrollHeight;
69
+ el.dispatchEvent(new Event('scroll', { bubbles: true }));
70
+ }
71
+ });
72
+ });
73
+ });
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Vertical scrolling — scoped to a CSS selector
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /**
80
+ * Scrolls to the top of a specific element identified by a CSS selector.
81
+ *
82
+ * Example #1: When I scroll to top of "#off-canvas"
83
+ * Example #2: And we scroll to top of "#sidebar"
84
+ * Example #3: When scrolling to top of "#main-container"
85
+ *
86
+ */
87
+ When(/^(I scroll|we scroll|scrolling)? to top of "([^"]*)"$/, async function (pronounCase, selector) {
88
+ await this.page.locator(selector).evaluate(el => {
89
+ el.scrollTop = 0;
90
+ el.dispatchEvent(new Event('scroll', { bubbles: true }));
91
+ });
92
+ });
93
+
94
+ /**
95
+ * Scrolls to the bottom of a specific element identified by a CSS selector.
96
+ *
97
+ * Example #1: When I scroll to bottom of "#off-canvas"
98
+ * Example #2: And we scroll to bottom of "#sidebar"
99
+ * Example #3: When scrolling to bottom of "#main-container"
100
+ *
101
+ */
102
+ When(/^(I scroll|we scroll|scrolling)? to bottom of "([^"]*)"$/, async function (pronounCase, selector) {
103
+ await this.page.locator(selector).evaluate(el => {
104
+ el.scrollTop = el.scrollHeight;
105
+ el.dispatchEvent(new Event('scroll', { bubbles: true }));
106
+ });
107
+ });
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Horizontal scrolling — page
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Scrolls the page right by a custom number of pixels (default 350).
115
+ *
116
+ * Example #1: And I scroll right
117
+ * Example #2: When I scroll right 1000
118
+ * Example #3: And we scroll right 300
119
+ * Example #4: When scrolling right 750
120
+ *
121
+ */
122
+ When(/^(I scroll|we scroll|scrolling)? right(?: (\d+))?$/, async function (pronounCase, numValue) {
123
+ await this.page.evaluate((v) => window.scrollBy(v, 0), numValue ? parseInt(numValue, 10) : 350);
124
+ });
125
+
126
+ /**
127
+ * Scrolls the page left by a custom number of pixels (default 350).
128
+ *
129
+ * Example #1: And I scroll left
130
+ * Example #2: When I scroll left 800
131
+ * Example #3: And we scroll left 500
132
+ * Example #4: When scrolling left 1200
133
+ *
134
+ */
135
+ When(/^(I scroll|we scroll|scrolling)? left(?: (\d+))?$/, async function (pronounCase, numValue) {
136
+ await this.page.evaluate((v) => window.scrollBy(-v, 0), numValue ? parseInt(numValue, 10) : 350);
137
+ });
138
+
139
+ /**
140
+ * Scrolls to the start (horizontal origin) of the page.
141
+ *
142
+ * Example #1: When I scroll to start
143
+ * Example #2: And we scroll to the start
144
+ * Example #3: When scrolling to the start of the page
145
+ *
146
+ */
147
+ When(/^(I scroll|we scroll|scrolling)? to( the)* start( of the page)*$/, async function (pronounCase, theCase, pageCase) {
148
+ await this.page.evaluate(() => window.scrollTo(0, window.scrollY));
149
+ });
150
+
151
+ /**
152
+ * Scrolls to the end (horizontal maximum) of the page.
153
+ *
154
+ * Example #1: When I scroll to the end
155
+ * Example #2: And we scroll to end
156
+ * Example #3: When scrolling to the end of the page
157
+ *
158
+ */
159
+ When(/^(I scroll|we scroll|scrolling)? to( the)* end( of the page)*$/, async function (pronounCase, theCase, pageCase) {
160
+ await this.page.evaluate(() => window.scrollTo(document.body.scrollWidth, window.scrollY));
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Horizontal scrolling — scoped to a CSS selector
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Scrolls to the start of a specific element identified by a CSS selector.
169
+ *
170
+ * Example #1: When I scroll to start of "#off-canvas"
171
+ * Example #2: And we scroll to start of "#sidebar"
172
+ * Example #3: When scrolling to start of "#main-container"
173
+ *
174
+ */
175
+ When(/^(I scroll|we scroll|scrolling)? to start of "([^"]*)"$/, async function (pronounCase, selector) {
176
+ await this.page.locator(selector).evaluate(el => { el.scrollLeft = 0; });
177
+ });
178
+
179
+ /**
180
+ * Scrolls to the end of a specific element identified by a CSS selector.
181
+ *
182
+ * Example #1: When I scroll to end of "#off-canvas"
183
+ * Example #2: And we scroll to end of "#sidebar"
184
+ * Example #3: When scrolling to end of "#main-container"
185
+ *
186
+ */
187
+ When(/^(I scroll|we scroll|scrolling)? to end of "([^"]*)"$/, async function (pronounCase, selector) {
188
+ await this.page.locator(selector).evaluate(el => { el.scrollLeft = el.scrollWidth; });
189
+ });