@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,1003 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { friendly } = require('./varbase-e2e');
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Varbase E2E — Selectors step definitions
|
|
7
|
+
//
|
|
8
|
+
// Uses Playwright Locator API (locator.boundingBox, locator.isVisible,
|
|
9
|
+
// locator.isHidden, locator.click) to assert named page components are
|
|
10
|
+
// positioned relative to each other: above, below, left, right, inside,
|
|
11
|
+
// outside, over, not over. Also asserts visibility, focus, and click.
|
|
12
|
+
//
|
|
13
|
+
// Advanced selector system: named CSS and XPath selectors + layout components
|
|
14
|
+
// + viewport breakpoints — all configured under one `selectors` block, loaded
|
|
15
|
+
// from worldParameters, JSON files, or registered inline via steps.
|
|
16
|
+
//
|
|
17
|
+
// Configuration: resolved per-scenario with this priority:
|
|
18
|
+
// 1. process.env.VARBASE_E2E_SELECTORS_* (CI / shell — highest)
|
|
19
|
+
// 2. cucumber.js worldParameters.selectors.* (project-level defaults)
|
|
20
|
+
// 3. built-in defaults
|
|
21
|
+
//
|
|
22
|
+
// Supported env vars / worldParameters keys:
|
|
23
|
+
// VARBASE_E2E_SELECTORS_OFFSET offset scroll offset in px, default 60
|
|
24
|
+
// VARBASE_E2E_SELECTORS_BREAKPOINTS (JSON string) named breakpoints
|
|
25
|
+
//
|
|
26
|
+
// Unified config block (worldParameters.selectors):
|
|
27
|
+
// selectors: {
|
|
28
|
+
// filesPath: './tests/selectors/', // base path for selector files
|
|
29
|
+
// files: ['front-end-selectors.json', 'back-end-selectors.json'],
|
|
30
|
+
// css: {
|
|
31
|
+
// 'page header': 'header.page-header',
|
|
32
|
+
// 'main nav': 'nav[role="navigation"]',
|
|
33
|
+
// },
|
|
34
|
+
// xpath: {
|
|
35
|
+
// 'page title': '//h1[contains(@class,"page-title")]',
|
|
36
|
+
// },
|
|
37
|
+
// offset: 60, // scroll offset (px)
|
|
38
|
+
// breakpoints: {
|
|
39
|
+
// xs: { width: 375, height: 667 }, // phone portrait
|
|
40
|
+
// sm: { width: 576, height: 800 }, // large phone / phablet
|
|
41
|
+
// md: { width: 768, height: 1024 }, // tablet portrait
|
|
42
|
+
// lg: { width: 992, height: 768 }, // small laptop / tablet landscape
|
|
43
|
+
// xl: { width: 1200, height: 900, default: true }, // desktop
|
|
44
|
+
// xxl: { width: 1400, height: 900 }, // wide desktop / HD
|
|
45
|
+
// xxxl: { width: 1920, height: 1080 }, // Full HD / large monitor
|
|
46
|
+
// },
|
|
47
|
+
// }
|
|
48
|
+
//
|
|
49
|
+
// Selector files (JSON only) have the structure:
|
|
50
|
+
// {
|
|
51
|
+
// "css": { "breadcrumb": ".breadcrumb", "logo": "header .logo" },
|
|
52
|
+
// "xpath": { "page title": "//h1[contains(@class,'title')]" }
|
|
53
|
+
// }
|
|
54
|
+
//
|
|
55
|
+
// One unified CSS registry: layout components and element locators share
|
|
56
|
+
// worldParameters.selectors.css. Register in bulk via "Given I define css selectors:"
|
|
57
|
+
// or one-at-a-time via "When I add ... css selector". Same for XPath via `xpath`.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const { Given, When, Then, Before } = require('@cucumber/cucumber');
|
|
61
|
+
const fs = require('fs');
|
|
62
|
+
const path = require('path');
|
|
63
|
+
|
|
64
|
+
function parseFile(filePath) {
|
|
65
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
66
|
+
if (ext !== '.json') {
|
|
67
|
+
throw friendly(`Selector files must be JSON (got "${ext}" for "${filePath}").`);
|
|
68
|
+
}
|
|
69
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Config
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
function pick(envVal, paramVal, fallback) {
|
|
76
|
+
if (envVal !== undefined && envVal !== '') return envVal;
|
|
77
|
+
if (paramVal !== undefined && paramVal !== '' && paramVal !== null) return paramVal;
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveConfig(parameters) {
|
|
82
|
+
const s = (parameters && parameters.selectors) || {};
|
|
83
|
+
|
|
84
|
+
let breakpoints = s.breakpoints || {
|
|
85
|
+
xs: { width: 375, height: 667 },
|
|
86
|
+
sm: { width: 576, height: 800 },
|
|
87
|
+
md: { width: 768, height: 1024 },
|
|
88
|
+
lg: { width: 992, height: 768 },
|
|
89
|
+
xl: { width: 1200, height: 900, default: true },
|
|
90
|
+
xxl: { width: 1400, height: 900 },
|
|
91
|
+
xxxl: { width: 1920, height: 1080 },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (process.env.VARBASE_E2E_SELECTORS_BREAKPOINTS) {
|
|
95
|
+
try { breakpoints = JSON.parse(process.env.VARBASE_E2E_SELECTORS_BREAKPOINTS); } catch { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
offset: parseInt(pick(process.env.VARBASE_E2E_SELECTORS_OFFSET, s.offset, '60'), 10),
|
|
100
|
+
breakpoints,
|
|
101
|
+
filesPath: s.filesPath || '',
|
|
102
|
+
files: Array.isArray(s.files) ? s.files : [],
|
|
103
|
+
css: Object.assign({}, s.css || {}),
|
|
104
|
+
xpath: Object.assign({}, s.xpath || {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Per-scenario registries
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
Before(function () {
|
|
112
|
+
const cfg = resolveConfig(this.parameters);
|
|
113
|
+
this.__selectorsConfig = cfg;
|
|
114
|
+
this.__selectorsCss = Object.assign({}, cfg.css);
|
|
115
|
+
this.__selectorsXpath = Object.assign({}, cfg.xpath);
|
|
116
|
+
|
|
117
|
+
// Auto-load selector files specified in worldParameters.
|
|
118
|
+
for (const file of cfg.files) {
|
|
119
|
+
const filePath = cfg.filesPath ? path.join(cfg.filesPath, file) : file;
|
|
120
|
+
try {
|
|
121
|
+
const parsed = parseFile(filePath);
|
|
122
|
+
if (parsed && parsed.css) Object.assign(this.__selectorsCss, parsed.css);
|
|
123
|
+
if (parsed && parsed.xpath) Object.assign(this.__selectorsXpath, parsed.xpath);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
throw friendly(`Selectors: failed to load selector file "${filePath}": ${e.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Selector resolution helpers
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolve a named selector to a Playwright-compatible selector string.
|
|
136
|
+
* Priority: css registry → xpath registry.
|
|
137
|
+
*/
|
|
138
|
+
function resolveSelector(world, name) {
|
|
139
|
+
const trimmed = name.trim();
|
|
140
|
+
|
|
141
|
+
if (world.__selectorsCss && world.__selectorsCss[trimmed]) {
|
|
142
|
+
return world.__selectorsCss[trimmed];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (world.__selectorsXpath && world.__selectorsXpath[trimmed]) {
|
|
146
|
+
const xp = world.__selectorsXpath[trimmed];
|
|
147
|
+
return xp.startsWith('xpath=') ? xp : `xpath=${xp}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Suggest the closest registered name when one exists. Cheap Levenshtein
|
|
151
|
+
// by character — good enough for short selector names.
|
|
152
|
+
const known = [
|
|
153
|
+
...Object.keys(world.__selectorsCss || {}),
|
|
154
|
+
...Object.keys(world.__selectorsXpath || {}),
|
|
155
|
+
];
|
|
156
|
+
let best = null; let bestDist = Infinity;
|
|
157
|
+
for (const k of known) {
|
|
158
|
+
const dist = (function lev(a, b) {
|
|
159
|
+
const m = a.length, n = b.length;
|
|
160
|
+
if (!m) return n; if (!n) return m;
|
|
161
|
+
const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
162
|
+
for (let i = 0; i <= m; i++) d[i][0] = i;
|
|
163
|
+
for (let j = 0; j <= n; j++) d[0][j] = j;
|
|
164
|
+
for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) {
|
|
165
|
+
d[i][j] = a[i - 1] === b[j - 1] ? d[i - 1][j - 1] : 1 + Math.min(d[i - 1][j], d[i][j - 1], d[i - 1][j - 1]);
|
|
166
|
+
}
|
|
167
|
+
return d[m][n];
|
|
168
|
+
})(trimmed.toLowerCase(), k.toLowerCase());
|
|
169
|
+
if (dist < bestDist) { bestDist = dist; best = k; }
|
|
170
|
+
}
|
|
171
|
+
const suggestion = best && bestDist <= Math.max(2, Math.floor(trimmed.length / 3))
|
|
172
|
+
? `\n Did you mean "${best}"?`
|
|
173
|
+
: '';
|
|
174
|
+
const examples = known.length
|
|
175
|
+
? `\n Registered names: ${known.slice(0, 8).join(', ')}${known.length > 8 ? ', ...' : ''}`
|
|
176
|
+
: '\n No selectors registered yet.';
|
|
177
|
+
|
|
178
|
+
throw friendly(
|
|
179
|
+
`Unknown selector "${trimmed}".${suggestion}${examples}\n` +
|
|
180
|
+
` Register one with:\n` +
|
|
181
|
+
` Given I define css selectors:\n` +
|
|
182
|
+
` | name | css |\n` +
|
|
183
|
+
` When I add "${trimmed}" selector for "<css>" css selector\n` +
|
|
184
|
+
` or load a JSON preset via worldParameters.selectors.files.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseNames(text) {
|
|
189
|
+
return text.split(/\s*(?:,|and)\s*/).map(s => s.trim()).filter(Boolean);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Geometry helpers using Playwright locator.boundingBox()
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
async function getGeometry(page, selector) {
|
|
196
|
+
const loc = page.locator(selector).first();
|
|
197
|
+
try { await loc.scrollIntoViewIfNeeded({ timeout: 3000 }); } catch { /* may already be visible */ }
|
|
198
|
+
|
|
199
|
+
const box = await loc.boundingBox();
|
|
200
|
+
if (!box) return null;
|
|
201
|
+
|
|
202
|
+
const scroll = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
|
|
203
|
+
|
|
204
|
+
// z-index still requires evaluate — no native Playwright API for computed style.
|
|
205
|
+
const zIndex = await loc.evaluate(el => {
|
|
206
|
+
let z = 0;
|
|
207
|
+
let node = el;
|
|
208
|
+
while (node && node !== document.body) {
|
|
209
|
+
const cs = window.getComputedStyle(node);
|
|
210
|
+
if (cs.position !== 'static' && cs.zIndex !== 'auto') {
|
|
211
|
+
const n = parseInt(cs.zIndex, 10);
|
|
212
|
+
if (!isNaN(n)) z = n;
|
|
213
|
+
}
|
|
214
|
+
node = node.parentElement;
|
|
215
|
+
}
|
|
216
|
+
return z;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return { top: box.y + scroll.y, left: box.x + scroll.x, width: box.width, height: box.height, zIndex };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function assertPosition(page, position, c1Name, c1Sel, c2Name, c2Sel) {
|
|
223
|
+
const g1 = await getGeometry(page, c1Sel);
|
|
224
|
+
if (!g1) throw friendly(`Cannot get bounding box for "${c1Name}" (${c1Sel}) — element not visible`);
|
|
225
|
+
const g2 = await getGeometry(page, c2Sel);
|
|
226
|
+
if (!g2) throw friendly(`Cannot get bounding box for "${c2Name}" (${c2Sel}) — element not visible`);
|
|
227
|
+
|
|
228
|
+
let pass = false;
|
|
229
|
+
switch (position) {
|
|
230
|
+
case 'above': pass = g2.top >= g1.top + g1.height; break;
|
|
231
|
+
case 'below': pass = g1.top >= g2.top + g2.height; break;
|
|
232
|
+
case 'left': pass = g1.left + g1.width <= g2.left; break;
|
|
233
|
+
case 'right': pass = g1.left >= g2.left + g2.width; break;
|
|
234
|
+
case 'inside':
|
|
235
|
+
pass = g1.top >= g2.top && g1.top + g1.height <= g2.top + g2.height &&
|
|
236
|
+
g1.left >= g2.left && g1.left + g1.width <= g2.left + g2.width;
|
|
237
|
+
break;
|
|
238
|
+
case 'outside':
|
|
239
|
+
pass = g1.top <= g2.top && g1.top + g1.height >= g2.top + g2.height &&
|
|
240
|
+
g1.left <= g2.left && g1.left + g1.width >= g2.left + g2.width;
|
|
241
|
+
break;
|
|
242
|
+
case 'over': {
|
|
243
|
+
const intersects = !(g1.left >= g2.left + g2.width || g1.left + g1.width <= g2.left ||
|
|
244
|
+
g1.top >= g2.top + g2.height || g1.top + g1.height <= g2.top);
|
|
245
|
+
pass = intersects && g1.zIndex <= g2.zIndex;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return pass;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function dispatcher(world, position, subjectText, othersText, negate = false) {
|
|
253
|
+
const subjects = parseNames(subjectText);
|
|
254
|
+
const others = parseNames(othersText);
|
|
255
|
+
const errors = [];
|
|
256
|
+
for (const subjectName of subjects) {
|
|
257
|
+
const subjectSel = resolveSelector(world, subjectName);
|
|
258
|
+
for (const otherName of others) {
|
|
259
|
+
const otherSel = resolveSelector(world, otherName);
|
|
260
|
+
try {
|
|
261
|
+
const pass = await assertPosition(world.page, position, subjectName, subjectSel, otherName, otherSel);
|
|
262
|
+
if (!pass && !negate) errors.push(`"${subjectName}" is not ${position} "${otherName}"`);
|
|
263
|
+
else if (pass && negate) errors.push(`"${subjectName}" is ${position} "${otherName}" (expected not to be)`);
|
|
264
|
+
} catch (e) { errors.push(e.message); }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (errors.length > 0) throw friendly(errors.join('\n'));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ===========================================================================
|
|
271
|
+
// STEP DEFINITIONS — Advanced selector system
|
|
272
|
+
// ===========================================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Register a named CSS selector at runtime.
|
|
276
|
+
*
|
|
277
|
+
* Example #1: When I add "mobile logo" selector for "header img#logo" css selector
|
|
278
|
+
* Example #2: When I add "breadcrumb" selector for ".breadcrumb" css selector
|
|
279
|
+
* Example #3: When I add "breadcrumb first link" selector for ".breadcrumb li:nth-child(1) a" css selector
|
|
280
|
+
* Example #4: When I add "cta button" selector for ".cta .btn-primary" css selector
|
|
281
|
+
* Example #5: When I add "page header" selector for "header.page-header" css selector
|
|
282
|
+
* Example #6: When I add "main nav" selector for "nav[role='navigation']" css selector
|
|
283
|
+
* Example #7: When I add "hero image" selector for ".hero img" css selector
|
|
284
|
+
* Example #8: When I add "footer links" selector for "footer a" css selector
|
|
285
|
+
* Example #9: When I add "search field" selector for "input[type='search']" css selector
|
|
286
|
+
* Example #10: When I add "submit button" selector for "button[type='submit']" css selector
|
|
287
|
+
*
|
|
288
|
+
* Advanced:
|
|
289
|
+
* Example #11: Later override same name → latest wins:
|
|
290
|
+
* When I add "target" selector for "h1" css selector
|
|
291
|
+
* And I add "target" selector for "ul" css selector
|
|
292
|
+
* Example #12: Component (CSS) takes precedence over CSS registry with same name:
|
|
293
|
+
* When I add "shared" selector for "ul" css selector
|
|
294
|
+
* And I define css selectors:
|
|
295
|
+
* | shared | h1 |
|
|
296
|
+
* Example #13: Combine with XPath registration and use together:
|
|
297
|
+
* When I add "cta" selector for ".cta" css selector
|
|
298
|
+
* And I add "cta link" selector for "//a[contains(@class,'cta')]" xpath selector
|
|
299
|
+
*/
|
|
300
|
+
When(/^(I |we )*add "([^"]*)" selector for "([^"]*)" css selector$/, function (pronounCase, selectorName, cssSelector) {
|
|
301
|
+
if (!selectorName || !cssSelector) throw friendly('Selectors: selector name and CSS selector must not be empty.');
|
|
302
|
+
this.__selectorsCss[selectorName.trim()] = cssSelector.trim();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Register a named XPath selector at runtime.
|
|
307
|
+
*
|
|
308
|
+
* Example #1: When I add "page title" selector for "//h1[contains(@class,'page-header')]" xpath selector
|
|
309
|
+
* Example #2: When I add "Dashboard" selector for "//*[@id='navbar-link-admin-dashboard']" xpath selector
|
|
310
|
+
* Example #3: When I add "Vertical orientation" selector for "//*[@id='navbar-item--2-tray']/div/div[2]/div/button" xpath selector
|
|
311
|
+
* Example #4: When I add "admin menu" selector for "//nav[contains(@class,'admin-menu')]" xpath selector
|
|
312
|
+
* Example #5: When I add "first table row" selector for "//table/tbody/tr[1]" xpath selector
|
|
313
|
+
* Example #6: When I add "active tab" selector for "//li[contains(@class,'active')]/a" xpath selector
|
|
314
|
+
* Example #7: When I add "error message" selector for "//*[contains(@class,'error')]" xpath selector
|
|
315
|
+
* Example #8: When I add "site name" selector for "//a[contains(@class,'site-name')]" xpath selector
|
|
316
|
+
* Example #9: When I add "user menu" selector for "//*[@id='user-menu']" xpath selector
|
|
317
|
+
* Example #10: When I add "language switcher" selector for "//ul[contains(@class,'language-switcher')]" xpath selector
|
|
318
|
+
*
|
|
319
|
+
* Advanced:
|
|
320
|
+
* Example #11: Axis predicates — first match only (locator.first() is applied):
|
|
321
|
+
* When I add "first row" selector for "//table//tr[1]" xpath selector
|
|
322
|
+
* Example #12: Text contains — name by rendered label:
|
|
323
|
+
* When I add "login link" selector for "//a[contains(normalize-space(.),'Login')]" xpath selector
|
|
324
|
+
* Example #13: Attribute + position — last nav item:
|
|
325
|
+
* When I add "last nav item" selector for "//nav[@role='navigation']//a[last()]" xpath selector
|
|
326
|
+
*/
|
|
327
|
+
When(/^(I |we )*add "([^"]*)" selector for "([^"]*)" xpath selector$/, function (pronounCase, selectorName, xpathSelector) {
|
|
328
|
+
if (!selectorName || !xpathSelector) throw friendly('Selectors: selector name and XPath selector must not be empty.');
|
|
329
|
+
this.__selectorsXpath[selectorName.trim()] = xpathSelector.trim();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Load CSS and XPath selectors from a JSON file.
|
|
334
|
+
* File path is relative to worldParameters.selectors.filesPath.
|
|
335
|
+
*
|
|
336
|
+
* Example #1: When I add selectors from "selectors.json" file
|
|
337
|
+
* Example #2: When I add selectors from "admin-selectors.json" file
|
|
338
|
+
* Example #3: When I add selectors from "frontend.json" file
|
|
339
|
+
* Example #4: When I add selectors from "theme-selectors.json" file
|
|
340
|
+
* Example #5: When I add selectors from "components.json" file
|
|
341
|
+
* Example #6: When we add selectors from "shared.json" file
|
|
342
|
+
* Example #7: When add selectors from "backend.json" file
|
|
343
|
+
* Example #8: When I add selectors from "page-selectors.json" file
|
|
344
|
+
* Example #9: When I add selectors from "module-selectors.json" file
|
|
345
|
+
* Example #10: When we add selectors from "mobile.json" file
|
|
346
|
+
*
|
|
347
|
+
* Advanced:
|
|
348
|
+
* Example #11: Load a file then override a single entry inline:
|
|
349
|
+
* When I add selectors from "homepage-selectors.json" file
|
|
350
|
+
* And I add "homepage heading" selector for "header h1" css selector
|
|
351
|
+
* Example #12: Load multiple files — later files override earlier keys:
|
|
352
|
+
* When I add selectors from "front-end-selectors.json" file
|
|
353
|
+
* And I add selectors from "back-end-selectors.json" file
|
|
354
|
+
* Example #13: File + inline xpath + component — all three registries merged:
|
|
355
|
+
* When I add selectors from "homepage-selectors.json" file
|
|
356
|
+
* And I add "extra link" selector for "//a[@rel='canonical']" xpath selector
|
|
357
|
+
* And I define css selectors:
|
|
358
|
+
* | wrapper | #main |
|
|
359
|
+
*/
|
|
360
|
+
When(/^(I |we )*add selectors from "([^"]*)" file$/, function (pronounCase, fileName) {
|
|
361
|
+
const cfg = this.__selectorsConfig;
|
|
362
|
+
if (!fileName) throw friendly('Selectors: file name must not be empty.');
|
|
363
|
+
if (!cfg.filesPath) throw friendly('Selectors: worldParameters.selectors.filesPath must be set to load selector files.');
|
|
364
|
+
const filePath = path.join(cfg.filesPath, fileName);
|
|
365
|
+
let parsed;
|
|
366
|
+
try { parsed = parseFile(filePath); } catch (e) {
|
|
367
|
+
throw friendly(`Selectors: failed to load selector file "${filePath}": ${e.message}`);
|
|
368
|
+
}
|
|
369
|
+
if (parsed && parsed.css) Object.assign(this.__selectorsCss, parsed.css);
|
|
370
|
+
if (parsed && parsed.xpath) Object.assign(this.__selectorsXpath, parsed.xpath);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Print all registered CSS selectors to console (debug/inspection).
|
|
375
|
+
*
|
|
376
|
+
* Example #1: Then I print css selectors
|
|
377
|
+
* Example #2: When I print css selectors
|
|
378
|
+
* Example #3: Then we print css selectors
|
|
379
|
+
* Example #4: When we print css selectors
|
|
380
|
+
* Example #5: Then print css selectors
|
|
381
|
+
* Example #6: When print css selectors
|
|
382
|
+
* Example #7: And I print css selectors
|
|
383
|
+
* Example #8: And we print css selectors
|
|
384
|
+
* Example #9: Given I print css selectors
|
|
385
|
+
* Example #10: But I print css selectors
|
|
386
|
+
*/
|
|
387
|
+
Then(/^(I |we )*print css selectors$/, function (_pronounCase) {
|
|
388
|
+
console.log('\n--- CSS Selectors ---');
|
|
389
|
+
for (const [name, sel] of Object.entries(this.__selectorsCss || {})) {
|
|
390
|
+
console.log(` "${name}": ${sel}`);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Print all registered XPath selectors to console (debug/inspection).
|
|
396
|
+
*
|
|
397
|
+
* Example #1: Then I print xpath selectors
|
|
398
|
+
* Example #2: When I print xpath selectors
|
|
399
|
+
* Example #3: Then we print xpath selectors
|
|
400
|
+
* Example #4: When we print xpath selectors
|
|
401
|
+
* Example #5: Then print xpath selectors
|
|
402
|
+
* Example #6: When print xpath selectors
|
|
403
|
+
* Example #7: And I print xpath selectors
|
|
404
|
+
* Example #8: And we print xpath selectors
|
|
405
|
+
* Example #9: Given I print xpath selectors
|
|
406
|
+
* Example #10: But I print xpath selectors
|
|
407
|
+
*/
|
|
408
|
+
Then(/^(I |we )*print xpath selectors$/, function (_pronounCase) {
|
|
409
|
+
console.log('\n--- XPath Selectors ---');
|
|
410
|
+
for (const [name, sel] of Object.entries(this.__selectorsXpath || {})) {
|
|
411
|
+
console.log(` "${name}": ${sel}`);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// ===========================================================================
|
|
416
|
+
// STEP DEFINITIONS — Layout components
|
|
417
|
+
// ===========================================================================
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Define named CSS selectors in bulk for the current scenario.
|
|
421
|
+
*
|
|
422
|
+
* Example #1:
|
|
423
|
+
* Given I define css selectors:
|
|
424
|
+
* | header | #header |
|
|
425
|
+
* | nav | nav.primary |
|
|
426
|
+
* | footer | footer |
|
|
427
|
+
*
|
|
428
|
+
* Example #2:
|
|
429
|
+
* Given I define css selectors:
|
|
430
|
+
* | logo | .site-logo |
|
|
431
|
+
* | search | #search-input |
|
|
432
|
+
*
|
|
433
|
+
* Example #3:
|
|
434
|
+
* Given I define css selectors:
|
|
435
|
+
* | hero | .hero-banner |
|
|
436
|
+
* | content | .main-content |
|
|
437
|
+
* | sidebar | .sidebar |
|
|
438
|
+
*
|
|
439
|
+
* Example #4:
|
|
440
|
+
* Given I define css selectors:
|
|
441
|
+
* | menu | ul.menu |
|
|
442
|
+
* | button | .cta-button |
|
|
443
|
+
*
|
|
444
|
+
* Example #5:
|
|
445
|
+
* Given I define css selectors:
|
|
446
|
+
* | top bar | .top-bar |
|
|
447
|
+
* | page | #page-wrapper |
|
|
448
|
+
* | footer | .site-footer |
|
|
449
|
+
*
|
|
450
|
+
* Example #6:
|
|
451
|
+
* Given we define css selectors:
|
|
452
|
+
* | modal | .modal-dialog |
|
|
453
|
+
* | overlay | .modal-backdrop |
|
|
454
|
+
*
|
|
455
|
+
* Example #7:
|
|
456
|
+
* When I define css selectors:
|
|
457
|
+
* | input | input[name=q] |
|
|
458
|
+
* | button | button[type=submit] |
|
|
459
|
+
*
|
|
460
|
+
* Example #8:
|
|
461
|
+
* And I define css selectors:
|
|
462
|
+
* | card | .card |
|
|
463
|
+
* | title | .card-title |
|
|
464
|
+
*
|
|
465
|
+
* Example #9:
|
|
466
|
+
* Given define css selectors:
|
|
467
|
+
* | heading | h1 |
|
|
468
|
+
* | list | ul |
|
|
469
|
+
*
|
|
470
|
+
* Example #10:
|
|
471
|
+
* Given I define css selectors:
|
|
472
|
+
* | left | .col-left |
|
|
473
|
+
* | center | .col-center |
|
|
474
|
+
* | right | .col-right |
|
|
475
|
+
*
|
|
476
|
+
* Advanced:
|
|
477
|
+
* Example #11: Later entry with same name overrides earlier:
|
|
478
|
+
* When I add "shared" selector for "ul" css selector
|
|
479
|
+
* Given I define css selectors:
|
|
480
|
+
* | shared | h1 |
|
|
481
|
+
* Then I see visible shared # resolves to h1 (latest wins).
|
|
482
|
+
*
|
|
483
|
+
* Example #12: Define + use in position + visibility asserts:
|
|
484
|
+
* Given I define css selectors:
|
|
485
|
+
* | heading | h1 |
|
|
486
|
+
* | list | ul |
|
|
487
|
+
* Then I see visible heading, list
|
|
488
|
+
* And I see heading above list
|
|
489
|
+
*
|
|
490
|
+
* Example #13: Layered page-shell selectors:
|
|
491
|
+
* Given I define css selectors:
|
|
492
|
+
* | topbar | .topbar |
|
|
493
|
+
* | header | header.site |
|
|
494
|
+
* | nav | nav[role=navigation] |
|
|
495
|
+
* | main | main |
|
|
496
|
+
* | footer | footer |
|
|
497
|
+
*/
|
|
498
|
+
Given(/^(I |we )*define css selectors:$/, function (pronounCase, dataTable) {
|
|
499
|
+
for (const [name, selector] of dataTable.raw()) {
|
|
500
|
+
if (name && selector) this.__selectorsCss[name.trim()] = selector.trim();
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Define named XPath selectors in bulk for the current scenario.
|
|
506
|
+
*
|
|
507
|
+
* Example #1:
|
|
508
|
+
* Given I define xpath selectors:
|
|
509
|
+
* | first heading | //h1[1] |
|
|
510
|
+
* | first subline | //h3[1] |
|
|
511
|
+
* Example #2:
|
|
512
|
+
* Given we define xpath selectors:
|
|
513
|
+
* | active tab | //li[contains(@class,'active')]/a |
|
|
514
|
+
* Example #3:
|
|
515
|
+
* Given define xpath selectors:
|
|
516
|
+
* | last nav item | //nav//a[last()] |
|
|
517
|
+
* Example #4:
|
|
518
|
+
* And I define xpath selectors:
|
|
519
|
+
* | login link | //a[contains(normalize-space(.),'Login')] |
|
|
520
|
+
* Example #5:
|
|
521
|
+
* When I define xpath selectors:
|
|
522
|
+
* | error message | //*[contains(@class,'error')] |
|
|
523
|
+
* Example #6:
|
|
524
|
+
* Given I define xpath selectors:
|
|
525
|
+
* | first table row | //table/tbody/tr[1] |
|
|
526
|
+
* | first table cell| //table/tbody/tr[1]/td[1] |
|
|
527
|
+
* Example #7:
|
|
528
|
+
* Given I define xpath selectors:
|
|
529
|
+
* | page title | //h1[contains(@class,'page-header')] |
|
|
530
|
+
* Example #8:
|
|
531
|
+
* Given I define xpath selectors:
|
|
532
|
+
* | admin menu | //nav[contains(@class,'admin-menu')] |
|
|
533
|
+
* Example #9:
|
|
534
|
+
* Given I define xpath selectors:
|
|
535
|
+
* | language switcher | //ul[contains(@class,'language-switcher')] |
|
|
536
|
+
* Example #10:
|
|
537
|
+
* Given I define xpath selectors:
|
|
538
|
+
* | user menu | //*[@id='user-menu'] |
|
|
539
|
+
*/
|
|
540
|
+
Given(/^(I |we )*define xpath selectors:$/, function (pronounCase, dataTable) {
|
|
541
|
+
for (const [name, selector] of dataTable.raw()) {
|
|
542
|
+
if (name && selector) this.__selectorsXpath[name.trim()] = selector.trim();
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Resize viewport to a named breakpoint.
|
|
548
|
+
* Built-in names: xs, sm, md, lg, xl, xxl, xxxl.
|
|
549
|
+
*
|
|
550
|
+
* xs 375 x 667 phone portrait
|
|
551
|
+
* sm 576 x 800 large phone / phablet
|
|
552
|
+
* md 768 x 1024 tablet portrait
|
|
553
|
+
* lg 992 x 768 small laptop / tablet landscape
|
|
554
|
+
* xl 1200 x 900 desktop (default)
|
|
555
|
+
* xxl 1400 x 900 wide desktop / HD
|
|
556
|
+
* xxxl 1920 x 1080 Full HD / large monitor
|
|
557
|
+
*
|
|
558
|
+
* Example #1: Given I am viewing the site on a xl screen
|
|
559
|
+
* Example #2: Given I am viewing the site on a xs screen
|
|
560
|
+
* Example #3: Given I am viewing the site on a md screen
|
|
561
|
+
* Example #4: Given I am viewing the site on a "xl" screen
|
|
562
|
+
* Example #5: Given I am viewing the site on a lg device
|
|
563
|
+
* Example #6: Given I am viewing the site on a "sm" device
|
|
564
|
+
* Example #7: Given I am viewing the site on a xxl screen
|
|
565
|
+
* Example #8: Given I am viewing the site on a "xxxl" screen
|
|
566
|
+
* Example #9: Given I am viewing the site on a xxxl device
|
|
567
|
+
* Example #10: Given I am viewing the site on a "xxl" device
|
|
568
|
+
* Example #11: Given we are viewing the site on a xs screen
|
|
569
|
+
* Example #12: Given viewing the site on a lg screen
|
|
570
|
+
*/
|
|
571
|
+
Given(/^(I am |we are )?viewing the site on a (?:"([^"]+)"|([a-zA-Z0-9 _,]+)) (?:screen|device)$/, async function (pronounCase, quoted, bare) {
|
|
572
|
+
const name = (quoted || bare || '').trim();
|
|
573
|
+
const cfg = this.__selectorsConfig;
|
|
574
|
+
if (!cfg.breakpoints[name]) throw friendly(`Selectors: breakpoint "${name}" not defined in worldParameters.selectors.breakpoints.`);
|
|
575
|
+
const { width, height } = cfg.breakpoints[name];
|
|
576
|
+
await this.page.setViewportSize({ width, height });
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// ===========================================================================
|
|
580
|
+
// STEP DEFINITIONS — Relative position assertions
|
|
581
|
+
// ===========================================================================
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Assert a subject locator is above one or more others (absolute page coordinates).
|
|
585
|
+
*
|
|
586
|
+
* Example #1: Then I see header above footer
|
|
587
|
+
* Example #2: Then I see logo above search
|
|
588
|
+
* Example #3: Then I see nav above content, footer
|
|
589
|
+
* Example #4: Then I see hero above sidebar and footer
|
|
590
|
+
* Example #5: Then I see top bar above nav, content and footer
|
|
591
|
+
* Example #6: Then I see heading above subline
|
|
592
|
+
* Example #7: Then I see nav above content
|
|
593
|
+
* Example #8: Then I see search above results
|
|
594
|
+
* Example #9: Then I see banner above body
|
|
595
|
+
* Example #10: Then I see menu above page
|
|
596
|
+
*
|
|
597
|
+
* Advanced — multi-subject / multi-target dispatch:
|
|
598
|
+
* Example #11: Single subject, many targets (comma + "and"):
|
|
599
|
+
* Then I see heading above subline, list and footer
|
|
600
|
+
* Example #12: Many subjects, single target:
|
|
601
|
+
* Then I see heading, subline above list
|
|
602
|
+
* Example #13: Cartesian — many subjects × many targets:
|
|
603
|
+
* Then I see heading, subline above list, footer
|
|
604
|
+
* Example #14: Combined with inline xpath registration:
|
|
605
|
+
* When I add "first heading xp" selector for "//h1[1]" xpath selector
|
|
606
|
+
* Then I see first heading xp above list
|
|
607
|
+
*/
|
|
608
|
+
Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) above ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
|
|
609
|
+
await dispatcher(this, 'above', subject, others);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Assert a subject locator is below one or more others.
|
|
614
|
+
*
|
|
615
|
+
* Example #1: Then I see footer below header
|
|
616
|
+
* Example #2: Then I see content below nav
|
|
617
|
+
* Example #3: Then I see footer below header, nav and content
|
|
618
|
+
* Example #4: Then I see results below search
|
|
619
|
+
* Example #5: Then I see sidebar below hero
|
|
620
|
+
* Example #6: Then I see list below heading
|
|
621
|
+
* Example #7: Then I see nav below top bar
|
|
622
|
+
* Example #8: Then I see content below logo
|
|
623
|
+
* Example #9: Then I see page below menu
|
|
624
|
+
* Example #10: Then I see body below banner
|
|
625
|
+
*/
|
|
626
|
+
Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) below ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
|
|
627
|
+
await dispatcher(this, 'below', subject, others);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Assert a subject locator is to the left of one or more others.
|
|
632
|
+
*
|
|
633
|
+
* Example #1: Then I see logo to the left of nav
|
|
634
|
+
* Example #2: Then I see sidebar to left of content
|
|
635
|
+
* Example #3: Then I see search to the left of search button
|
|
636
|
+
* Example #4: Then I see menu to the left of cta
|
|
637
|
+
* Example #5: Then I see label to left of input
|
|
638
|
+
* Example #6: Then I see icon to the left of title
|
|
639
|
+
* Example #7: Then I see thumbnail to the left of description
|
|
640
|
+
* Example #8: Then I see avatar to left of username
|
|
641
|
+
* Example #9: Then I see back button to the left of forward button
|
|
642
|
+
* Example #10: Then I see flag to left of country name
|
|
643
|
+
*/
|
|
644
|
+
Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) to (?:the )?left of ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
|
|
645
|
+
await dispatcher(this, 'left', subject, others);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Assert a subject locator is to the right of one or more others.
|
|
650
|
+
*
|
|
651
|
+
* Example #1: Then I see nav to the right of logo
|
|
652
|
+
* Example #2: Then I see content to right of sidebar
|
|
653
|
+
* Example #3: Then I see search button to the right of search
|
|
654
|
+
* Example #4: Then I see cta to the right of menu
|
|
655
|
+
* Example #5: Then I see input to right of label
|
|
656
|
+
* Example #6: Then I see title to the right of icon
|
|
657
|
+
* Example #7: Then I see description to the right of thumbnail
|
|
658
|
+
* Example #8: Then I see username to right of avatar
|
|
659
|
+
* Example #9: Then I see forward button to the right of back button
|
|
660
|
+
* Example #10: Then I see country name to right of flag
|
|
661
|
+
*/
|
|
662
|
+
Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) to (?:the )?right of ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
|
|
663
|
+
await dispatcher(this, 'right', subject, others);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Assert a subject locator's bounding box is fully inside another's.
|
|
668
|
+
*
|
|
669
|
+
* Example #1: Then I see logo inside of header
|
|
670
|
+
* Example #2: Then I see nav inside of header
|
|
671
|
+
* Example #3: Then I see search inside of header
|
|
672
|
+
* Example #4: Then I see cta inside of hero
|
|
673
|
+
* Example #5: Then I see label inside of form
|
|
674
|
+
* Example #6: Then I see icon inside of button
|
|
675
|
+
* Example #7: Then I see title inside of banner
|
|
676
|
+
* Example #8: Then I see content inside of page
|
|
677
|
+
* Example #9: Then I see sidebar inside of page
|
|
678
|
+
* Example #10: Then I see footer inside of page
|
|
679
|
+
*/
|
|
680
|
+
Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) inside of ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
|
|
681
|
+
await dispatcher(this, 'inside', subject, others);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Assert a subject locator's bounding box fully contains another's.
|
|
686
|
+
*
|
|
687
|
+
* Example #1: Then I see header outside of logo
|
|
688
|
+
* Example #2: Then I see header outside of nav
|
|
689
|
+
* Example #3: Then I see hero outside of cta
|
|
690
|
+
* Example #4: Then I see form outside of label
|
|
691
|
+
* Example #5: Then I see button outside of icon
|
|
692
|
+
* Example #6: Then I see banner outside of title
|
|
693
|
+
* Example #7: Then I see page outside of content
|
|
694
|
+
* Example #8: Then I see page outside of sidebar
|
|
695
|
+
* Example #9: Then I see page outside of footer
|
|
696
|
+
* Example #10: Then I see wrapper outside of inner
|
|
697
|
+
*/
|
|
698
|
+
Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) outside of ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
|
|
699
|
+
await dispatcher(this, 'outside', subject, others);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Assert a subject locator overlaps another via intersection and z-index.
|
|
704
|
+
*
|
|
705
|
+
* Example #1: Then I see modal over content
|
|
706
|
+
* Example #2: Then I see dropdown over nav
|
|
707
|
+
* Example #3: Then I see tooltip over label
|
|
708
|
+
* Example #4: Then I see overlay over page
|
|
709
|
+
* Example #5: Then I see popup over hero
|
|
710
|
+
* Example #6: Then I see sticky header over content
|
|
711
|
+
* Example #7: Then I see cookie banner over footer
|
|
712
|
+
* Example #8: Then I see dialog over sidebar
|
|
713
|
+
* Example #9: Then I see notification over header
|
|
714
|
+
* Example #10: Then I see lightbox over page
|
|
715
|
+
*/
|
|
716
|
+
Then(/^(I |we )*see ((?:[a-zA-Z0-9 ,\-](?!not))+) over ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
|
|
717
|
+
await dispatcher(this, 'over', subject, others);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Assert a subject locator does NOT overlap another element.
|
|
722
|
+
*
|
|
723
|
+
* Example #1: Then I see header not over content
|
|
724
|
+
* Example #2: Then I see nav not over sidebar
|
|
725
|
+
* Example #3: Then I see footer not over content
|
|
726
|
+
* Example #4: Then I see logo not over search
|
|
727
|
+
* Example #5: Then I see sidebar not over footer
|
|
728
|
+
* Example #6: Then I see menu not over hero
|
|
729
|
+
* Example #7: Then I see label not over input
|
|
730
|
+
* Example #8: Then I see icon not over title
|
|
731
|
+
* Example #9: Then I see button not over form
|
|
732
|
+
* Example #10: Then I see nav not over page
|
|
733
|
+
*/
|
|
734
|
+
Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) not over ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subject, others) {
|
|
735
|
+
await dispatcher(this, 'over', subject, others, true);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// ===========================================================================
|
|
739
|
+
// STEP DEFINITIONS — Visibility and focus (uses locator.isVisible / isHidden)
|
|
740
|
+
// ===========================================================================
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Assert one or more named locators are visible.
|
|
744
|
+
*
|
|
745
|
+
* Example #1: Then I see visible header
|
|
746
|
+
* Example #2: Then I see visible nav, footer
|
|
747
|
+
* Example #3: Then I see visible logo and search
|
|
748
|
+
* Example #4: Then I see visible cta
|
|
749
|
+
* Example #5: Then I see visible hero, content and sidebar
|
|
750
|
+
* Example #6: Then I see visible menu
|
|
751
|
+
* Example #7: Then I see visible search button
|
|
752
|
+
* Example #8: Then I see visible banner
|
|
753
|
+
* Example #9: Then I see visible footer
|
|
754
|
+
* Example #10: Then I see visible top bar, nav and content
|
|
755
|
+
*
|
|
756
|
+
* Advanced:
|
|
757
|
+
* Example #11: Mix of components, inline CSS, inline XPath — all resolve:
|
|
758
|
+
* When I add "page heading" selector for "h1" css selector
|
|
759
|
+
* And I add "first para" selector for "//p[1]" xpath selector
|
|
760
|
+
* Then I see visible page heading, first para
|
|
761
|
+
* Example #12: After breakpoint resize — assert still visible:
|
|
762
|
+
* Given I am viewing the site on a xs screen
|
|
763
|
+
* Then I see visible heading, list
|
|
764
|
+
* Example #13: After loading a selector file — names from file are visible:
|
|
765
|
+
* When I add selectors from "homepage-selectors.json" file
|
|
766
|
+
* Then I see visible homepage heading, homepage first heading
|
|
767
|
+
*/
|
|
768
|
+
Then(/^(I |we )*see visible ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subjectsText) {
|
|
769
|
+
const names = parseNames(subjectsText);
|
|
770
|
+
const errors = [];
|
|
771
|
+
for (const name of names) {
|
|
772
|
+
const sel = resolveSelector(this, name);
|
|
773
|
+
// Playwright native auto-retrying assertion — waits until the locator becomes visible.
|
|
774
|
+
try { await this.page.locator(sel).first().waitFor({ state: 'visible', timeout: 5000 }); }
|
|
775
|
+
catch { errors.push(`"${name}" (${sel}) is not visible`); }
|
|
776
|
+
}
|
|
777
|
+
if (errors.length > 0) throw friendly(errors.join('\n'));
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Assert one or more named locators are hidden.
|
|
782
|
+
*
|
|
783
|
+
* Example #1: Then I don't see modal
|
|
784
|
+
* Example #2: Then I don't see dropdown, tooltip
|
|
785
|
+
* Example #3: Then I don't see overlay
|
|
786
|
+
* Example #4: Then I don't see cookie banner
|
|
787
|
+
* Example #5: Then I don't see popup
|
|
788
|
+
* Example #6: Then I don't see notification
|
|
789
|
+
* Example #7: Then I don't see lightbox
|
|
790
|
+
* Example #8: Then I don't see sticky header
|
|
791
|
+
* Example #9: Then I don't see dialog
|
|
792
|
+
* Example #10: Then I don't see sidebar, overlay and popup
|
|
793
|
+
*/
|
|
794
|
+
Then(/^(I |we )*(don't|do not) see ([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, negation, subjectsText) {
|
|
795
|
+
const names = parseNames(subjectsText);
|
|
796
|
+
const errors = [];
|
|
797
|
+
for (const name of names) {
|
|
798
|
+
const sel = resolveSelector(this, name);
|
|
799
|
+
// Playwright native auto-retrying assertion — waits until the locator is detached or hidden.
|
|
800
|
+
try { await this.page.locator(sel).first().waitFor({ state: 'hidden', timeout: 5000 }); }
|
|
801
|
+
catch { errors.push(`"${name}" (${sel}) is visible (expected hidden)`); }
|
|
802
|
+
}
|
|
803
|
+
if (errors.length > 0) throw friendly(errors.join('\n'));
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Assert a named locator has keyboard focus.
|
|
808
|
+
*
|
|
809
|
+
* Example #1: Then I see search has focus
|
|
810
|
+
* Example #2: Then I see email input has focus
|
|
811
|
+
* Example #3: Then we see submit button has focus
|
|
812
|
+
* Example #4: Then I see username has focus
|
|
813
|
+
* Example #5: Then I see password has focus
|
|
814
|
+
* Example #6: Then we see search button has focus
|
|
815
|
+
* Example #7: Then I see first link has focus
|
|
816
|
+
* Example #8: Then I see skip link has focus
|
|
817
|
+
* Example #9: Then I see close button has focus
|
|
818
|
+
* Example #10: Then I see modal heading has focus
|
|
819
|
+
*/
|
|
820
|
+
Then(/^(I |we )*see ([a-zA-Z0-9 ,\-]+) has focus$/, async function (pronounCase, subjectText) {
|
|
821
|
+
const name = subjectText.trim();
|
|
822
|
+
const sel = resolveSelector(this, name);
|
|
823
|
+
const loc = this.page.locator(sel).first();
|
|
824
|
+
// Playwright native auto-retrying focus check via waitForFunction.
|
|
825
|
+
try {
|
|
826
|
+
await loc.waitFor({ state: 'attached', timeout: 5000 });
|
|
827
|
+
const handle = await loc.elementHandle();
|
|
828
|
+
await this.page.waitForFunction((el) => el === document.activeElement, handle, { timeout: 5000 });
|
|
829
|
+
} catch {
|
|
830
|
+
throw friendly(`"${name}" (${sel}) does not have focus`);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// ===========================================================================
|
|
835
|
+
// STEP DEFINITIONS — Focus and text selection
|
|
836
|
+
// ===========================================================================
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Resolve a form field by label → placeholder → role=textbox → [name]/#id.
|
|
840
|
+
* Uses Playwright's ready-made semantic locators (getByLabel, getByPlaceholder,
|
|
841
|
+
* getByRole) with .or() fallbacks — matches the accessibility-first lookup
|
|
842
|
+
* chain recommended by https://playwright.dev/docs/locators.
|
|
843
|
+
*/
|
|
844
|
+
function resolveField(page, field) {
|
|
845
|
+
return page.getByLabel(field).or(
|
|
846
|
+
page.getByPlaceholder(field)
|
|
847
|
+
).or(
|
|
848
|
+
page.getByRole('textbox', { name: field })
|
|
849
|
+
).or(
|
|
850
|
+
page.locator(`[name="${field}"], #${field}`)
|
|
851
|
+
).first();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Move keyboard focus to a field by label, name, or id.
|
|
856
|
+
*
|
|
857
|
+
* Example #1: When I move focus to "Title" field
|
|
858
|
+
* Example #2: When I move focus to "Body" field
|
|
859
|
+
* Example #3: When I move focus to "Email" field
|
|
860
|
+
* Example #4: When I move focus to "Search" field
|
|
861
|
+
* Example #5: When I move focus to "Username" field
|
|
862
|
+
* Example #6: When I move focus to "Password" field
|
|
863
|
+
* Example #7: When I move focus to "First name" field
|
|
864
|
+
* Example #8: When I move focus to "Last name" field
|
|
865
|
+
* Example #9: When I move focus to "Description" field
|
|
866
|
+
* Example #10: When I move focus to "Phone" field
|
|
867
|
+
*/
|
|
868
|
+
When(/^(I |we )*move focus to "([^"]*)" field$/, async function (pronounCase, fieldLabel) {
|
|
869
|
+
const loc = resolveField(this.page, fieldLabel);
|
|
870
|
+
await loc.focus();
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Select all text inside a field by label, name, or id.
|
|
875
|
+
*
|
|
876
|
+
* Example #1: When I select all text in "Title" field
|
|
877
|
+
* Example #2: When I select all text in "Description" field
|
|
878
|
+
* Example #3: When I select all text in "Body" field
|
|
879
|
+
* Example #4: When I select all text in "Email" field
|
|
880
|
+
* Example #5: When I select all text in "Username" field
|
|
881
|
+
* Example #6: When I select all text in "Search" field
|
|
882
|
+
* Example #7: When I select all text in "Name" field
|
|
883
|
+
* Example #8: When I select all text in "Notes" field
|
|
884
|
+
* Example #9: When I select all text in "Address" field
|
|
885
|
+
* Example #10: When I select all text in "Message" field
|
|
886
|
+
*/
|
|
887
|
+
When(/^(I |we )*select all text in "([^"]*)" field$/, async function (pronounCase, fieldLabel) {
|
|
888
|
+
const loc = resolveField(this.page, fieldLabel);
|
|
889
|
+
await loc.focus();
|
|
890
|
+
await loc.selectText();
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Select a character range (from..to) inside a field.
|
|
895
|
+
*
|
|
896
|
+
* Example #1: When I select from 0 to 5 text in "Title" field
|
|
897
|
+
* Example #2: When I select from 0 to 10 text in "Body" field
|
|
898
|
+
* Example #3: When I select from 3 to 8 text in "Description" field
|
|
899
|
+
* Example #4: When I select from 0 to 5 text in "Email" field
|
|
900
|
+
* Example #5: When I select from 2 to 6 text in "Username" field
|
|
901
|
+
* Example #6: When we select from 0 to 4 text in "Name" field
|
|
902
|
+
* Example #7: When select from 1 to 7 text in "Search" field
|
|
903
|
+
* Example #8: When I select from 0 to 20 text in "Notes" field
|
|
904
|
+
* Example #9: When I select from 5 to 15 text in "Address" field
|
|
905
|
+
* Example #10: When we select from 0 to 3 text in "Phone" field
|
|
906
|
+
*
|
|
907
|
+
* Advanced:
|
|
908
|
+
* Example #11: Select first word after fill:
|
|
909
|
+
* And I fill in "Title" with "Release 2.0 notes"
|
|
910
|
+
* When I select from 0 to 7 text in "Title" field
|
|
911
|
+
* Example #12: Select middle substring by index range:
|
|
912
|
+
* And I fill in "Body" with "abcdefghij"
|
|
913
|
+
* When I select from 3 to 6 text in "Body" field
|
|
914
|
+
* Example #13: Chain — fill, focus, range-select, assert focus:
|
|
915
|
+
* And I fill in "Notes" with "hello world"
|
|
916
|
+
* When I move focus to "Notes" field
|
|
917
|
+
* And I select from 0 to 5 text in "Notes" field
|
|
918
|
+
* Then I see Notes has focus
|
|
919
|
+
*/
|
|
920
|
+
When(/^(I |we )*select from (\d+) to (\d+) text in "([^"]*)" field$/, async function (pronounCase, from, to, fieldLabel) {
|
|
921
|
+
const start = parseInt(from, 10);
|
|
922
|
+
const end = parseInt(to, 10);
|
|
923
|
+
const loc = resolveField(this.page, fieldLabel);
|
|
924
|
+
await loc.focus();
|
|
925
|
+
await loc.evaluate((el, [s, e]) => {
|
|
926
|
+
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(s, e);
|
|
927
|
+
}, [start, end]);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Select a specific substring of text inside a field.
|
|
932
|
+
*
|
|
933
|
+
* Example #1: When I select "title name" text in "Title" field
|
|
934
|
+
* Example #2: When I select "some content" text in "Description" field
|
|
935
|
+
* Example #3: When I select "hello" text in "Body" field
|
|
936
|
+
* Example #4: When I select "admin" text in "Username" field
|
|
937
|
+
* Example #5: When I select "example" text in "Email" field
|
|
938
|
+
* Example #6: When we select "varbase-e2e" text in "Search" field
|
|
939
|
+
* Example #7: When select "lorem" text in "Notes" field
|
|
940
|
+
* Example #8: When I select "street" text in "Address" field
|
|
941
|
+
* Example #9: When we select "555" text in "Phone" field
|
|
942
|
+
* Example #10: When I select "summary" text in "Message" field
|
|
943
|
+
*/
|
|
944
|
+
When(/^(I |we )*select "([^"]*)" text in "([^"]*)" field$/, async function (pronounCase, selectedText, fieldLabel) {
|
|
945
|
+
const loc = resolveField(this.page, fieldLabel);
|
|
946
|
+
|
|
947
|
+
const value = await loc.inputValue();
|
|
948
|
+
const start = value.indexOf(selectedText);
|
|
949
|
+
if (start === -1) throw friendly(`Text "${selectedText}" not found in field "${fieldLabel}"`);
|
|
950
|
+
const end = start + selectedText.length;
|
|
951
|
+
|
|
952
|
+
await loc.focus();
|
|
953
|
+
await loc.evaluate((el, [s, e]) => {
|
|
954
|
+
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(s, e);
|
|
955
|
+
}, [start, end]);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// ===========================================================================
|
|
959
|
+
// STEP DEFINITIONS — Click by named component/selector
|
|
960
|
+
// ===========================================================================
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Click one or more named components — uses locator.click() which auto-scrolls.
|
|
964
|
+
*
|
|
965
|
+
* The negative lookahead keeps three phrasings out of this greedy pattern —
|
|
966
|
+
* "the delete button", "on tasks in the toolbar" and "next button in tour" —
|
|
967
|
+
* because drupal-core / drupal-moderation / varbase own those exact steps and
|
|
968
|
+
* cucumber-js has no notion of a more-specific match winning.
|
|
969
|
+
*
|
|
970
|
+
* Example #1: When I click nav
|
|
971
|
+
* Example #2: When I click search button
|
|
972
|
+
* Example #3: When click cta
|
|
973
|
+
* Example #4: When I click on logo
|
|
974
|
+
* Example #5: When I click menu
|
|
975
|
+
* Example #6: When I click on submit button
|
|
976
|
+
* Example #7: When I click close button
|
|
977
|
+
* Example #8: When click back button
|
|
978
|
+
* Example #9: When I click on forward button
|
|
979
|
+
* Example #10: When I click hero, cta
|
|
980
|
+
*
|
|
981
|
+
* Advanced:
|
|
982
|
+
* Example #11: Click via XPath-registered selector:
|
|
983
|
+
* When I add "login link xp" selector for "//a[contains(.,'Login')]" xpath selector
|
|
984
|
+
* And I click login link xp
|
|
985
|
+
* Example #12: Click a sequence — comma-separated names:
|
|
986
|
+
* Given I define css selectors:
|
|
987
|
+
* | tab 1 | [data-tab='1'] |
|
|
988
|
+
* | tab 2 | [data-tab='2'] |
|
|
989
|
+
* When I click tab 1, tab 2
|
|
990
|
+
* Example #13: Register with CSS then click — no components needed:
|
|
991
|
+
* When I add "cta primary" selector for ".cta .btn-primary" css selector
|
|
992
|
+
* And I click cta primary
|
|
993
|
+
*/
|
|
994
|
+
When(/^(I |we )*click (?!the delete button$|on tasks in the toolbar$|next button in tour$)(?:on |a )?([a-zA-Z0-9 ,\-]+)$/, async function (pronounCase, subjectsText) {
|
|
995
|
+
const names = parseNames(subjectsText);
|
|
996
|
+
const errors = [];
|
|
997
|
+
for (const name of names) {
|
|
998
|
+
const sel = resolveSelector(this, name);
|
|
999
|
+
try { await this.page.locator(sel).first().click(); }
|
|
1000
|
+
catch (e) { errors.push(`Cannot click "${name}" (${sel}): ${e.message}`); }
|
|
1001
|
+
}
|
|
1002
|
+
if (errors.length > 0) throw friendly(errors.join('\n'));
|
|
1003
|
+
});
|