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