@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,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// -----------------------------------------------------------------------------
|
|
4
|
+
// Paragraphs step definitions: choose a paragraph component in the add-widget
|
|
5
|
+
// dialog of a paragraphs field.
|
|
6
|
+
//
|
|
7
|
+
// Ported from the Varbase Project BDD suites (11.0.x, 10.1.x, 9.2.x) so every
|
|
8
|
+
// Varbase site gets them from @vardot/varbase-e2e instead of copying them into
|
|
9
|
+
// each project's own tests/step-definitions/.
|
|
10
|
+
// -----------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const { When } = require('@cucumber/cucumber');
|
|
13
|
+
const { smartSettle, friendly } = require('./varbase-e2e');
|
|
14
|
+
const { budget } = require('./drupal-helpers');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Select a paragraph component in the "Add component" dialog.
|
|
18
|
+
*
|
|
19
|
+
* Ports VarbaseContext::iSelectTheParagraphComponent: clicks the add button in
|
|
20
|
+
* the paragraphs-add-dialog whose name matches the component.
|
|
21
|
+
*
|
|
22
|
+
* Example #1: When I select the "Text" paragraph component
|
|
23
|
+
* Example #2: And I select the "Modal" paragraph component
|
|
24
|
+
* Example #3: When we select the "Drupal block" paragraph component
|
|
25
|
+
* Example #4: And I select the "Rich Text" paragraph component
|
|
26
|
+
* Example #5: Given I select the "Accordion" paragraph component
|
|
27
|
+
*/
|
|
28
|
+
When(/^(?:I |we )*select the "([^"]*)" paragraph component$/, async function (component) {
|
|
29
|
+
const ok = await this.page.evaluate((component) => {
|
|
30
|
+
const dialog = document.querySelector('.paragraphs-add-dialog.ui-dialog-content, .paragraphs-add-dialog');
|
|
31
|
+
const scope = dialog || document;
|
|
32
|
+
const btn = [...scope.querySelectorAll('input, button')]
|
|
33
|
+
.find((b) => (b.getAttribute('name') || '').includes(component) || (b.value || '').includes(component) || (b.textContent || '').includes(component));
|
|
34
|
+
if (!btn) return false;
|
|
35
|
+
// The paragraphs "Add <type>" buttons are Drupal AJAX submit buttons whose
|
|
36
|
+
// handler is bound to the `mousedown` event (Drupal core ajax.js binds form
|
|
37
|
+
// buttons on mousedown), so a bare element.click() fires only a `click`
|
|
38
|
+
// event and the AJAX that inserts the paragraph subform never runs — the
|
|
39
|
+
// dialog just sits there and the subform (and its CKEditor field) never
|
|
40
|
+
// appears. Dispatch the full pointer sequence so the mousedown-bound AJAX
|
|
41
|
+
// fires and the subform is added.
|
|
42
|
+
btn.scrollIntoView({ block: 'center' });
|
|
43
|
+
for (const type of ['mousedown', 'mouseup', 'click']) {
|
|
44
|
+
btn.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}, component);
|
|
48
|
+
if (!ok) throw friendly(`Could not find the "${component}" paragraph component in the add dialog.`);
|
|
49
|
+
await smartSettle(this.page, budget(this));
|
|
50
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { friendly } = require('./varbase-e2e');
|
|
4
|
+
|
|
5
|
+
// Element interactions and visual / positional assertions using Playwright.
|
|
6
|
+
|
|
7
|
+
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Assert element A is positioned vertically below element B (greater Y).
|
|
12
|
+
*
|
|
13
|
+
* Example #1: Then the element "#footer" should appear after the element "#main"
|
|
14
|
+
* Example #2: Then the element ".checkout" should appear after the element ".cart"
|
|
15
|
+
* Example #3: And the element "#summary" should appear after the element "#details"
|
|
16
|
+
* Example #4: Then the element "h2" should appear after the element "h1"
|
|
17
|
+
* Example #5: Then the element "[data-testid=cta]" should appear after the element ".hero"
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
20
|
+
Then('the element {string} should appear after the element {string}', async function (sel1, sel2) {
|
|
21
|
+
const el1 = await this.page.locator(sel1).first();
|
|
22
|
+
const el2 = await this.page.locator(sel2).first();
|
|
23
|
+
const b1 = await el1.boundingBox();
|
|
24
|
+
const b2 = await el2.boundingBox();
|
|
25
|
+
assert.ok(b1 && b2, 'Both elements must be visible.');
|
|
26
|
+
assert.ok(b1.y > b2.y, `Element "${sel1}" should appear after "${sel2}".`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Assert text A appears after text B in the document body's text order.
|
|
31
|
+
*
|
|
32
|
+
* Example #1: Then the text "Sign in" should appear after the text "Welcome"
|
|
33
|
+
* Example #2: Then the text "Total" should appear after the text "Subtotal"
|
|
34
|
+
* Example #3: And the text "Footer" should appear after the text "Header"
|
|
35
|
+
* Example #4: Then the text "Privacy" should appear after the text "Terms"
|
|
36
|
+
* Example #5: Then the text "Step 3" should appear after the text "Step 2"
|
|
37
|
+
*
|
|
38
|
+
*/
|
|
39
|
+
Then('the text {string} should appear after the text {string}', async function (t1, t2) {
|
|
40
|
+
const body = await this.page.locator('body').innerText();
|
|
41
|
+
const i1 = body.indexOf(t1);
|
|
42
|
+
const i2 = body.indexOf(t2);
|
|
43
|
+
assert.ok(i1 !== -1, `Text "${t1}" not found.`);
|
|
44
|
+
assert.ok(i2 !== -1, `Text "${t2}" not found.`);
|
|
45
|
+
assert.ok(i1 > i2, `Text "${t1}" should appear after "${t2}".`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Assert at least one element matching `<selector>[<attr>="<value>"]` exists.
|
|
50
|
+
*
|
|
51
|
+
* Example #1: Then the element "a" with the attribute "href" and the value "/about" should exist
|
|
52
|
+
* Example #2: Then the element "input" with the attribute "name" and the value "email" should exist
|
|
53
|
+
* Example #3: And the element "button" with the attribute "type" and the value "submit" should exist
|
|
54
|
+
* Example #4: Then the element "div" with the attribute "data-testid" and the value "user-list" should exist
|
|
55
|
+
* Example #5: Then the element "img" with the attribute "alt" and the value "Logo" should exist
|
|
56
|
+
*
|
|
57
|
+
*/
|
|
58
|
+
Then('the element {string} with the attribute {string} and the value {string} should exist', async function (sel, attr, value) {
|
|
59
|
+
const count = await this.page.locator(`${sel}[${attr}="${value}"]`).count();
|
|
60
|
+
assert.ok(count > 0, `Element "${sel}" with ${attr}="${value}" not found.`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Assert at least one element with attribute value containing a substring exists.
|
|
65
|
+
*
|
|
66
|
+
* Example #1: Then the element "a" with the attribute "href" and the value containing "/about" should exist
|
|
67
|
+
* Example #2: Then the element "input" with the attribute "class" and the value containing "is-valid" should exist
|
|
68
|
+
* Example #3: And the element "button" with the attribute "data-testid" and the value containing "cta" should exist
|
|
69
|
+
* Example #4: Then the element "img" with the attribute "src" and the value containing ".png" should exist
|
|
70
|
+
* Example #5: Then the element "div" with the attribute "aria-label" and the value containing "card" should exist
|
|
71
|
+
*
|
|
72
|
+
*/
|
|
73
|
+
Then('the element {string} with the attribute {string} and the value containing {string} should exist', async function (sel, attr, value) {
|
|
74
|
+
const count = await this.page.locator(`${sel}[${attr}*="${value}"]`).count();
|
|
75
|
+
assert.ok(count > 0, `Element "${sel}" with ${attr} containing "${value}" not found.`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Assert NO element matches `<selector>[<attr>="<value>"]`.
|
|
80
|
+
*
|
|
81
|
+
* Example #1: Then the element "a" with the attribute "href" and the value "/admin" should not exist
|
|
82
|
+
* Example #2: Then the element "input" with the attribute "type" and the value "hidden" should not exist
|
|
83
|
+
* Example #3: And the element "button" with the attribute "disabled" and the value "true" should not exist
|
|
84
|
+
* Example #4: Then the element "div" with the attribute "data-testid" and the value "error" should not exist
|
|
85
|
+
* Example #5: Then the element "img" with the attribute "alt" and the value "" should not exist
|
|
86
|
+
*
|
|
87
|
+
*/
|
|
88
|
+
Then('the element {string} with the attribute {string} and the value {string} should not exist', async function (sel, attr, value) {
|
|
89
|
+
const count = await this.page.locator(`${sel}[${attr}="${value}"]`).count();
|
|
90
|
+
assert.strictEqual(count, 0, `Element "${sel}" with ${attr}="${value}" should not exist.`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Assert NO element has attribute value containing a substring.
|
|
95
|
+
*
|
|
96
|
+
* Example #1: Then the element "a" with the attribute "href" and the value containing "/old" should not exist
|
|
97
|
+
* Example #2: Then the element "div" with the attribute "class" and the value containing "is-error" should not exist
|
|
98
|
+
* Example #3: And the element "button" with the attribute "data-testid" and the value containing "deprecated" should not exist
|
|
99
|
+
* Example #4: Then the element "img" with the attribute "src" and the value containing "tracking" should not exist
|
|
100
|
+
* Example #5: Then the element "input" with the attribute "name" and the value containing "_legacy" should not exist
|
|
101
|
+
*
|
|
102
|
+
*/
|
|
103
|
+
Then('the element {string} with the attribute {string} and the value containing {string} should not exist', async function (sel, attr, value) {
|
|
104
|
+
const count = await this.page.locator(`${sel}[${attr}*="${value}"]`).count();
|
|
105
|
+
assert.strictEqual(count, 0, `Element "${sel}" with ${attr} containing "${value}" should not exist.`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Assert an element is positioned within the top 100 px of the viewport.
|
|
110
|
+
*
|
|
111
|
+
* Example #1: Then the element "#header" should be at the top of the viewport
|
|
112
|
+
* Example #2: Then the element "h1" should be at the top of the viewport
|
|
113
|
+
* Example #3: And the element ".sticky-nav" should be at the top of the viewport
|
|
114
|
+
* Example #4: When I scroll to the top
|
|
115
|
+
* Then the element "#hero" should be at the top of the viewport
|
|
116
|
+
* Example #5: Then the element "[role=banner]" should be at the top of the viewport
|
|
117
|
+
*
|
|
118
|
+
*/
|
|
119
|
+
Then('the element {string} should be at the top of the viewport', async function (sel) {
|
|
120
|
+
const box = await this.page.locator(sel).first().boundingBox();
|
|
121
|
+
assert.ok(box, `Element "${sel}" not found or not visible.`);
|
|
122
|
+
assert.ok(box.y >= 0 && box.y <= 100, `Element "${sel}" is not at top of viewport (y=${box.y}).`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Assert an element is horizontally centered in the viewport (±10% tolerance).
|
|
127
|
+
*
|
|
128
|
+
* Example #1: Then the element ".hero" should be centered in the viewport
|
|
129
|
+
* Example #2: Then the element "#cta" should be centered in the viewport
|
|
130
|
+
* Example #3: And the element ".modal-dialog" should be centered in the viewport
|
|
131
|
+
* Example #4: Then the element "h1" should be centered in the viewport
|
|
132
|
+
* Example #5: Then the element ".loader" should be centered in the viewport
|
|
133
|
+
*
|
|
134
|
+
*/
|
|
135
|
+
Then('the element {string} should be centered in the viewport', async function (sel) {
|
|
136
|
+
const box = await this.page.locator(sel).first().boundingBox();
|
|
137
|
+
assert.ok(box, `Element "${sel}" not found.`);
|
|
138
|
+
const vw = this.page.viewportSize().width;
|
|
139
|
+
const center = box.x + box.width / 2;
|
|
140
|
+
const tolerance = vw * 0.1;
|
|
141
|
+
assert.ok(Math.abs(center - vw / 2) <= tolerance, `Element "${sel}" not centered (center=${center}, vw=${vw}).`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Native browser dialog handlers (alert / confirm / prompt) live in
|
|
145
|
+
// tests/step-definitions/dialog.steps.js.
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Click an element addressed by CSS selector.
|
|
149
|
+
*
|
|
150
|
+
* Example #1: When I click on the element "#sign-in"
|
|
151
|
+
* Example #2: When I click on the element ".btn-primary"
|
|
152
|
+
* Example #3: And we click on the element "[data-testid=cta]"
|
|
153
|
+
* Example #4: When I click on the element "li.nav-item:first-child a"
|
|
154
|
+
* Example #5: When I click on the element "button[aria-label='Close']"
|
|
155
|
+
*
|
|
156
|
+
*/
|
|
157
|
+
When(/^(I |we )*click on the element "([^"]*)"$/, async function (pronoun, sel) {
|
|
158
|
+
try {
|
|
159
|
+
await this.page.locator(sel).first().click();
|
|
160
|
+
} catch (e) {
|
|
161
|
+
throw friendly({
|
|
162
|
+
action: `click on the element "${sel}"`,
|
|
163
|
+
cause: e,
|
|
164
|
+
hint: `check that the element exists on the page, is visible, and is clickable.`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Dispatch a synthetic JavaScript event on a CSS-addressed element.
|
|
171
|
+
*
|
|
172
|
+
* Useful for components that listen for non-bubbling events or for events
|
|
173
|
+
* Playwright cannot synthesize through normal interactions.
|
|
174
|
+
*
|
|
175
|
+
* Example #1: When I trigger the JS event "click" on the element "#cta"
|
|
176
|
+
* Example #2: When I trigger the JS event "change" on the element "#country"
|
|
177
|
+
* Example #3: And I trigger the JS event "input" on the element "#search"
|
|
178
|
+
* Example #4: When I trigger the JS event "focus" on the element "#email"
|
|
179
|
+
* Example #5: When I trigger the JS event "blur" on the element "#name"
|
|
180
|
+
*
|
|
181
|
+
*/
|
|
182
|
+
When(/^(I |we )*trigger the JS event "([^"]*)" on the element "([^"]*)"$/, async function (pronoun, event, sel) {
|
|
183
|
+
try {
|
|
184
|
+
await this.page.locator(sel).first().dispatchEvent(event);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
throw friendly({
|
|
187
|
+
action: `trigger the "${event}" event on "${sel}"`,
|
|
188
|
+
cause: e,
|
|
189
|
+
hint: `use a standard event name like click, change, input, focus, or blur.`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Scroll a CSS-addressed element into view (uses Playwright's auto-scroll).
|
|
196
|
+
*
|
|
197
|
+
* Example #1: When I scroll to the element "#footer"
|
|
198
|
+
* Example #2: When I scroll to the element ".pricing-table"
|
|
199
|
+
* Example #3: And we scroll to the element "[data-testid=signup-cta]"
|
|
200
|
+
* Example #4: When I scroll to the element "tbody tr:nth-child(20)"
|
|
201
|
+
* Example #5: When I scroll to the element ".testimonial:last-child"
|
|
202
|
+
*
|
|
203
|
+
*/
|
|
204
|
+
When(/^(I |we )*scroll to the element "([^"]*)"$/, async function (pronoun, sel) {
|
|
205
|
+
try {
|
|
206
|
+
await this.page.locator(sel).first().scrollIntoViewIfNeeded();
|
|
207
|
+
} catch (e) {
|
|
208
|
+
throw friendly({
|
|
209
|
+
action: `scroll to "${sel}"`,
|
|
210
|
+
cause: e,
|
|
211
|
+
hint: `check the element exists on the page and was not removed before scrolling.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Hover the mouse pointer over a CSS-addressed element.
|
|
218
|
+
*
|
|
219
|
+
* Example #1: When I hover over the element ".tooltip-trigger"
|
|
220
|
+
* Example #2: When I hover over the element "#user-menu"
|
|
221
|
+
* Example #3: And we hover over the element "[data-testid=avatar]"
|
|
222
|
+
* Example #4: When I hover over the element "img.preview"
|
|
223
|
+
* Example #5: When I hover over the element "li.menu-item"
|
|
224
|
+
*
|
|
225
|
+
*/
|
|
226
|
+
When(/^(I |we )*hover over the element "([^"]*)"$/, async function (pronoun, sel) {
|
|
227
|
+
try {
|
|
228
|
+
await this.page.locator(sel).first().hover();
|
|
229
|
+
} catch (e) {
|
|
230
|
+
throw friendly({
|
|
231
|
+
action: `hover over "${sel}"`,
|
|
232
|
+
cause: e,
|
|
233
|
+
hint: `check the element exists, is visible, and is not covered by another element.`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Move keyboard focus to a CSS-addressed element.
|
|
240
|
+
*
|
|
241
|
+
* Example #1: When I focus on the element "#email"
|
|
242
|
+
* Example #2: When I focus on the element "input[type=search]"
|
|
243
|
+
* Example #3: And we focus on the element "[data-testid=combobox]"
|
|
244
|
+
* Example #4: When I focus on the element "textarea#message"
|
|
245
|
+
* Example #5: When I focus on the element "button.primary"
|
|
246
|
+
*
|
|
247
|
+
*/
|
|
248
|
+
When(/^(I |we )*focus on the element "([^"]*)"$/, async function (pronoun, sel) {
|
|
249
|
+
try {
|
|
250
|
+
await this.page.locator(sel).first().focus();
|
|
251
|
+
} catch (e) {
|
|
252
|
+
throw friendly({
|
|
253
|
+
action: `focus on "${sel}"`,
|
|
254
|
+
cause: e,
|
|
255
|
+
hint: `the element must be focusable — an input, button, link, or have a tabindex.`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Wait for a CSS-addressed element to become visible (display !== none, etc.).
|
|
262
|
+
*
|
|
263
|
+
* Example #1: Then the element "#dashboard" should be displayed
|
|
264
|
+
* Example #2: Then the element ".success-banner" should be displayed
|
|
265
|
+
* Example #3: And the element "[data-testid=user-list]" should be displayed
|
|
266
|
+
* Example #4: Then the element ".toast" should be displayed
|
|
267
|
+
* Example #5: When I press "Save"
|
|
268
|
+
* Then the element ".toast-success" should be displayed
|
|
269
|
+
*
|
|
270
|
+
*/
|
|
271
|
+
Then('the element {string} should be displayed', async function (sel) {
|
|
272
|
+
await this.page.locator(sel).first().waitFor({ state: 'visible' });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Assert a CSS-addressed element is hidden or absent.
|
|
277
|
+
*
|
|
278
|
+
* Example #1: Then the element "#loading-spinner" should not be displayed
|
|
279
|
+
* Example #2: Then the element ".error-banner" should not be displayed
|
|
280
|
+
* Example #3: And the element "[data-testid=skeleton]" should not be displayed
|
|
281
|
+
* Example #4: Then the element ".modal" should not be displayed
|
|
282
|
+
* Example #5: When I press "Submit"
|
|
283
|
+
* Then the element ".pending-badge" should not be displayed
|
|
284
|
+
*
|
|
285
|
+
*/
|
|
286
|
+
Then('the element {string} should not be displayed', async function (sel) {
|
|
287
|
+
const loc = this.page.locator(sel).first();
|
|
288
|
+
const count = await loc.count();
|
|
289
|
+
if (count === 0) return;
|
|
290
|
+
assert.ok(!(await loc.isVisible()), `Element "${sel}" should not be displayed.`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
async function inViewport(page, sel, topOffset = 0) {
|
|
294
|
+
return await page.locator(sel).first().evaluate((el, off) => {
|
|
295
|
+
const r = el.getBoundingClientRect();
|
|
296
|
+
return r.top >= off && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth;
|
|
297
|
+
}, topOffset);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Assert an element is fully inside the current viewport rectangle.
|
|
302
|
+
*
|
|
303
|
+
* Example #1: Then the element "#hero" should be displayed within a viewport
|
|
304
|
+
* Example #2: Then the element ".cta" should be displayed within a viewport
|
|
305
|
+
* Example #3: And the element "[data-testid=summary]" should be displayed within a viewport
|
|
306
|
+
* Example #4: Then the element "h1" should be displayed within a viewport
|
|
307
|
+
* Example #5: When I scroll to the element "#footer"
|
|
308
|
+
* Then the element "#footer" should be displayed within a viewport
|
|
309
|
+
*
|
|
310
|
+
*/
|
|
311
|
+
Then('the element {string} should be displayed within a viewport', async function (sel) {
|
|
312
|
+
assert.ok(await inViewport(this.page, sel, 0), `Element "${sel}" is not within viewport.`);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Assert an element is fully inside the viewport, allowing a top offset.
|
|
317
|
+
* Useful for sticky headers — pass the header height as the offset.
|
|
318
|
+
*
|
|
319
|
+
* Example #1: Then the element "#hero" should be displayed within a viewport with a top offset of 60 pixels
|
|
320
|
+
* Example #2: Then the element ".main" should be displayed within a viewport with a top offset of 80 pixels
|
|
321
|
+
* Example #3: And the element ".cta" should be displayed within a viewport with a top offset of 50 pixels
|
|
322
|
+
* Example #4: Then the element "h1" should be displayed within a viewport with a top offset of 100 pixels
|
|
323
|
+
* Example #5: Then the element "#summary" should be displayed within a viewport with a top offset of 64 pixels
|
|
324
|
+
*
|
|
325
|
+
*/
|
|
326
|
+
Then('the element {string} should be displayed within a viewport with a top offset of {int} pixels', async function (sel, off) {
|
|
327
|
+
assert.ok(await inViewport(this.page, sel, off), `Element "${sel}" is not within viewport (offset ${off}).`);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Assert an element is NOT inside the viewport (top-offset variant).
|
|
332
|
+
*
|
|
333
|
+
* Example #1: Then the element "#footer" should not be displayed within a viewport with a top offset of 60 pixels
|
|
334
|
+
* Example #2: Then the element ".off-screen" should not be displayed within a viewport with a top offset of 80 pixels
|
|
335
|
+
* Example #3: And the element ".pending" should not be displayed within a viewport with a top offset of 50 pixels
|
|
336
|
+
* Example #4: Then the element "#cookies-banner" should not be displayed within a viewport with a top offset of 100 pixels
|
|
337
|
+
* Example #5: Then the element ".low-priority" should not be displayed within a viewport with a top offset of 64 pixels
|
|
338
|
+
*
|
|
339
|
+
*/
|
|
340
|
+
Then('the element {string} should not be displayed within a viewport with a top offset of {int} pixels', async function (sel, off) {
|
|
341
|
+
assert.ok(!(await inViewport(this.page, sel, off)), `Element "${sel}" should not be in viewport (offset ${off}).`);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Assert an element is NOT inside the viewport rectangle.
|
|
346
|
+
*
|
|
347
|
+
* Example #1: Then the element "#footer" should not be displayed within a viewport
|
|
348
|
+
* Example #2: Then the element ".off-screen" should not be displayed within a viewport
|
|
349
|
+
* Example #3: And the element ".pending" should not be displayed within a viewport
|
|
350
|
+
* Example #4: Then the element "#cookies-banner" should not be displayed within a viewport
|
|
351
|
+
* Example #5: Then the element ".low-priority" should not be displayed within a viewport
|
|
352
|
+
*
|
|
353
|
+
*/
|
|
354
|
+
Then('the element {string} should not be displayed within a viewport', async function (sel) {
|
|
355
|
+
assert.ok(!(await inViewport(this.page, sel, 0)), `Element "${sel}" should not be in viewport.`);
|
|
356
|
+
});
|