@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.
- package/AGENTS.md +148 -0
- package/CLAUDE.md +544 -0
- package/README.md +22 -0
- package/bin/generate-reports.js +363 -0
- package/bin/init-varbase-e2e.js +330 -0
- package/bin/postinstall.js +26 -0
- package/cucumber.js +125 -0
- package/docs/00-quick-start.md +102 -0
- package/docs/01-getting-started.md +102 -0
- package/docs/02-bbr-smart-waits.md +77 -0
- package/docs/03-selector-registry.md +95 -0
- package/docs/04-step-reference.md +801 -0
- package/docs/05-web-first-assertions.md +91 -0
- package/docs/06-network-and-dialogs.md +83 -0
- package/docs/07-auth-state.md +75 -0
- package/docs/08-clock-mocking.md +59 -0
- package/docs/09-api-testing.md +114 -0
- package/docs/10-accessibility.md +167 -0
- package/docs/11-debugging.md +129 -0
- package/docs/12-ai-agent-guide.md +338 -0
- package/docs/13-faq.md +236 -0
- package/docs/14-recipes-cookbook.md +255 -0
- package/docs/15-tag-conventions.md +76 -0
- package/docs/16-ci-cd.md +90 -0
- package/docs/README.md +125 -0
- package/docs/advanced-screenshots/then-i-save-fullscreen-screenshot.md +36 -0
- package/docs/advanced-screenshots/then-i-save-width-x-height-screenshot.md +34 -0
- package/docs/advanced-screenshots/when-i-save-screenshot-name-filename.md +45 -0
- package/docs/advanced-screenshots.md +52 -0
- package/docs/advanced-selectors.md +18 -0
- package/docs/api-step-definitions.md +45 -0
- package/docs/global-settings.md +267 -0
- package/docs/install-varbase-e2e/ddev-varbase-e2e.md +52 -0
- package/docs/install-varbase-e2e.md +34 -0
- package/docs/overview.md +40 -0
- package/docs/step-definitions/given-i-am-anonymous-user.md +67 -0
- package/docs/step-definitions/given-i-am-homepage.md +68 -0
- package/docs/step-definitions/given-i-am-specific-page.md +72 -0
- package/docs/step-definitions/then-i-should-be.md +56 -0
- package/docs/step-definitions/then-i-should-see.md +62 -0
- package/docs/step-definitions/then-item-checkbox.md +54 -0
- package/docs/step-definitions/then-item-link-should-contain-url.md +84 -0
- package/docs/step-definitions/then-item-should-contain.md +67 -0
- package/docs/step-definitions/then-radio-button-value-should-be-selected.md +68 -0
- package/docs/step-definitions/then-response-status-code-should-be-number.md +69 -0
- package/docs/step-definitions/then-value-radio-button-selected.md +62 -0
- package/docs/step-definitions/when-i-attach-file-file-name-element.md +71 -0
- package/docs/step-definitions/when-i-check-checkbox.md +60 -0
- package/docs/step-definitions/when-i-click-link.md +51 -0
- package/docs/step-definitions/when-i-click-operation-text-row.md +71 -0
- package/docs/step-definitions/when-i-fill.md +59 -0
- package/docs/step-definitions/when-i-go-homepage.md +60 -0
- package/docs/step-definitions/when-i-go-specific-page.md +67 -0
- package/docs/step-definitions/when-i-move-backward-one-page.md +49 -0
- package/docs/step-definitions/when-i-move-forward-one-page.md +58 -0
- package/docs/step-definitions/when-i-press-button.md +58 -0
- package/docs/step-definitions/when-i-reload-page.md +51 -0
- package/docs/step-definitions/when-i-scroll.md +73 -0
- package/docs/step-definitions/when-i-select-option-select-list.md +53 -0
- package/docs/step-definitions/when-i-select-radio-button-value.md +62 -0
- package/docs/step-definitions/when-i-uncheck-checkbox.md +57 -0
- package/docs/step-definitions/when-i-wait-ajax-finish.md +64 -0
- package/docs/step-definitions/when-i-wait.md +62 -0
- package/docs/step-definitions.md +82 -0
- package/docs/steps/README.md +137 -0
- package/docs/steps/a11y.md +731 -0
- package/docs/steps/action.md +181 -0
- package/docs/steps/api.md +679 -0
- package/docs/steps/assertion.md +328 -0
- package/docs/steps/auth.md +108 -0
- package/docs/steps/clock.md +171 -0
- package/docs/steps/cookie.md +293 -0
- package/docs/steps/debug.md +52 -0
- package/docs/steps/dialog.md +226 -0
- package/docs/steps/drupal-canvas.md +414 -0
- package/docs/steps/drupal-ckeditor.md +118 -0
- package/docs/steps/drupal-core.md +688 -0
- package/docs/steps/drupal-layout-builder.md +437 -0
- package/docs/steps/drupal-media.md +107 -0
- package/docs/steps/drupal-moderation.md +81 -0
- package/docs/steps/drupal-paragraphs.md +36 -0
- package/docs/steps/element.md +455 -0
- package/docs/steps/field.md +648 -0
- package/docs/steps/file-download.md +235 -0
- package/docs/steps/form.md +344 -0
- package/docs/steps/iframe.md +248 -0
- package/docs/steps/input.md +220 -0
- package/docs/steps/javascript.md +111 -0
- package/docs/steps/keyboard.md +107 -0
- package/docs/steps/link.md +217 -0
- package/docs/steps/metatag.md +99 -0
- package/docs/steps/modal.md +232 -0
- package/docs/steps/navigation.md +275 -0
- package/docs/steps/network.md +284 -0
- package/docs/steps/path.md +203 -0
- package/docs/steps/response.md +102 -0
- package/docs/steps/responsive.md +139 -0
- package/docs/steps/rest.md +140 -0
- package/docs/steps/screenshot.md +211 -0
- package/docs/steps/scroll.md +266 -0
- package/docs/steps/selectors.md +848 -0
- package/docs/steps/storage.md +254 -0
- package/docs/steps/table.md +216 -0
- package/docs/steps/varbase.md +391 -0
- package/docs/steps/video.md +111 -0
- package/docs/steps/wait.md +553 -0
- package/docs/steps/web-first.md +289 -0
- package/docs/steps/xml.md +505 -0
- package/docs/steps/yaml.md +951 -0
- package/examples/a11y.html +31 -0
- package/examples/about-us.html +11 -0
- package/examples/account.html +32 -0
- package/examples/ajax-wait-examples.html +147 -0
- package/examples/contact-us.html +10 -0
- package/examples/cookies.html +10 -0
- package/examples/date.html +12 -0
- package/examples/dynamic-examples.html +498 -0
- package/examples/element.html +11 -0
- package/examples/example-api.json +28 -0
- package/examples/existing-page.html +11 -0
- package/examples/field.html +24 -0
- package/examples/index.html +37 -0
- package/examples/keyboard.html +11 -0
- package/examples/links.html +9 -0
- package/examples/modal.html +14 -0
- package/examples/path.html +3 -0
- package/examples/table.html +13 -0
- package/examples/test--modal-dialogs.html +252 -0
- package/examples/test--then--i-should--not--see-text-in-element.html +48 -0
- package/examples/test--then--i-should--see-text-in-element.html +48 -0
- package/examples/test--then--i-should--see-text-matching-pattern.html +31 -0
- package/examples/test--then--i-should-see-text-in-table-row.html +232 -0
- package/examples/test--then--the-checkbox-checked.html +68 -0
- package/examples/test--then--the-element-should_not-contain-cssproperty.html +47 -0
- package/examples/test--then--the-link-should-contain.html +21 -0
- package/examples/test--when--i-additionally-select.html +27 -0
- package/examples/test--when--i-attach-file-to-feild.html +50 -0
- package/examples/test--when--i-check-uncheck-checkbox.html +47 -0
- package/examples/test--when--i-click-link.html +18 -0
- package/examples/test--when--i-click-text-in-table-row.html +178 -0
- package/examples/test--when--i-fill-in.html +48 -0
- package/examples/test--when--i-press-button.html +31 -0
- package/examples/test--when--i-reload-page.html +27 -0
- package/examples/test--when--i-scroll-functionality.html +177 -0
- package/examples/test--when--i-scroll-left-right-functionality.html +149 -0
- package/examples/test--when--i-select-option-from-dropdownlist.html +35 -0
- package/examples/test--when--i-select-radio-button.html +95 -0
- package/examples/test--when-i-wait-max-of-time.html +39 -0
- package/examples/test--when-i-wait-page-until-is-loaded.html +47 -0
- package/examples/test--when-i-wait-specific-time.html +39 -0
- package/examples/test-acceptable-url-path.html +11 -0
- package/examples/video-demo.html +43 -0
- package/package.json +56 -0
- package/playwright.config.ts +39 -0
- package/tests/assets/test-document.pdf +14 -0
- package/tests/assets/vardot.png +0 -0
- package/tests/features/a11y.feature +39 -0
- package/tests/features/ajax-wait-examples.feature +24 -0
- package/tests/features/api-step-definitions-individual.feature +185 -0
- package/tests/features/complete-api-testing-examples.feature +205 -0
- package/tests/features/cookie.feature +13 -0
- package/tests/features/date.feature +11 -0
- package/tests/features/dynamic-content-examples.feature +156 -0
- package/tests/features/element.feature +16 -0
- package/tests/features/field.feature +25 -0
- package/tests/features/keyboard.feature +10 -0
- package/tests/features/link.feature +11 -0
- package/tests/features/modal-advanced-example.feature +54 -0
- package/tests/features/modal-basic-example.feature +42 -0
- package/tests/features/modal-error-handling-example.feature +37 -0
- package/tests/features/modal.feature +11 -0
- package/tests/features/path.feature +16 -0
- package/tests/features/response.feature +8 -0
- package/tests/features/responsive.feature +10 -0
- package/tests/features/table.feature +18 -0
- package/tests/features/test--given--i-am-an-anonymous-user.feature +10 -0
- package/tests/features/test--given--i-am-on-homepage.feature +16 -0
- package/tests/features/test--then--i-save-screenshot.feature +33 -0
- package/tests/features/test--then--i-see-locator.feature +320 -0
- package/tests/features/test--then--i-should--not--see-text-in-element.feature +13 -0
- package/tests/features/test--then--i-should--see-text-in-element-by-attr.feature +10 -0
- package/tests/features/test--then--i-should--see-text-in-element.feature +10 -0
- package/tests/features/test--then--i-should--see-text-matching-pattern-in-element.feature +13 -0
- package/tests/features/test--then--i-should--see-text-matching-pattern.feature +18 -0
- package/tests/features/test--then--i-should-be-on-page.feature +10 -0
- package/tests/features/test--then--i-should-be-on-the-homepage.feature +10 -0
- package/tests/features/test--then--i-should-not-see-an-element-by-attr.feature +8 -0
- package/tests/features/test--then--i-should-not-see-an-element.feature +8 -0
- package/tests/features/test--then--i-should-see-an-element-by-attr.feature +8 -0
- package/tests/features/test--then--i-should-see-an-element.feature +8 -0
- package/tests/features/test--then--i-should-see-n-elements.feature +24 -0
- package/tests/features/test--then--i-should-see-text-in-table-row.feature +96 -0
- package/tests/features/test--then--print-current-url-and-response.feature +21 -0
- package/tests/features/test--then--the-checkbox-is-checked.feature +18 -0
- package/tests/features/test--then--the-checkbox-should-be-checked.feature +16 -0
- package/tests/features/test--then--the-element-should-contain-cssproperty.feature +9 -0
- package/tests/features/test--then--the-element-should-not-contain-cssproperty.feature +10 -0
- package/tests/features/test--then--the-field-should-contain-value.feature +11 -0
- package/tests/features/test--then--the-response-should-contain.feature +11 -0
- package/tests/features/test--then--the-response-status-code-should-be.feature +31 -0
- package/tests/features/test--then--the-url-should-match.feature +7 -0
- package/tests/features/test--then-the-link-should-contain-by-attribute.feature +18 -0
- package/tests/features/test--then-the-link-should-contain.feature +7 -0
- package/tests/features/test--when--i-additionally-select.feature +30 -0
- package/tests/features/test--when--i-attach-file-to-feild.feature +9 -0
- package/tests/features/test--when--i-check-checkbox.feature +21 -0
- package/tests/features/test--when--i-click-link-by-attribute.feature +21 -0
- package/tests/features/test--when--i-click-link.feature +9 -0
- package/tests/features/test--when--i-click-text-in-table-row.feature +37 -0
- package/tests/features/test--when--i-fill-in-by-attr.feature +31 -0
- package/tests/features/test--when--i-fill-in-table-by-attr.feature +19 -0
- package/tests/features/test--when--i-fill-in.feature +32 -0
- package/tests/features/test--when--i-follow.feature +22 -0
- package/tests/features/test--when--i-go-to-homepage.feature +9 -0
- package/tests/features/test--when--i-move-forward-backward-one-page.feature +14 -0
- package/tests/features/test--when--i-press-button-by-attribute.feature +18 -0
- package/tests/features/test--when--i-press-button.feature +13 -0
- package/tests/features/test--when--i-reload-page.feature +11 -0
- package/tests/features/test--when--i-scroll-left-right.feature +48 -0
- package/tests/features/test--when--i-scroll.feature +48 -0
- package/tests/features/test--when--i-select-option-from-dropdownlist.feature +21 -0
- package/tests/features/test--when--i-select-radio-button.feature +37 -0
- package/tests/features/test--when--i-wait-page-is-loaded.feature +8 -0
- package/tests/features/test--when--i-wait-time.feature +21 -0
- package/tests/features/video.feature +18 -0
- package/tests/features/wait.feature +7 -0
- package/tests/features/yaml.feature +102 -0
- package/tests/selectors/_canonical-keys.json +113 -0
- package/tests/selectors/back-end-selectors.json +19 -0
- package/tests/selectors/cms-contentful-admin.json +35 -0
- package/tests/selectors/cms-craft-admin.json +39 -0
- package/tests/selectors/cms-drupal-cms-gin.json +56 -0
- package/tests/selectors/cms-drupal-core-claro.json +50 -0
- package/tests/selectors/cms-generic-admin.json +34 -0
- package/tests/selectors/cms-ghost-admin.json +34 -0
- package/tests/selectors/cms-joomla-admin.json +41 -0
- package/tests/selectors/cms-magento2-admin.json +32 -0
- package/tests/selectors/cms-prestashop-admin.json +38 -0
- package/tests/selectors/cms-shopify-admin.json +25 -0
- package/tests/selectors/cms-strapi-admin.json +42 -0
- package/tests/selectors/cms-typo3-admin.json +41 -0
- package/tests/selectors/cms-woocommerce-front.json +50 -0
- package/tests/selectors/cms-wordpress-admin.json +31 -0
- package/tests/selectors/framework-ant-design.json +80 -0
- package/tests/selectors/framework-bootstrap.json +86 -0
- package/tests/selectors/framework-bulma.json +74 -0
- package/tests/selectors/framework-chakra.json +58 -0
- package/tests/selectors/framework-foundation.json +51 -0
- package/tests/selectors/framework-material-ui.json +72 -0
- package/tests/selectors/framework-shadcn.json +51 -0
- package/tests/selectors/framework-tailwind.json +40 -0
- package/tests/selectors/framework-vuetify.json +61 -0
- package/tests/selectors/front-end-selectors.json +16 -0
- package/tests/selectors/homepage-selectors.json +13 -0
- package/tests/step-definitions/a11y.steps.js +800 -0
- package/tests/step-definitions/action.steps.js +167 -0
- package/tests/step-definitions/api.steps.js +732 -0
- package/tests/step-definitions/assertion.steps.js +352 -0
- package/tests/step-definitions/auth.steps.js +126 -0
- package/tests/step-definitions/clock.steps.js +128 -0
- package/tests/step-definitions/cookie.steps.js +216 -0
- package/tests/step-definitions/debug.steps.js +39 -0
- package/tests/step-definitions/dialog.steps.js +193 -0
- package/tests/step-definitions/drupal-canvas.steps.js +764 -0
- package/tests/step-definitions/drupal-ckeditor.steps.js +108 -0
- package/tests/step-definitions/drupal-core.steps.js +665 -0
- package/tests/step-definitions/drupal-helpers.js +371 -0
- package/tests/step-definitions/drupal-layout-builder.steps.js +466 -0
- package/tests/step-definitions/drupal-media.steps.js +99 -0
- package/tests/step-definitions/drupal-moderation.steps.js +71 -0
- package/tests/step-definitions/drupal-paragraphs.steps.js +50 -0
- package/tests/step-definitions/element.steps.js +356 -0
- package/tests/step-definitions/field.steps.js +653 -0
- package/tests/step-definitions/file-download.steps.js +211 -0
- package/tests/step-definitions/form.steps.js +333 -0
- package/tests/step-definitions/iframe.steps.js +228 -0
- package/tests/step-definitions/input.steps.js +202 -0
- package/tests/step-definitions/javascript.steps.js +232 -0
- package/tests/step-definitions/keyboard.steps.js +112 -0
- package/tests/step-definitions/link.steps.js +142 -0
- package/tests/step-definitions/metatag.steps.js +91 -0
- package/tests/step-definitions/modal.steps.js +244 -0
- package/tests/step-definitions/navigation.steps.js +218 -0
- package/tests/step-definitions/network.steps.js +280 -0
- package/tests/step-definitions/path.steps.js +141 -0
- package/tests/step-definitions/response.steps.js +98 -0
- package/tests/step-definitions/responsive.steps.js +125 -0
- package/tests/step-definitions/rest.steps.js +111 -0
- package/tests/step-definitions/screenshot.steps.js +423 -0
- package/tests/step-definitions/scroll.steps.js +189 -0
- package/tests/step-definitions/selectors.steps.js +1003 -0
- package/tests/step-definitions/storage.steps.js +194 -0
- package/tests/step-definitions/table.steps.js +181 -0
- package/tests/step-definitions/varbase-e2e.js +897 -0
- package/tests/step-definitions/varbase.steps.js +458 -0
- package/tests/step-definitions/video.steps.js +178 -0
- package/tests/step-definitions/wait.steps.js +400 -0
- package/tests/step-definitions/web-first.steps.js +298 -0
- package/tests/step-definitions/xml.steps.js +411 -0
- package/tests/step-definitions/yaml.steps.js +913 -0
- 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
|
+
});
|