@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,458 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// -----------------------------------------------------------------------------
|
|
4
|
+
// Varbase step definitions: the Varbase testing users registry, the welcome
|
|
5
|
+
// tour, the default theme settings (sticky header), the international phone
|
|
6
|
+
// field, the editorial accessibility checker, the responsive warm-up pass and
|
|
7
|
+
// the working header / footer checklists.
|
|
8
|
+
//
|
|
9
|
+
// Ported from the Varbase Project BDD suites (11.0.x, 10.1.x, 9.2.x) so every
|
|
10
|
+
// Varbase site gets them from @vardot/varbase-e2e instead of copying them into
|
|
11
|
+
// each project's own tests/step-definitions/.
|
|
12
|
+
// -----------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
15
|
+
const assert = require('assert');
|
|
16
|
+
const { smartSettle, friendly } = require('./varbase-e2e');
|
|
17
|
+
const { budget, defaultThemeSettingsPath } = require('./drupal-helpers');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Authenticate a Varbase user defined in cucumber.js worldParameters.users.
|
|
21
|
+
*
|
|
22
|
+
* Example #1: Given I am a logged in user with the "webmaster" user
|
|
23
|
+
* Example #2: Given I am a logged in user with the "Content admin" user
|
|
24
|
+
* Example #3: Given I am a logged in user with the username "editor"
|
|
25
|
+
* Example #4: Given I am a logged in user with "admin"
|
|
26
|
+
* Example #5: And I am a logged in user with the "webmaster" user
|
|
27
|
+
*/
|
|
28
|
+
Given(/^I am a logged in user with( the)*( username)* "([^"]*)?"( user)*$/, async function (theCase, usernameCase, username, userCase) {
|
|
29
|
+
const users = this.parameters.users;
|
|
30
|
+
|
|
31
|
+
if (!(username in users)) {
|
|
32
|
+
throw friendly(
|
|
33
|
+
`User "${username}" is not configured.`,
|
|
34
|
+
`Add it to worldParameters.users in cucumber.js. Known users: ${Object.keys(users).join(', ')}.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const loginName = users[username].username || username;
|
|
39
|
+
const password = users[username].password;
|
|
40
|
+
if (password == null) return;
|
|
41
|
+
|
|
42
|
+
await this.page.goto(this.launchUrl + '/user/login', { waitUntil: 'domcontentloaded' });
|
|
43
|
+
// Wait for the username field to be actionable, fill, then submit by
|
|
44
|
+
// triggering the button's native click in-page. The Varbase login form is
|
|
45
|
+
// a standard POST; an in-page submit cannot be intercepted by the floating
|
|
46
|
+
// AI chatbot widget (a Playwright click would hit a 30s actionability
|
|
47
|
+
// timeout when the widget overlaps the button).
|
|
48
|
+
await this.page.waitForSelector('#edit-name', { state: 'visible', timeout: 15000 });
|
|
49
|
+
await this.page.fill('#edit-name', loginName);
|
|
50
|
+
await this.page.fill('#edit-pass', password);
|
|
51
|
+
await Promise.all([
|
|
52
|
+
this.page.waitForNavigation({ waitUntil: 'domcontentloaded' }).catch(() => {}),
|
|
53
|
+
this.page.evaluate(() => {
|
|
54
|
+
const submit = document.querySelector('#edit-submit');
|
|
55
|
+
if (submit) { submit.click(); return; }
|
|
56
|
+
const form = document.querySelector('#user-login-form') || document.forms[0];
|
|
57
|
+
if (form) form.submit();
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
// Smart-settle the post-login page so the next step sees a stable page.
|
|
61
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Fill an international telephone field (Webform `tel` element with
|
|
66
|
+
* `#international`, rendered by the intl-tel-input library). Setting the raw
|
|
67
|
+
* value with a plain "fill in" step is not enough: intl-tel-input validates
|
|
68
|
+
* the number with libphonenumber and rejects anything it cannot parse, so the
|
|
69
|
+
* webform's clientside/serverside validation fails. This step drives the
|
|
70
|
+
* intl-tel-input instance itself (`setNumber`) and dispatches input/blur so
|
|
71
|
+
* both the widget and the webform see a valid, formatted number.
|
|
72
|
+
*
|
|
73
|
+
* Pass the number in E.164 form (e.g. "+14155552671") for deterministic
|
|
74
|
+
* results regardless of the field's selected country.
|
|
75
|
+
*
|
|
76
|
+
* Example #1: When I fill in the international phone field with "+14155552671"
|
|
77
|
+
* Example #2: And I fill in the international phone number with "+442071838750"
|
|
78
|
+
* Example #3: When we fill in the international phone field with "+14155552671"
|
|
79
|
+
* Example #4: And I fill in the international phone field "Phone" with "+14155552671"
|
|
80
|
+
* Example #5: Given I fill in the international phone number with "+12025550143"
|
|
81
|
+
*/
|
|
82
|
+
When(/^(?:I |we )*fill in the international phone (?:field|number)(?: "[^"]*")? with "([^"]*)"$/, async function (number) {
|
|
83
|
+
const valid = await this.page.evaluate((num) => {
|
|
84
|
+
const g = window.intlTelInputGlobals;
|
|
85
|
+
if (!g || !g.instances) return null;
|
|
86
|
+
const keys = Object.keys(g.instances);
|
|
87
|
+
if (!keys.length) return null;
|
|
88
|
+
const inst = g.instances[keys[0]];
|
|
89
|
+
inst.setNumber(num);
|
|
90
|
+
const input = inst.telInput || document.querySelector('input[type="tel"]');
|
|
91
|
+
if (input) {
|
|
92
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
93
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
94
|
+
input.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
95
|
+
}
|
|
96
|
+
return inst.isValidNumber();
|
|
97
|
+
}, number);
|
|
98
|
+
|
|
99
|
+
if (valid === null) {
|
|
100
|
+
throw friendly(
|
|
101
|
+
'No intl-tel-input phone field was found on the page.',
|
|
102
|
+
'This step only works on a Webform "tel" element with #international enabled.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!valid) {
|
|
106
|
+
throw friendly(
|
|
107
|
+
`"${number}" is not a valid phone number for this field.`,
|
|
108
|
+
'Pass a libphonenumber-valid number in E.164 form, e.g. "+14155552671".'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Go to the default theme's settings page, resolved by machine name (not
|
|
115
|
+
* hard-coded), so the test works whatever the default theme is.
|
|
116
|
+
*
|
|
117
|
+
* Example #1: When I go to the default theme settings page
|
|
118
|
+
* Example #2: And I go to the default theme settings
|
|
119
|
+
* Example #3: When we go to the default theme settings page
|
|
120
|
+
* Example #4: And we go to the default theme settings
|
|
121
|
+
* Example #5: Given I go to the default theme settings page
|
|
122
|
+
*/
|
|
123
|
+
When(/^(?:I |we )*go to the default theme settings(?: page)?$/, async function () {
|
|
124
|
+
const path = await defaultThemeSettingsPath(this.page, this.launchUrl);
|
|
125
|
+
if (!path) throw friendly('Could not find the default theme settings link.', 'Open /admin/appearance as a user who can administer themes.');
|
|
126
|
+
await this.page.goto(`${this.launchUrl.replace(/\/$/, '')}${path}`, { waitUntil: 'domcontentloaded' });
|
|
127
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Enable or disable the default theme's "Sticky header" setting and save.
|
|
132
|
+
* The Gin-styled checkbox is visually hidden, so it is toggled in-page. Opens
|
|
133
|
+
* the default theme's settings form, sets the checkbox, and submits.
|
|
134
|
+
*
|
|
135
|
+
* Run a login step first (e.g. the webmaster).
|
|
136
|
+
*
|
|
137
|
+
* Example #1: When I enable the sticky header theme setting
|
|
138
|
+
* Example #2: And I disable the sticky header theme setting
|
|
139
|
+
* Example #3: When we enable the sticky header theme setting
|
|
140
|
+
* Example #4: And we disable the sticky header theme setting
|
|
141
|
+
* Example #5: Given I enable the sticky header theme setting
|
|
142
|
+
*/
|
|
143
|
+
When(/^(?:I |we )*(enable|disable) the sticky header theme setting$/, async function (action) {
|
|
144
|
+
const want = action === 'enable';
|
|
145
|
+
const settingsPath = await defaultThemeSettingsPath(this.page, this.launchUrl);
|
|
146
|
+
if (!settingsPath) throw friendly('Could not find the default theme settings link.', 'Open /admin/appearance as a user who can administer themes.');
|
|
147
|
+
await this.page.goto(`${this.launchUrl.replace(/\/$/, '')}${settingsPath}`, { waitUntil: 'domcontentloaded' });
|
|
148
|
+
const ok = await this.page.evaluate((on) => {
|
|
149
|
+
const cb = document.querySelector('input[name="sticky_header"]');
|
|
150
|
+
if (!cb) return false;
|
|
151
|
+
if (cb.checked !== on) cb.click();
|
|
152
|
+
const submit = document.getElementById('edit-submit') || [...document.querySelectorAll('input[type="submit"]')].find((b) => /save configuration/i.test(b.value));
|
|
153
|
+
if (!submit) return false;
|
|
154
|
+
submit.click();
|
|
155
|
+
return true;
|
|
156
|
+
}, want);
|
|
157
|
+
if (!ok) throw friendly('Could not toggle the "Sticky header" theme setting.', 'Open the default theme settings as a user who can administer the theme.');
|
|
158
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Warm up a page across every viewport breakpoint in the testing settings.
|
|
163
|
+
*
|
|
164
|
+
* The default theme renders responsive images via drimage_improved, which builds
|
|
165
|
+
* a different WebP derivative per rendered width. This visits the page once at
|
|
166
|
+
* each breakpoint from worldParameters.selectors.breakpoints (scrolling to the
|
|
167
|
+
* bottom to trigger lazy images) so every derivative is generated and cached to
|
|
168
|
+
* disk before the health checks assert on console errors. It makes no
|
|
169
|
+
* assertions; it only primes the cache.
|
|
170
|
+
*
|
|
171
|
+
* Example #1: When I warm up "/" at all testing breakpoints
|
|
172
|
+
* Example #2: And I warm up "/features" at all testing breakpoints
|
|
173
|
+
* Example #3: When we warm up "/blog" at all testing breakpoints
|
|
174
|
+
* Example #4: And I warm up "/contact-us" at all testing breakpoints
|
|
175
|
+
* Example #5: Given I warm up "/about-varbase" at all testing breakpoints
|
|
176
|
+
*/
|
|
177
|
+
When(/^(?:I |we )*warm up "([^"]*)" at all testing breakpoints$/, async function (path) {
|
|
178
|
+
const base = this.launchUrl.replace(/\/$/, '');
|
|
179
|
+
const url = path.startsWith('http') ? path : base + (path.startsWith('/') ? path : '/' + path);
|
|
180
|
+
const configured = (this.parameters.selectors && this.parameters.selectors.breakpoints) || {};
|
|
181
|
+
const breakpoints = Object.values(configured);
|
|
182
|
+
if (!breakpoints.length) {
|
|
183
|
+
breakpoints.push({ width: 1920, height: 1080 });
|
|
184
|
+
}
|
|
185
|
+
const budget = (this.minWaitTime && this.minWaitTime.page) || 8000;
|
|
186
|
+
for (const breakpoint of breakpoints) {
|
|
187
|
+
await this.page.setViewportSize({ width: breakpoint.width, height: breakpoint.height });
|
|
188
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
189
|
+
await smartSettle(this.page, budget);
|
|
190
|
+
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
191
|
+
await smartSettle(this.page, budget);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Click the "Next" button in the Varbase / Drupal Shepherd tour.
|
|
197
|
+
*
|
|
198
|
+
* Ports VarbaseContext::iClickNextInTour. The tour button lives in a
|
|
199
|
+
* shadow/overlay dialog; click it via an in-page native click after scrolling
|
|
200
|
+
* it into view.
|
|
201
|
+
*
|
|
202
|
+
* Example #1: When I click next button in tour
|
|
203
|
+
* Example #2: And I click next button in tour
|
|
204
|
+
* Example #3: When we click next button in tour
|
|
205
|
+
* Example #4: Given I click next button in tour
|
|
206
|
+
* Example #5: And we click next button in tour
|
|
207
|
+
*/
|
|
208
|
+
When(/^(?:I |we )*click next button in tour$/, async function () {
|
|
209
|
+
const ok = await this.page.evaluate(() => {
|
|
210
|
+
// Advance via the Shepherd API. The Drupal/Varbase tour renders each tip
|
|
211
|
+
// through Shepherd.js; its "Next" button's click handler does not reliably
|
|
212
|
+
// fire under a synthetic element.click() (the internal step pointer moves
|
|
213
|
+
// but the new tip does not render, or nothing happens at all), so drive the
|
|
214
|
+
// active tour directly — which shows the next step's tip exactly as the
|
|
215
|
+
// button is meant to. Fall back to clicking the button if the API is not
|
|
216
|
+
// exposed.
|
|
217
|
+
if (window.Shepherd && Shepherd.activeTour && typeof Shepherd.activeTour.next === 'function') {
|
|
218
|
+
Shepherd.activeTour.next();
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
const byText = [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === 'Next');
|
|
222
|
+
const btn = byText || document.querySelector('.shepherd-button:not(.shepherd-button-secondary)');
|
|
223
|
+
if (!btn) return false;
|
|
224
|
+
btn.scrollIntoView({ block: 'center' });
|
|
225
|
+
btn.click();
|
|
226
|
+
return true;
|
|
227
|
+
});
|
|
228
|
+
if (!ok) throw friendly('The "Next" button in the tour was not found.');
|
|
229
|
+
await smartSettle(this.page, budget(this));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Close the Varbase / Drupal Shepherd tour.
|
|
234
|
+
*
|
|
235
|
+
* Ports VarbaseContext::iCloseTour: tries the cancel/close control, then falls
|
|
236
|
+
* back to dispatching Escape.
|
|
237
|
+
*
|
|
238
|
+
* Example #1: When I close the tour
|
|
239
|
+
* Example #2: And I close the tour
|
|
240
|
+
* Example #3: When we close the tour
|
|
241
|
+
* Example #4: Given I close the tour
|
|
242
|
+
* Example #5: And we close the tour
|
|
243
|
+
*/
|
|
244
|
+
When(/^(?:I |we )*close the tour$/, async function () {
|
|
245
|
+
await this.page.evaluate(() => {
|
|
246
|
+
const btn = document.querySelector('.shepherd-cancel-icon, button[aria-label="Close"], .shepherd-button:last-child');
|
|
247
|
+
if (btn) { btn.scrollIntoView({ block: 'center' }); btn.click(); return; }
|
|
248
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
249
|
+
});
|
|
250
|
+
await smartSettle(this.page, budget(this));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Assert the Editoria11y accessibility checker panel is (or is not) present.
|
|
255
|
+
*
|
|
256
|
+
* Ports VarbaseContext::iShouldSeeTheAccessibilityChecker /
|
|
257
|
+
* iShouldNotSeeTheAccessibilityChecker. The checker mounts an
|
|
258
|
+
* <ed11y-element-panel> custom element.
|
|
259
|
+
*
|
|
260
|
+
* Example #1: Then I should see the accessibility checker
|
|
261
|
+
* Example #2: And I should see the a11y checker
|
|
262
|
+
* Example #3: Then should see a11y checker
|
|
263
|
+
* Example #4: And I should not see the accessibility checker
|
|
264
|
+
* Example #5: Then should not see a11y checker
|
|
265
|
+
*/
|
|
266
|
+
Then(/^(?:I |we )*should( not)? see (?:the )?(?:accessibility |a11y )?checker$/, async function (negate) {
|
|
267
|
+
const loc = this.page.locator('ed11y-element-panel');
|
|
268
|
+
const count = await loc.count().catch(() => 0);
|
|
269
|
+
if (negate) {
|
|
270
|
+
assert.strictEqual(count, 0, friendly('The accessibility checker panel was unexpectedly present.'));
|
|
271
|
+
} else {
|
|
272
|
+
assert.ok(count > 0, friendly('The accessibility checker panel was not found on the page.'));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Close the Editoria11y accessibility checker to clear space for more actions.
|
|
278
|
+
*
|
|
279
|
+
* Ports VarbaseContext::iCloseTheAccessibilityChecker: toggles the panel via
|
|
280
|
+
* its shadow-root toggle button.
|
|
281
|
+
*
|
|
282
|
+
* Example #1: When I close the accessibility checker
|
|
283
|
+
* Example #2: And I close the a11y checker
|
|
284
|
+
* Example #3: When we close the accessibility checker
|
|
285
|
+
* Example #4: Given I close the a11y checker
|
|
286
|
+
* Example #5: And we close the accessibility checker
|
|
287
|
+
*/
|
|
288
|
+
When(/^(?:I |we )*close (?:the )?(?:accessibility |a11y )?checker$/, async function () {
|
|
289
|
+
const ok = await this.page.evaluate(() => {
|
|
290
|
+
const panel = document.querySelector('ed11y-element-panel');
|
|
291
|
+
if (!panel || !panel.shadowRoot) return false;
|
|
292
|
+
const toggle = panel.shadowRoot.querySelector('#ed11y-toggle');
|
|
293
|
+
if (!toggle) return false;
|
|
294
|
+
toggle.click();
|
|
295
|
+
return true;
|
|
296
|
+
});
|
|
297
|
+
if (!ok) throw friendly('The accessibility checker toggle was not found.');
|
|
298
|
+
await smartSettle(this.page, budget(this));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Verify the page header is "working".
|
|
303
|
+
*
|
|
304
|
+
* On a Varbase site the Main navigation menu is rendered through the Drupal
|
|
305
|
+
* Canvas global Header region, so a working header means those primary links
|
|
306
|
+
* are present. Alter the links below to match your own site's main menu.
|
|
307
|
+
*
|
|
308
|
+
* Example: Then the page should have a working header
|
|
309
|
+
*
|
|
310
|
+
* Example #1: Then the page should have a working header
|
|
311
|
+
* Example #2: And I should have a working header
|
|
312
|
+
* Example #3: Then I should have a working header
|
|
313
|
+
* Example #4: And we should have a working header
|
|
314
|
+
* Example #5: Then the page should have a working header
|
|
315
|
+
*/
|
|
316
|
+
Then(/^(?:the page should have|(?:I |we )*should have) a working header$/, async function () {
|
|
317
|
+
// Smart-wait for the page to reach a quiet edge before reading the header.
|
|
318
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
319
|
+
|
|
320
|
+
// `banner` is the ARIA role of the site header (<header role="banner">).
|
|
321
|
+
const header = this.page.getByRole('banner').first();
|
|
322
|
+
const text = (await header.textContent().catch(() => '')) || '';
|
|
323
|
+
|
|
324
|
+
// Main navigation menu links expected in the header (one per line):
|
|
325
|
+
if (!text.includes('About Varbase')) throw friendly('Header is missing the "About Varbase" link.', 'Check the Main navigation menu in the Canvas Header region.');
|
|
326
|
+
if (!text.includes('Features')) throw friendly('Header is missing the "Features" link.', 'Check the Main navigation menu in the Canvas Header region.');
|
|
327
|
+
if (!text.includes('Blog')) throw friendly('Header is missing the "Blog" link.', 'Check the Main navigation menu in the Canvas Header region.');
|
|
328
|
+
if (!text.includes('Contact Us')) throw friendly('Header is missing the "Contact Us" link.', 'Check the Main navigation menu in the Canvas Header region.');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Verify the page footer is "working".
|
|
333
|
+
*
|
|
334
|
+
* On a Varbase site the Secondary, Footer and Social media menus are rendered
|
|
335
|
+
* through the Drupal Canvas global Footer region. A working footer means the
|
|
336
|
+
* footer link text is present, the social profiles are linked, and the credits
|
|
337
|
+
* and logos show. Alter the lines below to match your own site.
|
|
338
|
+
*
|
|
339
|
+
* Example: Then the page should have a working footer
|
|
340
|
+
*
|
|
341
|
+
* Example #1: Then the page should have a working footer
|
|
342
|
+
* Example #2: And I should have a working footer
|
|
343
|
+
* Example #3: Then I should have a working footer
|
|
344
|
+
* Example #4: And we should have a working footer
|
|
345
|
+
* Example #5: Then the page should have a working footer
|
|
346
|
+
*/
|
|
347
|
+
Then(/^(?:the page should have|(?:I |we )*should have) a working footer$/, async function () {
|
|
348
|
+
// Smart-wait for the page to reach a quiet edge before reading the footer.
|
|
349
|
+
await smartSettle(this.page, (this.minWaitTime && this.minWaitTime.page) || 8000);
|
|
350
|
+
|
|
351
|
+
// `contentinfo` is the ARIA role of the site footer (<footer role="contentinfo">).
|
|
352
|
+
// Scope to the role so card/section <footer> elements elsewhere are ignored.
|
|
353
|
+
const footer = this.page.getByRole('contentinfo').first();
|
|
354
|
+
const text = (await footer.textContent().catch(() => '')) || '';
|
|
355
|
+
|
|
356
|
+
// Secondary menu (Footer sitemap / Quicklinks) link text (one per line):
|
|
357
|
+
if (!text.includes('Quicklinks')) throw friendly('Footer is missing the "Quicklinks" heading.', 'Check the Secondary menu in the Canvas Footer region.');
|
|
358
|
+
if (!text.includes('Features')) throw friendly('Footer is missing the "Features" link.', 'Check the Secondary menu in the Canvas Footer region.');
|
|
359
|
+
if (!text.includes('About Varbase')) throw friendly('Footer is missing the "About Varbase" link.', 'Check the Secondary menu in the Canvas Footer region.');
|
|
360
|
+
|
|
361
|
+
// Footer menu (Support) link text (one per line):
|
|
362
|
+
if (!text.includes('Support')) throw friendly('Footer is missing the "Support" heading.', 'Check the Footer menu in the Canvas Footer region.');
|
|
363
|
+
if (!text.includes('Documentation')) throw friendly('Footer is missing the "Documentation" link.', 'Check the Footer menu in the Canvas Footer region.');
|
|
364
|
+
if (!text.includes('Get Professional Support')) throw friendly('Footer is missing the "Get Professional Support" link.', 'Check the Footer menu in the Canvas Footer region.');
|
|
365
|
+
if (!text.includes('Community Support')) throw friendly('Footer is missing the "Community Support" link.', 'Check the Footer menu in the Canvas Footer region.');
|
|
366
|
+
|
|
367
|
+
// Footer credit and legal text (one per line):
|
|
368
|
+
if (!text.includes('Enjoy the free software, or')) throw friendly('Footer is missing the "Enjoy the free software, or get a quote ..." credit.');
|
|
369
|
+
if (!text.includes('get a quote')) throw friendly('Footer is missing the "get a quote" link text.');
|
|
370
|
+
if (!text.includes('Terms and Conditions')) throw friendly('Footer is missing the "Terms and Conditions" link.');
|
|
371
|
+
if (!text.includes('Powered by')) throw friendly('Footer is missing the "Powered by" credit.');
|
|
372
|
+
|
|
373
|
+
// Social media menu profiles - full links (one per line):
|
|
374
|
+
if ((await footer.locator('a[href="https://www.linkedin.com/company/vardot"]').count()) === 0) throw friendly('Footer is missing the LinkedIn link (https://www.linkedin.com/company/vardot).', 'Check the Social media menu in the Canvas Footer region.');
|
|
375
|
+
if ((await footer.locator('a[href="https://www.facebook.com/vardotters/"]').count()) === 0) throw friendly('Footer is missing the Facebook link (https://www.facebook.com/vardotters/).', 'Check the Social media menu in the Canvas Footer region.');
|
|
376
|
+
if ((await footer.locator('a[href="https://www.instagram.com/vardotters/"]').count()) === 0) throw friendly('Footer is missing the Instagram link (https://www.instagram.com/vardotters/).', 'Check the Social media menu in the Canvas Footer region.');
|
|
377
|
+
if ((await footer.locator('a[href="https://x.com/Vardot"]').count()) === 0) throw friendly('Footer is missing the X link (https://x.com/Vardot).', 'Check the Social media menu in the Canvas Footer region.');
|
|
378
|
+
|
|
379
|
+
// Footer logos (one per line):
|
|
380
|
+
if ((await footer.locator('img[alt="Varbase logo"]').count()) === 0) throw friendly('Footer is missing the Varbase logo.');
|
|
381
|
+
if ((await footer.locator('img[alt="Vardot"]').count()) === 0) throw friendly('Footer is missing the Vardot logo next to "Powered by".');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Press a responsive-preview device button (Drupal core Responsive preview).
|
|
386
|
+
*
|
|
387
|
+
* Ports VarbaseContext::iPressResponsivePreviewDeviceButton: clicks the control
|
|
388
|
+
* carrying data-responsive-preview-name.
|
|
389
|
+
*
|
|
390
|
+
* Example #1: When I press the "desktop" responsive preview device button
|
|
391
|
+
* Example #2: And I press the "mobile" responsive preview device button
|
|
392
|
+
* Example #3: When we press the "tablet" responsive preview device button
|
|
393
|
+
* Example #4: And I press the "widescreen" responsive preview device button
|
|
394
|
+
* Example #5: When I press the "mobile" responsive preview device button
|
|
395
|
+
*/
|
|
396
|
+
When(/^(?:I |we )*press the "([^"]*)" responsive preview device button$/, async function (deviceName) {
|
|
397
|
+
const ok = await this.page.evaluate((deviceName) => {
|
|
398
|
+
const btn = document.querySelector(`[data-responsive-preview-name="${deviceName}"]`);
|
|
399
|
+
if (!btn) return false;
|
|
400
|
+
btn.click();
|
|
401
|
+
return true;
|
|
402
|
+
}, deviceName);
|
|
403
|
+
if (!ok) throw friendly(`The "${deviceName}" responsive preview device option was not found.`);
|
|
404
|
+
await smartSettle(this.page, budget(this));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Assert an element renders as a sticky element (CSS position: sticky).
|
|
409
|
+
*
|
|
410
|
+
* Resolves a named selector from the registry (falling back to a raw CSS
|
|
411
|
+
* selector) and checks its computed position. Reads better in a feature than
|
|
412
|
+
* asserting on a raw "position:sticky;" CSS property string.
|
|
413
|
+
*
|
|
414
|
+
* Example #1: Then the "site header" should be sticky
|
|
415
|
+
* Example #2: And the "site header" should be sticky
|
|
416
|
+
* Example #3: Then the "site header" should not be sticky
|
|
417
|
+
* Example #4: And the "footer" should not be sticky
|
|
418
|
+
* Example #5: Then the "main content" should be sticky
|
|
419
|
+
*/
|
|
420
|
+
Then(/^the "([^"]*)" should( not)? be sticky$/, async function (name, negate) {
|
|
421
|
+
const selector = (this.__selectorsCss && this.__selectorsCss[name]) || name;
|
|
422
|
+
const position = await this.page.locator(selector).first().evaluate((el) => window.getComputedStyle(el).position);
|
|
423
|
+
if (negate) {
|
|
424
|
+
assert.notStrictEqual(position, 'sticky', friendly(`Expected "${name}" not to be sticky, but its computed position is "${position}".`));
|
|
425
|
+
} else {
|
|
426
|
+
assert.strictEqual(position, 'sticky', friendly(`Expected "${name}" to be sticky, but its computed position is "${position}".`));
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Assert a named element gains (or loses) a CSS class, with auto-retry.
|
|
432
|
+
*
|
|
433
|
+
* Resolves a named selector from the registry (falling back to a raw CSS
|
|
434
|
+
* selector) and polls its class list. Use this instead of the raw-selector
|
|
435
|
+
* web-first step when the feature should read with a registered name.
|
|
436
|
+
*
|
|
437
|
+
* Example #1: Then the "site header" should have the "scrolled" class within 5 seconds
|
|
438
|
+
* Example #2: And the "site header" should have the "scrolled" class
|
|
439
|
+
* Example #3: Then the "site header" should not have the "scrolled" class
|
|
440
|
+
* Example #4: And the "main content" should have the "is-active" class within 3 seconds
|
|
441
|
+
* Example #5: Then the "footer" should not have the "scrolled" class
|
|
442
|
+
*/
|
|
443
|
+
Then(/^the "([^"]*)" should( not)? have the "([^"]*)" class(?: within (\d+) seconds?)?$/, async function (name, negate, cls, sec) {
|
|
444
|
+
const selector = (this.__selectorsCss && this.__selectorsCss[name]) || name;
|
|
445
|
+
const loc = this.page.locator(selector).first();
|
|
446
|
+
const timeout = (sec ? parseInt(sec, 10) : 5) * 1000;
|
|
447
|
+
const want = !negate;
|
|
448
|
+
const start = Date.now();
|
|
449
|
+
let classes = '';
|
|
450
|
+
do {
|
|
451
|
+
classes = (await loc.getAttribute('class')) || '';
|
|
452
|
+
if (classes.split(/\s+/).includes(cls) === want) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
await this.page.waitForTimeout(100);
|
|
456
|
+
} while (Date.now() - start < timeout);
|
|
457
|
+
throw friendly(`Expected "${name}" ${want ? 'to have' : 'not to have'} the "${cls}" class within ${timeout / 1000}s; last class was "${classes}".`);
|
|
458
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { friendly } = require('./varbase-e2e');
|
|
4
|
+
|
|
5
|
+
// Video recording steps — start / stop / save mid-scenario.
|
|
6
|
+
//
|
|
7
|
+
// Playwright records video only at browser-context creation. Mid-scenario
|
|
8
|
+
// `start` / `stop` therefore work by tearing down the current page +
|
|
9
|
+
// context and reopening with the new recording flag. Page state is lost
|
|
10
|
+
// across the boundary — call `start` BEFORE any navigation and `stop` only
|
|
11
|
+
// when you no longer need the current page state.
|
|
12
|
+
//
|
|
13
|
+
// Default mode is 'off' (no recording). See cucumber.js → worldParameters.video.
|
|
14
|
+
|
|
15
|
+
const { When, Then } = require('@cucumber/cucumber');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
function settings(world) {
|
|
20
|
+
const cfg = (world.parameters && world.parameters.video) || {};
|
|
21
|
+
return {
|
|
22
|
+
dir: process.env.VARBASE_E2E_VIDEO_DIR || cfg.dir || './videos',
|
|
23
|
+
size: cfg.size || { width: 1280, height: 720 },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function rebuildContext(world, recordVideo) {
|
|
28
|
+
await world.closeBrowser();
|
|
29
|
+
if (recordVideo) {
|
|
30
|
+
const s = settings(world);
|
|
31
|
+
fs.mkdirSync(s.dir, { recursive: true });
|
|
32
|
+
world._videoRequested = true;
|
|
33
|
+
await world.openBrowser({ recordVideo: { dir: s.dir, size: s.size } });
|
|
34
|
+
} else {
|
|
35
|
+
world._videoRequested = false;
|
|
36
|
+
await world.openBrowser();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Start recording the browser as a webm video. Closes the current page +
|
|
42
|
+
* context and reopens with `recordVideo` enabled. Place BEFORE any
|
|
43
|
+
* navigation in the scenario.
|
|
44
|
+
*
|
|
45
|
+
* Example #1: When I start video recording
|
|
46
|
+
* Example #2: And I start video recording
|
|
47
|
+
* Example #3: Given I start video recording
|
|
48
|
+
* Example #4: But I start video recording
|
|
49
|
+
* Example #5: Then I start video recording
|
|
50
|
+
*
|
|
51
|
+
*/
|
|
52
|
+
When(/^(I |we )*start video recording$/, async function (pronoun) {
|
|
53
|
+
try {
|
|
54
|
+
await rebuildContext(this, true);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
throw friendly({
|
|
57
|
+
action: 'start video recording',
|
|
58
|
+
cause: e,
|
|
59
|
+
hint: `the videos directory (${(this.parameters && this.parameters.video && this.parameters.video.dir) || './videos'}) must be writable.`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Stop recording. Closes the current page + context (which flushes the
|
|
66
|
+
* webm to disk) and reopens a fresh, non-recording browser so the rest
|
|
67
|
+
* of the scenario can continue.
|
|
68
|
+
*
|
|
69
|
+
* Example #1: When I stop video recording
|
|
70
|
+
* Example #2: And I stop video recording
|
|
71
|
+
* Example #3: But I stop video recording
|
|
72
|
+
* Example #4: Then I stop video recording
|
|
73
|
+
* Example #5: Then I stop video recording
|
|
74
|
+
*
|
|
75
|
+
*/
|
|
76
|
+
When(/^(I |we )*stop video recording$/, async function (pronoun) {
|
|
77
|
+
try {
|
|
78
|
+
if (!this.page || !this._videoRequested) return;
|
|
79
|
+
const video = this.page.video();
|
|
80
|
+
// Close context first — flushes the webm — then saveAs while the
|
|
81
|
+
// browser is still alive, finally close the browser and reopen fresh.
|
|
82
|
+
try { await this.context.close(); } catch { /* ignore */ }
|
|
83
|
+
if (video) {
|
|
84
|
+
try {
|
|
85
|
+
const s = settings(this);
|
|
86
|
+
const name = this._videoSaveAsName || `manual.${Date.now()}.webm`;
|
|
87
|
+
const dest = path.join(s.dir, name);
|
|
88
|
+
await video.saveAs(dest);
|
|
89
|
+
await video.delete().catch(() => {});
|
|
90
|
+
process.stderr.write(`\n[varbase-e2e] video saved → ${dest}\n`);
|
|
91
|
+
// Reset so the After hook does not try to re-save.
|
|
92
|
+
this._videoSaveAsName = null;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
process.stderr.write(`\n[varbase-e2e] video save failed: ${e.message}\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (this.playwrightBrowser) {
|
|
98
|
+
try { await this.playwrightBrowser.close(); } catch { /* ignore */ }
|
|
99
|
+
this.playwrightBrowser = null;
|
|
100
|
+
}
|
|
101
|
+
this.context = null;
|
|
102
|
+
this.page = null;
|
|
103
|
+
this._videoRequested = false;
|
|
104
|
+
await this.openBrowser();
|
|
105
|
+
} catch (e) {
|
|
106
|
+
throw friendly({
|
|
107
|
+
action: 'stop video recording',
|
|
108
|
+
cause: e,
|
|
109
|
+
hint: 'the scenario will continue with a fresh, non-recording browser.',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reserve a custom filename for the current scenario's video. The actual
|
|
116
|
+
* save happens at scenario end (Playwright's `video.saveAs()` blocks
|
|
117
|
+
* until recording finishes, so we cannot copy bytes mid-flight). If the
|
|
118
|
+
* scenario calls `stop video recording` before scenario end, the manual
|
|
119
|
+
* webm uses this name instead of the automatic timestamped one.
|
|
120
|
+
*
|
|
121
|
+
* Example #1: When I save the current video as "checkout-flow.webm"
|
|
122
|
+
* Example #2: When I save the current video as "regression-21.webm"
|
|
123
|
+
* Example #3: And I save the current video as "smoke.webm"
|
|
124
|
+
* Example #4: Then I save the current video as "demo.webm"
|
|
125
|
+
* Example #5: But I save the current video as "rerun.webm"
|
|
126
|
+
*
|
|
127
|
+
*/
|
|
128
|
+
When(/^(I |we )*save the current video as "([^"]*)"$/, function (pronoun, name) {
|
|
129
|
+
try {
|
|
130
|
+
if (!this._videoRequested) {
|
|
131
|
+
throw new Error('recording is not active');
|
|
132
|
+
}
|
|
133
|
+
if (!name || !name.trim()) {
|
|
134
|
+
throw new Error('filename is empty');
|
|
135
|
+
}
|
|
136
|
+
this._videoSaveAsName = name;
|
|
137
|
+
} catch (e) {
|
|
138
|
+
throw friendly({
|
|
139
|
+
action: `save the current video as "${name}"`,
|
|
140
|
+
cause: e,
|
|
141
|
+
hint: 'call "When I start video recording" first, or set worldParameters.video.mode to "on".',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Print the path that the current video will be written to. Diagnostic
|
|
148
|
+
* only — never asserts, never fails.
|
|
149
|
+
*
|
|
150
|
+
* Example #1: Then print video path
|
|
151
|
+
* Example #2: And print video path
|
|
152
|
+
* Example #3: But print video path
|
|
153
|
+
* Example #4: Then print video path
|
|
154
|
+
* Example #5: Then print video path
|
|
155
|
+
*
|
|
156
|
+
*/
|
|
157
|
+
Then(/^print video path$/, async function () {
|
|
158
|
+
// Diagnostic only — swallow every error and report inline.
|
|
159
|
+
try {
|
|
160
|
+
if (!this.page || !this._videoRequested) {
|
|
161
|
+
console.log('\n--- Video path: recording is OFF ---');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const video = this.page.video();
|
|
165
|
+
if (!video) {
|
|
166
|
+
console.log('\n--- Video path: no video object on this page ---');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const p = await video.path();
|
|
171
|
+
console.log(`\n--- Video path: ${p} ---`);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.log(`\n--- Video path unavailable: ${e.message} ---`);
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
console.log(`\n--- Video path error: ${e.message} ---`);
|
|
177
|
+
}
|
|
178
|
+
});
|