@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,764 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// -----------------------------------------------------------------------------
|
|
4
|
+
// Drupal Canvas step definitions.
|
|
5
|
+
//
|
|
6
|
+
// Two flavours of step live here:
|
|
7
|
+
// 1. Editor steps that drive the real Drupal Canvas React editor the way a
|
|
8
|
+
// human site builder does - open a page in the editor, browse the Library,
|
|
9
|
+
// drag a component onto the canvas or right-click a pattern and choose
|
|
10
|
+
// Insert, configure it in the Settings panel, then publish through the
|
|
11
|
+
// editor's Review/Publish widget.
|
|
12
|
+
// 2. API helper steps that use Canvas's own authoring endpoints (the same
|
|
13
|
+
// ones the editor calls) to set up or read state quickly (e.g. creating a
|
|
14
|
+
// blank page for an editor step to then build on).
|
|
15
|
+
//
|
|
16
|
+
// Reuses Varbase E2E's own helpers (smartSettle + friendly) so these steps
|
|
17
|
+
// behave like the core navigation steps.
|
|
18
|
+
// -----------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const { When, Then } = require('@cucumber/cucumber');
|
|
21
|
+
const { smartSettle, friendly } = require('./varbase-e2e');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a canvas_page id by its title via the Canvas content API.
|
|
25
|
+
*/
|
|
26
|
+
async function resolveCanvasPageId(page, title) {
|
|
27
|
+
return page.evaluate(async (t) => {
|
|
28
|
+
const list = await fetch('/canvas/api/v0/content/canvas_page', { credentials: 'same-origin' })
|
|
29
|
+
.then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
|
30
|
+
const pages = (list && list.data) || [];
|
|
31
|
+
const match = pages.find((p) => p.title === t);
|
|
32
|
+
return match ? match.id : null;
|
|
33
|
+
}, title);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add a Webform block component to the bottom of a Drupal Canvas page and
|
|
38
|
+
* publish the page. Drupal Canvas builds pages from a React editor whose
|
|
39
|
+
* drag-and-drop cannot be driven by a browser test (the drop target lives in a
|
|
40
|
+
* cross-document iframe), so this step performs the same change the editor
|
|
41
|
+
* makes by calling Canvas's own authoring API:
|
|
42
|
+
* 1. resolve the canvas_page id by title,
|
|
43
|
+
* 2. append a `block.webform_block` component to the page's main content,
|
|
44
|
+
* 3. POST the updated layout (creates an auto-save),
|
|
45
|
+
* 4. publish the pending auto-save.
|
|
46
|
+
*
|
|
47
|
+
* Requires an authenticated user with "publish auto-saves" access (e.g. the
|
|
48
|
+
* webmaster) — run a login step first. The webform value is the entity
|
|
49
|
+
* autocomplete format "Label (machine_name)".
|
|
50
|
+
*
|
|
51
|
+
* Example:
|
|
52
|
+
* Given I am a logged in user with the "webmaster" user
|
|
53
|
+
* When I add the "Newsletter Subscribe (newsletter_subscribe)" webform to the bottom of the "Home" Canvas page and publish it
|
|
54
|
+
*
|
|
55
|
+
* Example #1: When I add the "Contact Us" webform to the bottom of the "Contact" Canvas page
|
|
56
|
+
* Example #2: And I add the "Newsletter" webform to the bottom of the "Home" page and publish it
|
|
57
|
+
* Example #3: When we add the "Business Contact" webform to the bottom of the "About Us" Canvas page and publish
|
|
58
|
+
* Example #4: And I add the "Feedback" webform to the bottom of the "Support" page
|
|
59
|
+
* Example #5: When I add the "Careers" webform to the bottom of the "Jobs" Canvas page and publish it
|
|
60
|
+
*/
|
|
61
|
+
When(/^(?:I |we )*add the "([^"]*)" webform to the bottom of the "([^"]*)" (?:Canvas )?page(?: and publish(?: it)?)?$/, async function (webformId, pageTitle) {
|
|
62
|
+
const result = await this.page.evaluate(async ({ webformId, pageTitle }) => {
|
|
63
|
+
const json = (r) => r.json();
|
|
64
|
+
const csrf = await (await fetch('/session/token', { credentials: 'same-origin' })).text();
|
|
65
|
+
const headers = { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf };
|
|
66
|
+
|
|
67
|
+
// 1. Resolve the canvas_page id by title.
|
|
68
|
+
const list = await fetch('/canvas/api/v0/content/canvas_page', { credentials: 'same-origin' }).then(json);
|
|
69
|
+
const pages = (list && list.data) || [];
|
|
70
|
+
const pageEntry = pages.find((p) => p.title === pageTitle);
|
|
71
|
+
if (!pageEntry) return { ok: false, error: `No Canvas page titled "${pageTitle}". Available: ${pages.map((p) => p.title).join(', ')}` };
|
|
72
|
+
const id = pageEntry.id;
|
|
73
|
+
|
|
74
|
+
// 2. Start from a clean auto-save for this page, then fetch its layout.
|
|
75
|
+
await fetch(`/canvas/api/v0/auto-saves/canvas_page/${id}`, { method: 'DELETE', credentials: 'same-origin', headers });
|
|
76
|
+
const data = await fetch(`/canvas/api/v0/layout/canvas_page/${id}`, { credentials: 'same-origin' }).then(json);
|
|
77
|
+
const layout = data.layout;
|
|
78
|
+
const model = data.model;
|
|
79
|
+
const content = layout.find((r) => r.nodeType === 'region' && r.id === 'content');
|
|
80
|
+
if (!content) return { ok: false, error: 'No main content region found in the Canvas layout.' };
|
|
81
|
+
|
|
82
|
+
// 3. Idempotency: drop any existing block for the same webform so re-runs
|
|
83
|
+
// (and adding to the same page twice) never duplicate the form.
|
|
84
|
+
for (const region of layout) {
|
|
85
|
+
if (!Array.isArray(region.components)) continue;
|
|
86
|
+
region.components = region.components.filter((c) => {
|
|
87
|
+
const m = model[c.uuid];
|
|
88
|
+
const isSame = m && m.resolved && m.resolved.webform_id === webformId;
|
|
89
|
+
if (isSame) delete model[c.uuid];
|
|
90
|
+
return !isSame;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4. Append the webform block component at the bottom of the content region.
|
|
95
|
+
// Resolve the block.webform_block component version at runtime — the
|
|
96
|
+
// version is a content hash of the component's plugin/config definition
|
|
97
|
+
// and therefore differs per environment (Drupal core minor, enabled
|
|
98
|
+
// modules, config), so it must never be hardcoded.
|
|
99
|
+
const components = await fetch('/canvas/api/v0/config/component', { credentials: 'same-origin' }).then(json);
|
|
100
|
+
const webformComponent = components && components['block.webform_block'];
|
|
101
|
+
if (!webformComponent || !webformComponent.version) {
|
|
102
|
+
return { ok: false, error: 'The "block.webform_block" Canvas component is not available. Ensure the webform + Canvas blocks are installed.' };
|
|
103
|
+
}
|
|
104
|
+
const uuid = crypto.randomUUID();
|
|
105
|
+
content.components.push({ uuid, nodeType: 'component', type: `block.webform_block@${webformComponent.version}`, name: null, slots: [] });
|
|
106
|
+
model[uuid] = { resolved: { webform_id: webformId, settings: { default_data: '', redirect: false, lazy: false }, label: 'Webform', label_display: '0' } };
|
|
107
|
+
|
|
108
|
+
// 5. Save the layout (auto-save) then publish it.
|
|
109
|
+
const post = await fetch(`/canvas/api/v0/layout/canvas_page/${id}`, {
|
|
110
|
+
method: 'POST', credentials: 'same-origin', headers,
|
|
111
|
+
body: JSON.stringify({ layout, model, autoSaves: data.autoSaves, clientInstanceId: crypto.randomUUID(), entity_form_fields: data.entity_form_fields }),
|
|
112
|
+
});
|
|
113
|
+
if (!post.ok) return { ok: false, error: `Layout POST failed (${post.status}): ${(await post.text()).slice(0, 200)}` };
|
|
114
|
+
|
|
115
|
+
const pending = await fetch('/canvas/api/v0/auto-saves/pending', { credentials: 'same-origin' }).then(json);
|
|
116
|
+
const pub = await fetch('/canvas/api/v0/auto-saves/publish', {
|
|
117
|
+
method: 'POST', credentials: 'same-origin', headers, body: JSON.stringify(pending.data),
|
|
118
|
+
});
|
|
119
|
+
if (!pub.ok) return { ok: false, error: `Publish failed (${pub.status}): ${(await pub.text()).slice(0, 200)}` };
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}, { webformId, pageTitle });
|
|
122
|
+
|
|
123
|
+
if (!result || !result.ok) {
|
|
124
|
+
throw friendly(
|
|
125
|
+
`Could not add the "${webformId}" webform to the "${pageTitle}" Canvas page.`,
|
|
126
|
+
(result && result.error) || 'Ensure you are logged in as a user who can publish Canvas auto-saves.'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Assert that a component is (or is not) offered in the Drupal Canvas editor's
|
|
134
|
+
* component library. The editor populates its library from the same
|
|
135
|
+
* `/canvas/api/v0/config/component` endpoint queried here, so this verifies
|
|
136
|
+
* what an editor sees when building a page - without driving the React editor.
|
|
137
|
+
*
|
|
138
|
+
* Requires an authenticated user who can edit Canvas pages (run a login step
|
|
139
|
+
* and navigate to an admin page first so the request is same-origin).
|
|
140
|
+
*
|
|
141
|
+
* Example:
|
|
142
|
+
* Then the Drupal Canvas component library should list the "block.system_menu_block.main" component
|
|
143
|
+
*
|
|
144
|
+
* Example #1: Then the Drupal Canvas component library should list the "Hero" component
|
|
145
|
+
* Example #2: And the Drupal Canvas component library should list the "Card" component
|
|
146
|
+
* Example #3: Then the Drupal Canvas component library should not list the "Legacy Slider" component
|
|
147
|
+
* Example #4: And the Drupal Canvas component library should list the "Accordion" component
|
|
148
|
+
* Example #5: Then the Drupal Canvas component library should not list the "Deprecated Banner" component
|
|
149
|
+
*/
|
|
150
|
+
Then(/^the Drupal Canvas component library should( not)? list the "([^"]*)" component$/, async function (negate, componentId) {
|
|
151
|
+
const present = await this.page.evaluate(async (cid) => {
|
|
152
|
+
const data = await fetch('/canvas/api/v0/config/component', { credentials: 'same-origin' })
|
|
153
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
154
|
+
.catch(() => null);
|
|
155
|
+
if (!data) return null;
|
|
156
|
+
const keys = Array.isArray(data) ? data.map((x) => x.id || x) : Object.keys(data);
|
|
157
|
+
return keys.includes(cid);
|
|
158
|
+
}, componentId);
|
|
159
|
+
|
|
160
|
+
if (present === null) {
|
|
161
|
+
throw friendly(
|
|
162
|
+
'Could not read the Drupal Canvas component library.',
|
|
163
|
+
'Run a login step (e.g. the webmaster) and navigate to an admin page first.'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (negate && present) {
|
|
167
|
+
throw friendly(`Component "${componentId}" should not be in the Canvas library, but it is.`);
|
|
168
|
+
}
|
|
169
|
+
if (!negate && !present) {
|
|
170
|
+
throw friendly(`Component "${componentId}" is not listed in the Canvas component library.`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create (or replace) a published Drupal Canvas page that contains a single
|
|
176
|
+
* component, configured from a Gherkin data table of input/value rows. The
|
|
177
|
+
* Canvas editor's drag-and-drop cannot be driven by a browser test, so this
|
|
178
|
+
* builds the page through Canvas's own content API:
|
|
179
|
+
* 1. delete any existing page at the same path (idempotent re-runs),
|
|
180
|
+
* 2. read the live component version (never hard-coded),
|
|
181
|
+
* 3. POST a new published page whose component tree is that one component
|
|
182
|
+
* with the given inputs.
|
|
183
|
+
*
|
|
184
|
+
* This step is generic: pass any Canvas component id (e.g.
|
|
185
|
+
* "sdc.vartheme_bs5.card-hero", "block.webform_block"). Boolean-looking values
|
|
186
|
+
* ("true"/"false") are coerced so checkbox props work. Requires a logged-in
|
|
187
|
+
* user who can create Canvas pages (run a login step first).
|
|
188
|
+
*
|
|
189
|
+
* Example:
|
|
190
|
+
* When I create a Canvas page "Hero - primary" at "/test-hero-primary" with the "sdc.vartheme_bs5.card-hero" component:
|
|
191
|
+
* | title | Primary hero |
|
|
192
|
+
* | background_color | bg-primary |
|
|
193
|
+
* | card_border | true |
|
|
194
|
+
*
|
|
195
|
+
* Example #1: When I create a Canvas page "Landing" at "/landing" with the "Hero" component:
|
|
196
|
+
* Example #2: And I create a Canvas page "Pricing" at "/pricing" with the "Card" component:
|
|
197
|
+
* Example #3: When we create a Canvas page "Team" at "/about/team" with the "Grid" component:
|
|
198
|
+
* Example #4: And I create a Canvas page "Events" at "/events" with the "Listing" component:
|
|
199
|
+
* Example #5: When I create a Canvas page "Support" at "/support" with the "Accordion" component:
|
|
200
|
+
*/
|
|
201
|
+
When(/^(?:I |we )*create a Canvas page "([^"]*)" at "([^"]*)" with the "([^"]*)" component:$/, async function (title, path, componentId, table) {
|
|
202
|
+
const inputs = {};
|
|
203
|
+
for (const [key, value] of table.raw()) {
|
|
204
|
+
if (value === 'true' || value === 'false') {
|
|
205
|
+
inputs[key] = value === 'true';
|
|
206
|
+
} else {
|
|
207
|
+
inputs[key] = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const result = await this.page.evaluate(async ({ title, path, componentId, inputs }) => {
|
|
212
|
+
const json = (r) => r.json();
|
|
213
|
+
const csrf = await (await fetch('/session/token', { credentials: 'same-origin' })).text();
|
|
214
|
+
const headers = { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf };
|
|
215
|
+
|
|
216
|
+
// 1. Delete any existing page at this path so re-runs start clean.
|
|
217
|
+
const list = await fetch('/canvas/api/v0/content/canvas_page', { credentials: 'same-origin' }).then(json).catch(() => null);
|
|
218
|
+
const pages = (list && list.data) || [];
|
|
219
|
+
const existing = pages.find((p) => p.path === path || p.title === title);
|
|
220
|
+
if (existing) {
|
|
221
|
+
await fetch(`/canvas/api/v0/content/canvas_page/${existing.id}`, { method: 'DELETE', credentials: 'same-origin', headers });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 2. Read the live component version (do not hard-code it).
|
|
225
|
+
const comp = await fetch('/canvas/api/v0/config/component', { credentials: 'same-origin' }).then(json).catch(() => null);
|
|
226
|
+
if (!comp) return { ok: false, error: 'Could not read the Canvas component list.' };
|
|
227
|
+
const def = Array.isArray(comp) ? comp.find((x) => x.id === componentId) : comp[componentId];
|
|
228
|
+
if (!def) return { ok: false, error: `The component "${componentId}" is not available in the Canvas component list.` };
|
|
229
|
+
|
|
230
|
+
// 3. Create the published page with that one component.
|
|
231
|
+
const body = {
|
|
232
|
+
title,
|
|
233
|
+
status: true,
|
|
234
|
+
path: { alias: path },
|
|
235
|
+
components: [{
|
|
236
|
+
uuid: crypto.randomUUID(),
|
|
237
|
+
component_id: componentId,
|
|
238
|
+
component_version: def.version,
|
|
239
|
+
inputs: JSON.stringify(inputs),
|
|
240
|
+
}],
|
|
241
|
+
};
|
|
242
|
+
const res = await fetch('/canvas/api/v0/content/canvas_page', { method: 'POST', credentials: 'same-origin', headers, body: JSON.stringify(body) });
|
|
243
|
+
if (res.status !== 201) return { ok: false, error: `Create failed (${res.status}): ${(await res.text()).slice(0, 200)}` };
|
|
244
|
+
return { ok: true };
|
|
245
|
+
}, { title, path, componentId, inputs });
|
|
246
|
+
|
|
247
|
+
if (!result || !result.ok) {
|
|
248
|
+
throw friendly(
|
|
249
|
+
`Could not create the "${title}" Canvas page with the "${componentId}" component.`,
|
|
250
|
+
(result && result.error) || 'Ensure you are logged in as a user who can create Canvas pages.'
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add a component to a Drupal Canvas page the way a human site builder does:
|
|
258
|
+
* open the page in the Canvas editor, open the Library, find the named
|
|
259
|
+
* component, and drag it onto the (empty) canvas drop zone. The dropped
|
|
260
|
+
* component is left selected with its Settings panel open, ready for option
|
|
261
|
+
* steps. Drupal Canvas renders its drop zones in the top document during an
|
|
262
|
+
* active drag, so the drag is performed with real pointer moves and only
|
|
263
|
+
* released once a drop zone reports it is being hovered.
|
|
264
|
+
*
|
|
265
|
+
* Run a login step first (e.g. the webmaster). Use the component's visible
|
|
266
|
+
* Library name (e.g. "Hero Card").
|
|
267
|
+
*
|
|
268
|
+
* Example:
|
|
269
|
+
* When I add the "Hero Card" component to the "Test Hero Editor" Canvas page using the editor
|
|
270
|
+
*
|
|
271
|
+
* Example #1: When I add the "Hero" component to the "Landing" Canvas page using the editor
|
|
272
|
+
* Example #2: And I add the "Card" component to the "Pricing" Canvas page using the editor
|
|
273
|
+
* Example #3: When we add the "Accordion" component to the "Support" Canvas page using the editor
|
|
274
|
+
* Example #4: And I add the "Grid" component to the "Team" Canvas page using the editor
|
|
275
|
+
* Example #5: When I add the "Listing" component to the "Events" Canvas page using the editor
|
|
276
|
+
*/
|
|
277
|
+
When(/^(?:I |we )*add the "([^"]*)" component to the "([^"]*)" Canvas page using the editor$/, { timeout: 120000 }, async function (componentName, pageTitle) {
|
|
278
|
+
const id = await resolveCanvasPageId(this.page, pageTitle);
|
|
279
|
+
if (!id) throw friendly(`No Canvas page titled "${pageTitle}" was found.`, 'Create the page first.');
|
|
280
|
+
|
|
281
|
+
await this.page.goto(`${this.launchUrl.replace(/\/$/, '')}/canvas/editor/canvas_page/${id}`, { waitUntil: 'domcontentloaded' });
|
|
282
|
+
await this.page.waitForTimeout(8000);
|
|
283
|
+
|
|
284
|
+
// Open the Library panel and the Components tab, then filter to the component.
|
|
285
|
+
// The editor is a heavy SPA, so poll: (re)open the Library and search until
|
|
286
|
+
// the named component item appears in the list (or give up after ~24s).
|
|
287
|
+
let itemReady = false;
|
|
288
|
+
for (let attempt = 0; attempt < 12 && !itemReady; attempt++) {
|
|
289
|
+
await this.page.evaluate(() => {
|
|
290
|
+
const lib = [...document.querySelectorAll('button,[role=button],[role=tab]')].find((b) => /^Library$/.test((b.getAttribute('aria-label') || b.innerText || '').trim()));
|
|
291
|
+
if (lib) lib.click();
|
|
292
|
+
});
|
|
293
|
+
await this.page.waitForTimeout(500);
|
|
294
|
+
await this.page.evaluate(() => {
|
|
295
|
+
const tab = [...document.querySelectorAll('button,[role=tab]')].find((b) => /^Components$/.test((b.getAttribute('aria-label') || b.innerText || '').trim()));
|
|
296
|
+
if (tab) tab.click();
|
|
297
|
+
});
|
|
298
|
+
await this.page.waitForTimeout(400);
|
|
299
|
+
await this.page.evaluate((name) => {
|
|
300
|
+
const s = document.querySelector('input[placeholder="Search…"]');
|
|
301
|
+
if (s) { s.focus(); s.value = name; s.dispatchEvent(new Event('input', { bubbles: true })); }
|
|
302
|
+
}, componentName);
|
|
303
|
+
await this.page.waitForTimeout(900);
|
|
304
|
+
itemReady = await this.page.evaluate((name) => [...document.querySelectorAll('span,div')].some((e) => e.children.length === 0 && e.textContent.trim() === name), componentName);
|
|
305
|
+
}
|
|
306
|
+
if (!itemReady) throw friendly(`The "${componentName}" component did not appear in the Canvas Library.`, 'Check the component name and that the Library panel opens.');
|
|
307
|
+
|
|
308
|
+
// Locate the draggable Library item and the empty canvas drop zone target.
|
|
309
|
+
const coords = await this.page.evaluate((name) => {
|
|
310
|
+
const leaf = [...document.querySelectorAll('span,div')].find((e) => e.children.length === 0 && e.textContent.trim() === name);
|
|
311
|
+
if (!leaf) return null;
|
|
312
|
+
let item = leaf;
|
|
313
|
+
while (item && !(item.getAttribute && item.getAttribute('aria-roledescription') === 'draggable')) item = item.parentElement;
|
|
314
|
+
item = item || leaf.closest('[role=button]');
|
|
315
|
+
if (!item) return null;
|
|
316
|
+
item.scrollIntoView({ block: 'center' });
|
|
317
|
+
const ir = item.getBoundingClientRect();
|
|
318
|
+
// Empty drop zone lives inside the preview iframe; map to viewport coords.
|
|
319
|
+
const frame = document.querySelector('iframe[title="Preview"]') || document.querySelectorAll('iframe')[1];
|
|
320
|
+
const fr = frame.getBoundingClientRect();
|
|
321
|
+
let drop = null;
|
|
322
|
+
try {
|
|
323
|
+
const z = frame.contentDocument.querySelector('[class*=emptyDropZone], .canvas--region-empty-placeholder');
|
|
324
|
+
const zr = z.getBoundingClientRect();
|
|
325
|
+
drop = { x: fr.x + zr.x + zr.width / 2, y: fr.y + zr.y + Math.min(zr.height / 2, 120) };
|
|
326
|
+
} catch (e) {
|
|
327
|
+
drop = { x: fr.x + fr.width / 2, y: fr.y + 160 };
|
|
328
|
+
}
|
|
329
|
+
return { sx: ir.x + ir.width / 2, sy: ir.y + ir.height / 2, tx: drop.x, ty: drop.y };
|
|
330
|
+
}, componentName);
|
|
331
|
+
if (!coords) throw friendly(`The "${componentName}" component was not found in the Canvas Library.`);
|
|
332
|
+
|
|
333
|
+
// Drag: press, activate with a small move, travel in steps, then hover the
|
|
334
|
+
// drop zone with micro-moves until it reports "over", and release.
|
|
335
|
+
const { sx, sy, tx, ty } = coords;
|
|
336
|
+
await this.page.mouse.move(sx, sy);
|
|
337
|
+
await this.page.mouse.down();
|
|
338
|
+
await this.page.mouse.move(sx + 10, sy + 8, { steps: 5 });
|
|
339
|
+
await this.page.mouse.move((sx + tx) / 2, (sy + ty) / 2, { steps: 15 });
|
|
340
|
+
await this.page.mouse.move(tx, ty, { steps: 20 });
|
|
341
|
+
let isOver = false;
|
|
342
|
+
for (let i = 0; i < 10; i++) {
|
|
343
|
+
await this.page.mouse.move(tx + (i % 2 ? 2 : -2), ty + (i % 2 ? 1 : -1), { steps: 2 });
|
|
344
|
+
await this.page.waitForTimeout(150);
|
|
345
|
+
isOver = await this.page.evaluate(() => [...document.querySelectorAll('[class*=componentDropZone],[class*=emptyDropZone]')].some((e) => /isOver/i.test(e.className)));
|
|
346
|
+
if (isOver) break;
|
|
347
|
+
}
|
|
348
|
+
await this.page.waitForTimeout(200);
|
|
349
|
+
await this.page.mouse.up();
|
|
350
|
+
await this.page.waitForTimeout(2500);
|
|
351
|
+
|
|
352
|
+
// Verify the dropped component actually landed: poll the page layout for the
|
|
353
|
+
// component's machine id (the content region gains it; header/footer already
|
|
354
|
+
// carry components, so match the specific component type).
|
|
355
|
+
const componentId = await this.page.evaluate((name) => {
|
|
356
|
+
const comp = window.__lastCanvasComponentId || null;
|
|
357
|
+
return comp;
|
|
358
|
+
}, componentName);
|
|
359
|
+
let landed = false;
|
|
360
|
+
for (let i = 0; i < 8 && !landed; i++) {
|
|
361
|
+
landed = await this.page.evaluate(async (pageId) => {
|
|
362
|
+
const data = await fetch(`/canvas/api/v0/layout/canvas_page/${pageId}`, { credentials: 'same-origin' }).then((r) => r.json()).catch(() => null);
|
|
363
|
+
if (!data) return false;
|
|
364
|
+
const content = (data.layout || []).find((r) => r.nodeType === 'region' && r.id === 'content');
|
|
365
|
+
return !!(content && Array.isArray(content.components) && content.components.length > 0);
|
|
366
|
+
}, id);
|
|
367
|
+
if (!landed) await this.page.waitForTimeout(1000);
|
|
368
|
+
}
|
|
369
|
+
if (!landed) {
|
|
370
|
+
throw friendly(`The "${componentName}" component was not added to the canvas.`, 'The editor drag-and-drop did not register a drop.');
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Publish the pending changes for the Canvas page currently open in the editor.
|
|
376
|
+
*
|
|
377
|
+
* The editor's React "Review changes" publish widget is not reliable to drive
|
|
378
|
+
* headless on CI (it can hang), so this commits through Canvas's own authoring
|
|
379
|
+
* API - the same endpoints the widget calls: re-POST the page's auto-saved
|
|
380
|
+
* layout (which the editor's drag-and-drop and Settings-panel edits have
|
|
381
|
+
* populated) to guarantee a pending auto-save, then POST it to the publish
|
|
382
|
+
* endpoint. Deterministic and fast, with no editor-UI timing.
|
|
383
|
+
*
|
|
384
|
+
* Example: When I publish the Canvas page changes
|
|
385
|
+
*
|
|
386
|
+
* Example #1: When I publish the Canvas page changes
|
|
387
|
+
* Example #2: And I publish the Canvas page changes
|
|
388
|
+
* Example #3: When we publish the Canvas page changes
|
|
389
|
+
* Example #4: Given I publish the Canvas page changes
|
|
390
|
+
* Example #5: And we publish the Canvas page changes
|
|
391
|
+
*/
|
|
392
|
+
When(/^(?:I |we )*publish the Canvas page changes$/, { timeout: 120000 }, async function () {
|
|
393
|
+
const pageId = (this.page.url().match(/canvas_page\/(\d+)/) || [])[1];
|
|
394
|
+
if (!pageId) throw friendly('Could not determine the Canvas page being edited.', 'Open a page in the Canvas editor first.');
|
|
395
|
+
|
|
396
|
+
const result = await this.page.evaluate(async (id) => {
|
|
397
|
+
const json = (r) => r.json();
|
|
398
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
399
|
+
const csrf = await (await fetch('/session/token', { credentials: 'same-origin' })).text();
|
|
400
|
+
const headers = { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf };
|
|
401
|
+
|
|
402
|
+
// Give the editor's debounced auto-save a moment to flush, then read the
|
|
403
|
+
// auto-saved layout (it includes the dragged-in component and Settings edits).
|
|
404
|
+
let data = null;
|
|
405
|
+
for (let i = 0; i < 20; i++) {
|
|
406
|
+
data = await fetch(`/canvas/api/v0/layout/canvas_page/${id}`, { credentials: 'same-origin' }).then(json).catch(() => null);
|
|
407
|
+
const content = data && Array.isArray(data.layout) && data.layout.find((r) => r.nodeType === 'region' && r.id === 'content');
|
|
408
|
+
if (content && Array.isArray(content.components) && content.components.length > 0) break;
|
|
409
|
+
await sleep(1000);
|
|
410
|
+
}
|
|
411
|
+
if (!data || !Array.isArray(data.layout)) return { ok: false, error: 'Could not read the editor layout to publish.' };
|
|
412
|
+
|
|
413
|
+
// Re-POST the layout to guarantee a pending auto-save, then publish it.
|
|
414
|
+
await fetch(`/canvas/api/v0/layout/canvas_page/${id}`, {
|
|
415
|
+
method: 'POST', credentials: 'same-origin', headers,
|
|
416
|
+
body: JSON.stringify({ layout: data.layout, model: data.model, autoSaves: data.autoSaves, clientInstanceId: crypto.randomUUID(), entity_form_fields: data.entity_form_fields }),
|
|
417
|
+
}).catch(() => {});
|
|
418
|
+
|
|
419
|
+
let pending = null;
|
|
420
|
+
for (let i = 0; i < 15; i++) {
|
|
421
|
+
pending = await fetch('/canvas/api/v0/auto-saves/pending', { credentials: 'same-origin' }).then(json).catch(() => null);
|
|
422
|
+
if (pending && pending.data && Object.keys(pending.data).length > 0) break;
|
|
423
|
+
await sleep(1000);
|
|
424
|
+
}
|
|
425
|
+
if (!pending || !pending.data || Object.keys(pending.data).length === 0) {
|
|
426
|
+
return { ok: false, error: 'No pending changes to publish (the editor auto-save never appeared).' };
|
|
427
|
+
}
|
|
428
|
+
const pub = await fetch('/canvas/api/v0/auto-saves/publish', { method: 'POST', credentials: 'same-origin', headers, body: JSON.stringify(pending.data) });
|
|
429
|
+
if (!pub.ok) return { ok: false, error: `Publish failed (${pub.status}): ${(await pub.text()).slice(0, 200)}` };
|
|
430
|
+
return { ok: true };
|
|
431
|
+
}, pageId);
|
|
432
|
+
|
|
433
|
+
if (!result || !result.ok) {
|
|
434
|
+
throw friendly('Could not publish the Canvas page changes.', (result && result.error) || 'Ensure there are pending changes and you can publish auto-saves.');
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Create (or replace) an empty published Drupal Canvas page, so a later editor
|
|
440
|
+
* step can add a component to it the human way. This only sets up the page
|
|
441
|
+
* container; it does not add components. Idempotent by path.
|
|
442
|
+
*
|
|
443
|
+
* Example: Given a new Canvas page "Test Hero Editor" at "/test-hero-editor"
|
|
444
|
+
*
|
|
445
|
+
* Example #1: Given there is a new Canvas page "Landing" at "/landing"
|
|
446
|
+
* Example #2: And I have a new Canvas page "Pricing" at "/pricing"
|
|
447
|
+
* Example #3: Given a new Canvas page "Team" at "/about/team"
|
|
448
|
+
* Example #4: And there is a new Canvas page "Events" at "/events"
|
|
449
|
+
* Example #5: Given I have a new Canvas page "Support" at "/support"
|
|
450
|
+
*/
|
|
451
|
+
When(/^(?:there is |I have )?a new Canvas page "([^"]*)" at "([^"]*)"$/, async function (title, path) {
|
|
452
|
+
const result = await this.page.evaluate(async ({ title, path }) => {
|
|
453
|
+
const json = (r) => r.json();
|
|
454
|
+
const csrf = await (await fetch('/session/token', { credentials: 'same-origin' })).text();
|
|
455
|
+
const headers = { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf };
|
|
456
|
+
const list = await fetch('/canvas/api/v0/content/canvas_page', { credentials: 'same-origin' }).then(json).catch(() => null);
|
|
457
|
+
const pages = (list && list.data) || [];
|
|
458
|
+
// REUSE an existing page at this path/title rather than delete+recreate.
|
|
459
|
+
// Deleting soft-deletes to trash, which keeps the path alias, so a recreate
|
|
460
|
+
// (on a cucumber retry or a re-run) hits "alias already in use" at publish.
|
|
461
|
+
// Reusing the same entity keeps one stable alias and never conflicts; we
|
|
462
|
+
// just reset it to an empty, published baseline so the test starts clean.
|
|
463
|
+
const existing = pages.find((p) => p.path === path) || pages.find((p) => p.title === title);
|
|
464
|
+
if (existing) {
|
|
465
|
+
const id = existing.id;
|
|
466
|
+
// Drop any stale auto-save, then publish an empty component tree so the
|
|
467
|
+
// page is blank again before the editor adds the component under test.
|
|
468
|
+
await fetch(`/canvas/api/v0/auto-saves/canvas_page/${id}`, { method: 'DELETE', credentials: 'same-origin', headers });
|
|
469
|
+
const data = await fetch(`/canvas/api/v0/layout/canvas_page/${id}`, { credentials: 'same-origin' }).then(json).catch(() => null);
|
|
470
|
+
if (data && Array.isArray(data.layout)) {
|
|
471
|
+
const content = data.layout.find((r) => r.nodeType === 'region' && r.id === 'content');
|
|
472
|
+
if (content && Array.isArray(content.components) && content.components.length > 0) {
|
|
473
|
+
content.components = [];
|
|
474
|
+
await fetch(`/canvas/api/v0/layout/canvas_page/${id}`, {
|
|
475
|
+
method: 'POST', credentials: 'same-origin', headers,
|
|
476
|
+
body: JSON.stringify({ layout: data.layout, model: data.model, autoSaves: data.autoSaves, clientInstanceId: crypto.randomUUID(), entity_form_fields: data.entity_form_fields }),
|
|
477
|
+
}).catch(() => {});
|
|
478
|
+
const pending = await fetch('/canvas/api/v0/auto-saves/pending', { credentials: 'same-origin' }).then(json).catch(() => null);
|
|
479
|
+
if (pending && pending.data && Object.keys(pending.data).length) {
|
|
480
|
+
await fetch('/canvas/api/v0/auto-saves/publish', { method: 'POST', credentials: 'same-origin', headers, body: JSON.stringify(pending.data) }).catch(() => {});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { ok: true };
|
|
485
|
+
}
|
|
486
|
+
const res = await fetch('/canvas/api/v0/content/canvas_page', {
|
|
487
|
+
method: 'POST', credentials: 'same-origin', headers,
|
|
488
|
+
body: JSON.stringify({ title, status: true, path: { alias: path }, components: [] }),
|
|
489
|
+
});
|
|
490
|
+
if (res.status !== 201) return { ok: false, error: `Create failed (${res.status}): ${(await res.text()).slice(0, 200)}` };
|
|
491
|
+
return { ok: true };
|
|
492
|
+
}, { title, path });
|
|
493
|
+
if (!result || !result.ok) {
|
|
494
|
+
throw friendly(`Could not create the "${title}" Canvas page.`, (result && result.error) || 'Ensure you are logged in as a user who can create Canvas pages.');
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Set an option on the component currently selected in the Drupal Canvas editor
|
|
500
|
+
* Settings panel, by its visible field label. Scoped to the component props
|
|
501
|
+
* form so it never clashes with same-named page fields (e.g. "Title").
|
|
502
|
+
* Dispatches input/change so Canvas auto-saves the change. Works for text
|
|
503
|
+
* inputs, textareas and select dropdowns (match the option by its visible text
|
|
504
|
+
* or its value).
|
|
505
|
+
*
|
|
506
|
+
* Example:
|
|
507
|
+
* When I set the Canvas component option "Background color" to "Primary"
|
|
508
|
+
* And I set the Canvas component option "Title" to "Configured hero"
|
|
509
|
+
*
|
|
510
|
+
* Example #1: When I set the Canvas component option "Heading" to "Welcome"
|
|
511
|
+
* Example #2: And I set the Canvas component option "Style" to "Primary"
|
|
512
|
+
* Example #3: When we set the Canvas component option "Alignment" to "Center"
|
|
513
|
+
* Example #4: And I set the Canvas component option "Background" to "Light"
|
|
514
|
+
* Example #5: When I set the Canvas component option "Link text" to "Read more"
|
|
515
|
+
*/
|
|
516
|
+
When(/^(?:I |we )*set the Canvas component option "([^"]*)" to "([^"]*)"$/, { timeout: 30000 }, async function (label, value) {
|
|
517
|
+
// The Settings panel populates asynchronously after a component is selected;
|
|
518
|
+
// wait for the labelled props field to be present before setting it.
|
|
519
|
+
await this.page.waitForFunction((lbl) => {
|
|
520
|
+
return [...document.querySelectorAll('[name^="canvas_component_props"]')].some((el) => {
|
|
521
|
+
const wrap = el.closest('.form-item, .js-form-item, [class*=formItem]');
|
|
522
|
+
return (el.labels && el.labels[0] && el.labels[0].textContent.trim() === lbl)
|
|
523
|
+
|| (wrap && [...wrap.querySelectorAll('label')].some((l) => l.textContent.trim() === lbl));
|
|
524
|
+
});
|
|
525
|
+
}, label, { timeout: 15000 }).catch(() => {});
|
|
526
|
+
|
|
527
|
+
const ok = await this.page.evaluate(({ label, value }) => {
|
|
528
|
+
const fields = [...document.querySelectorAll('[name^="canvas_component_props"]')];
|
|
529
|
+
const field = fields.find((el) => {
|
|
530
|
+
const byLabels = el.labels && el.labels[0] && el.labels[0].textContent.trim() === label;
|
|
531
|
+
const wrap = el.closest('.form-item, .js-form-item, [class*=formItem]');
|
|
532
|
+
const byWrap = wrap && [...wrap.querySelectorAll('label')].some((l) => l.textContent.trim() === label);
|
|
533
|
+
return byLabels || byWrap;
|
|
534
|
+
});
|
|
535
|
+
if (!field) return false;
|
|
536
|
+
if (field.tagName === 'SELECT') {
|
|
537
|
+
const opt = [...field.options].find((o) => o.text.trim() === value) || [...field.options].find((o) => o.value === value);
|
|
538
|
+
if (!opt) return false;
|
|
539
|
+
field.value = opt.value;
|
|
540
|
+
} else if (field.type === 'checkbox') {
|
|
541
|
+
const on = value === '1' || /^(true|on|yes|checked)$/i.test(value);
|
|
542
|
+
if (field.checked === on) return true;
|
|
543
|
+
field.checked = on;
|
|
544
|
+
} else if (field.type === 'radio') {
|
|
545
|
+
field.checked = true;
|
|
546
|
+
} else {
|
|
547
|
+
field.value = value;
|
|
548
|
+
}
|
|
549
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
550
|
+
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
551
|
+
field.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
552
|
+
return true;
|
|
553
|
+
}, { label, value });
|
|
554
|
+
if (!ok) {
|
|
555
|
+
throw friendly(`Could not set the Canvas component option "${label}".`, 'Open a component in the editor Settings panel first, and check the field label and option value.');
|
|
556
|
+
}
|
|
557
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Open a Canvas page in the real Drupal Canvas editor the way a site builder
|
|
562
|
+
* does: navigate to /canvas/editor/canvas_page/<id> and wait until the editor
|
|
563
|
+
* is ready to drive. Drupal Canvas is a heavy React SPA, so readiness is polled
|
|
564
|
+
* off the left toolbar "Library" button (it can take several seconds to mount)
|
|
565
|
+
* rather than a fixed sleep.
|
|
566
|
+
*
|
|
567
|
+
* Resolves the page id by its title, so create the page first (e.g. with a
|
|
568
|
+
* "a new Canvas page ..." step) and run a login step as a user who can edit
|
|
569
|
+
* Canvas pages (e.g. the webmaster).
|
|
570
|
+
*
|
|
571
|
+
* Example #1: When I open the "Landing" Canvas page in the editor
|
|
572
|
+
* Example #2: And I open the "Test Pattern Counters" Canvas page in the editor
|
|
573
|
+
* Example #3: When we open the "Home" Canvas page in the editor
|
|
574
|
+
* Example #4: And we open the "Test Pattern Header Footer" Canvas page in the editor
|
|
575
|
+
* Example #5: Given I open the "Pattern Library Check" Canvas page in the editor
|
|
576
|
+
*/
|
|
577
|
+
When(/^(?:I |we )*open the "([^"]*)" Canvas page in the editor$/, { timeout: 180000 }, async function (pageTitle) {
|
|
578
|
+
const id = await resolveCanvasPageId(this.page, pageTitle);
|
|
579
|
+
if (!id) throw friendly(`No Canvas page titled "${pageTitle}" was found.`, 'Create the page first with a "a new Canvas page ..." step.');
|
|
580
|
+
const editorUrl = `${this.launchUrl.replace(/\/$/, '')}/canvas/editor/canvas_page/${id}`;
|
|
581
|
+
const libraryButton = this.page.getByRole('button', { name: 'Library' }).first();
|
|
582
|
+
// The React editor occasionally fails to mount its toolbar under cumulative
|
|
583
|
+
// browser load; a fresh load recovers it. Reload-and-retry with an
|
|
584
|
+
// event-based wait for the "Library" button (Varbase E2E smart wait) instead of
|
|
585
|
+
// one long blind wait, so the step self-heals rather than leaning on the
|
|
586
|
+
// cucumber scenario retry.
|
|
587
|
+
let ready = false;
|
|
588
|
+
for (let attempt = 1; attempt <= 3 && !ready; attempt++) {
|
|
589
|
+
try {
|
|
590
|
+
await this.page.goto(editorUrl, { waitUntil: 'domcontentloaded' });
|
|
591
|
+
await libraryButton.waitFor({ state: 'visible', timeout: 45000 });
|
|
592
|
+
ready = true;
|
|
593
|
+
} catch (e) {
|
|
594
|
+
await smartSettle(this.page, 2000);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (!ready) {
|
|
598
|
+
throw friendly(`The Canvas editor for "${pageTitle}" did not become ready after 3 load attempts.`, 'The left toolbar "Library" button never appeared.');
|
|
599
|
+
}
|
|
600
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Open a named tab in the Drupal Canvas editor Library panel, the way a site
|
|
605
|
+
* builder browses ready-made sections. Opens the Library panel if it is closed
|
|
606
|
+
* (idempotent - it never toggles an already-open panel shut), activates the
|
|
607
|
+
* requested tab, and waits until the tab's list has actually populated (a known
|
|
608
|
+
* item is visible) so a following assertion or insert reads real, on-screen
|
|
609
|
+
* content - not an empty, still-loading panel.
|
|
610
|
+
*
|
|
611
|
+
* Only "Patterns" and "Components" are valid tab names. Open a page in the
|
|
612
|
+
* editor first.
|
|
613
|
+
*
|
|
614
|
+
* Example #1: When I open the "Patterns" tab in the Canvas Library
|
|
615
|
+
* Example #2: And I open the "Components" tab in the Canvas Library
|
|
616
|
+
* Example #3: When we open the "Patterns" tab in the Canvas Library
|
|
617
|
+
* Example #4: And we open the "Components" tab in the Canvas Library
|
|
618
|
+
* Example #5: Given I open the "Patterns" tab in the Canvas Library
|
|
619
|
+
*/
|
|
620
|
+
When(/^(?:I |we )*open the "(Patterns|Components)" tab in the Canvas Library$/, { timeout: 60000 }, async function (tabName) {
|
|
621
|
+
const key = tabName.toLowerCase();
|
|
622
|
+
const tabSelect = `canvas-library-${key}-tab-select`;
|
|
623
|
+
const tabContent = `canvas-library-${key}-tab-content`;
|
|
624
|
+
const probe = key === 'patterns' ? 'Counters' : 'Webform';
|
|
625
|
+
try {
|
|
626
|
+
const alreadyOpen = await this.page.getByTestId(tabSelect).isVisible().catch(() => false);
|
|
627
|
+
if (!alreadyOpen) {
|
|
628
|
+
await this.page.getByRole('button', { name: 'Library' }).first().click();
|
|
629
|
+
}
|
|
630
|
+
await this.page.getByTestId(tabSelect).waitFor({ state: 'visible', timeout: 20000 });
|
|
631
|
+
await this.page.getByTestId(tabSelect).click();
|
|
632
|
+
await this.page.getByTestId(tabContent).waitFor({ state: 'visible', timeout: 20000 });
|
|
633
|
+
await this.page.getByTestId(tabContent).getByText(probe, { exact: true }).first().waitFor({ state: 'visible', timeout: 20000 });
|
|
634
|
+
} catch (e) {
|
|
635
|
+
throw friendly(`Could not open the "${tabName}" tab in the Canvas Library.`, 'Open a page in the Canvas editor first, then check the Library panel opens and the tab populates.');
|
|
636
|
+
}
|
|
637
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Insert a default Canvas Pattern into the page open in the editor, exactly as a
|
|
642
|
+
* site builder does: right-click the pattern's row in the Library > Patterns
|
|
643
|
+
* list (a context-menu trigger) and choose "Insert". The whole section is added
|
|
644
|
+
* and the just-inserted component becomes selected, so the editor URL gains a
|
|
645
|
+
* fresh /component/<uuid>; the step waits for that uuid to change to confirm the
|
|
646
|
+
* insert registered. Inserting the same pattern twice therefore lands two
|
|
647
|
+
* independent copies (each gets its own uuid).
|
|
648
|
+
*
|
|
649
|
+
* Open the page in the editor and open the "Patterns" Library tab first. Use the
|
|
650
|
+
* pattern's visible human label (e.g. "Hero Slider", "Counters", "Site Header").
|
|
651
|
+
*
|
|
652
|
+
* Example #1: When I insert the "Hero Slider" pattern from the Canvas Library
|
|
653
|
+
* Example #2: And I insert the "Counters" pattern from the Canvas Library
|
|
654
|
+
* Example #3: And I insert the "Counters" pattern from the Canvas Library
|
|
655
|
+
* Example #4: When we insert the "Site Header" pattern from the Canvas Library
|
|
656
|
+
* Example #5: And we insert the "Site Footer" pattern from the Canvas Library
|
|
657
|
+
*/
|
|
658
|
+
When(/^(?:I |we )*insert the "([^"]*)" pattern from the Canvas Library$/, { timeout: 60000 }, async function (label) {
|
|
659
|
+
const uuidOf = (url) => (url.match(/\/component\/([0-9a-f-]+)/i) || [])[1] || null;
|
|
660
|
+
const before = uuidOf(this.page.url());
|
|
661
|
+
const row = this.page.getByTestId('canvas-library-patterns-tab-content').getByText(label, { exact: true }).first();
|
|
662
|
+
try {
|
|
663
|
+
await row.waitFor({ state: 'visible', timeout: 15000 });
|
|
664
|
+
await row.scrollIntoViewIfNeeded();
|
|
665
|
+
await row.click({ button: 'right' });
|
|
666
|
+
const insert = this.page.getByRole('menuitem', { name: 'Insert' });
|
|
667
|
+
await insert.waitFor({ state: 'visible', timeout: 10000 });
|
|
668
|
+
await insert.click();
|
|
669
|
+
await this.page.waitForFunction((prev) => {
|
|
670
|
+
const m = location.pathname.match(/\/component\/([0-9a-f-]+)/i);
|
|
671
|
+
return !!(m && m[1] !== prev);
|
|
672
|
+
}, before, { timeout: 25000 });
|
|
673
|
+
} catch (e) {
|
|
674
|
+
throw friendly(`Could not insert the "${label}" pattern from the Canvas Library.`, 'Open the "Patterns" Library tab first, and check the pattern label. Right-click on the row must open a menu with an "Insert" item.');
|
|
675
|
+
}
|
|
676
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Publish the pending Canvas changes through the editor's own Review/Publish
|
|
681
|
+
* widget, the way a site builder ships a page: click the topbar "Review N
|
|
682
|
+
* change(s)" button, "Select All" in the review list, then "Publish N selected".
|
|
683
|
+
* The step waits until the topbar widget resets to "No changes", confirming the
|
|
684
|
+
* publish landed. Drupal Canvas's review list is site-wide, so "Select All"
|
|
685
|
+
* publishes every pending change; drive this right after inserting on the page
|
|
686
|
+
* under test so only that page's changes are pending.
|
|
687
|
+
*
|
|
688
|
+
* Open a page in the editor and make at least one change (e.g. insert a pattern)
|
|
689
|
+
* first.
|
|
690
|
+
*
|
|
691
|
+
* Example #1: When I publish the Canvas page changes through the editor
|
|
692
|
+
* Example #2: And I publish the Canvas page changes through the editor
|
|
693
|
+
* Example #3: When we publish the Canvas page changes through the editor
|
|
694
|
+
* Example #4: And we publish the Canvas page changes through the editor
|
|
695
|
+
* Example #5: Then I publish the Canvas page changes through the editor
|
|
696
|
+
*/
|
|
697
|
+
When(/^(?:I |we )*publish the Canvas page changes through the editor$/, { timeout: 90000 }, async function () {
|
|
698
|
+
try {
|
|
699
|
+
const review = this.page.getByTestId('canvas-publish-review');
|
|
700
|
+
await review.waitFor({ state: 'visible', timeout: 20000 });
|
|
701
|
+
await review.click();
|
|
702
|
+
const selectAll = this.page.getByTestId('canvas-publish-review-select-all');
|
|
703
|
+
await selectAll.waitFor({ state: 'visible', timeout: 15000 });
|
|
704
|
+
await selectAll.click();
|
|
705
|
+
const publishButton = this.page.getByRole('button', { name: /Publish \d+ selected/i }).first();
|
|
706
|
+
await publishButton.waitFor({ state: 'visible', timeout: 15000 });
|
|
707
|
+
await publishButton.click();
|
|
708
|
+
await this.page.waitForFunction(() => {
|
|
709
|
+
const b = document.querySelector('[data-testid="canvas-publish-review"]');
|
|
710
|
+
return !!(b && /no changes/i.test(b.textContent));
|
|
711
|
+
}, null, { timeout: 40000 });
|
|
712
|
+
} catch (e) {
|
|
713
|
+
throw friendly('Could not publish the Canvas page changes through the editor.', 'Open a page in the editor and insert at least one change first. The Review -> Select All -> Publish widget must be reachable.');
|
|
714
|
+
}
|
|
715
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Assert that a Drupal Canvas Library panel does (or does not) list an item by
|
|
720
|
+
* its visible label. Scoped to the panel element by CSS selector (e.g. the
|
|
721
|
+
* Patterns or Components tab content) and read straight from the panel's
|
|
722
|
+
* on-screen text - the labels a site builder actually sees, not any API dump.
|
|
723
|
+
*
|
|
724
|
+
* This is the fast, panel-scoped counterpart of the generic "I should see ... in
|
|
725
|
+
* the ... element" assertion: it reads the panel's innerText once and checks the
|
|
726
|
+
* label against the panel's line items, so a library with a dozen checks stays
|
|
727
|
+
* quick and never trips a step timeout. Matches a whole line item exactly (so
|
|
728
|
+
* "Share" does not match "Social media menu" and an absent admin label is never
|
|
729
|
+
* a false positive from a longer entry).
|
|
730
|
+
*
|
|
731
|
+
* The panel selector is resolved from the named-selector registry
|
|
732
|
+
* (tests/selectors/<preset>.json): the "Patterns" tab maps to the
|
|
733
|
+
* "canvas patterns library" selector and "Components" to
|
|
734
|
+
* "canvas components library", so the feature reads by tab name and the CSS
|
|
735
|
+
* lives in one place. Open the page in the editor and open that Library tab
|
|
736
|
+
* first.
|
|
737
|
+
*
|
|
738
|
+
* Example #1: Then the Canvas Library "Patterns" tab should list "Hero Slider"
|
|
739
|
+
* Example #2: And the Canvas Library "Patterns" tab should list "Counters"
|
|
740
|
+
* Example #3: Then the Canvas Library "Components" tab should list "Webform"
|
|
741
|
+
* Example #4: And the Canvas Library "Components" tab should not list "Events Feed"
|
|
742
|
+
* Example #5: And the Canvas Library "Components" tab should not list "Recent pages"
|
|
743
|
+
*/
|
|
744
|
+
Then(/^the Canvas Library "(Patterns|Components)" tab should( not)? list "([^"]*)"$/, { timeout: 30000 }, async function (tab, negate, label) {
|
|
745
|
+
const selectorName = tab === 'Patterns' ? 'canvas patterns library' : 'canvas components library';
|
|
746
|
+
const registry = (this.__selectorsCss || {});
|
|
747
|
+
const selector = registry[selectorName] || `[data-testid='canvas-library-${tab.toLowerCase()}-tab-content']`;
|
|
748
|
+
const panel = this.page.locator(selector).first();
|
|
749
|
+
let text = '';
|
|
750
|
+
try {
|
|
751
|
+
await panel.waitFor({ state: 'visible', timeout: 15000 });
|
|
752
|
+
text = (await panel.innerText()) || '';
|
|
753
|
+
} catch (e) {
|
|
754
|
+
throw friendly(`Could not read the Canvas Library "${tab}" tab panel.`, 'Open the page in the editor and open that Library tab first.');
|
|
755
|
+
}
|
|
756
|
+
const items = text.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
757
|
+
const present = items.includes(label);
|
|
758
|
+
if (negate && present) {
|
|
759
|
+
throw friendly(`The Canvas Library "${tab}" tab should not list "${label}", but it does.`);
|
|
760
|
+
}
|
|
761
|
+
if (!negate && !present) {
|
|
762
|
+
throw friendly(`The Canvas Library "${tab}" tab should list "${label}", but it does not.`, `Panel items seen: ${items.join(', ')}`);
|
|
763
|
+
}
|
|
764
|
+
});
|