@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,352 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// All page-level assertion step definitions live here:
|
|
4
|
+
// see / not see text, in element, in row, link href, response body, response
|
|
5
|
+
// status code, element existence, CSS property, regex matching, count.
|
|
6
|
+
//
|
|
7
|
+
// Field-level (checkbox/radio/field-contains) assertions live in field.steps.js.
|
|
8
|
+
// URL/path assertions live in navigation.steps.js.
|
|
9
|
+
// Modal assertions live in modal.steps.js.
|
|
10
|
+
|
|
11
|
+
const { Then } = require('@cucumber/cucumber');
|
|
12
|
+
const assert = require('assert');
|
|
13
|
+
const axios = require('axios');
|
|
14
|
+
const { buildSelector, getLocatorText, friendly } = require('./varbase-e2e');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Plain page-text
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Assert that text is or is not present on the page.
|
|
22
|
+
*
|
|
23
|
+
* Example #1: Then I should see "Welcome"
|
|
24
|
+
* Example #2: Then we should see "Your accounts for the group is public"
|
|
25
|
+
* Example #3: Then I should not see "Access denied"
|
|
26
|
+
* Example #4: Then we should not see "Edit layout"
|
|
27
|
+
*
|
|
28
|
+
*/
|
|
29
|
+
Then(/^(I |we )*should( not)* see "([^"]*)?"$/, async function (pronounCase, notCase, expectedText) {
|
|
30
|
+
if (notCase) {
|
|
31
|
+
const text = await this.page.locator('body').textContent() || '';
|
|
32
|
+
assert.ok(!text.includes(expectedText), `Page should NOT contain "${expectedText}" but it does.`);
|
|
33
|
+
} else {
|
|
34
|
+
await this.page.locator('body').filter({ hasText: expectedText }).waitFor({ timeout: 5000 });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Assert that page contains or does not contain text matching a regex pattern.
|
|
40
|
+
*
|
|
41
|
+
* Example #1: Then I should see text matching "^T\w+"
|
|
42
|
+
* Example #2: Then I should not see text matching "^O\w+"
|
|
43
|
+
* Example #3: And we should see text matching "\d{4}"
|
|
44
|
+
*
|
|
45
|
+
*/
|
|
46
|
+
Then(/^(I |we )*should( not)* see text matching "([^"]*)?"$/, async function (pronounCase, notCase, textPattern) {
|
|
47
|
+
const bodyText = await this.page.evaluate(() => document.body.innerText || '');
|
|
48
|
+
const regex = new RegExp(textPattern);
|
|
49
|
+
if (notCase) {
|
|
50
|
+
assert.ok(!regex.test(bodyText), `Page text should NOT match "${textPattern}" but it does.`);
|
|
51
|
+
} else {
|
|
52
|
+
assert.ok(regex.test(bodyText), `Page text should match "${textPattern}" but it does not.`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Table-row text
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Assert text is or is not visible inside a table row identified by text.
|
|
62
|
+
*
|
|
63
|
+
* Example #1: Then I should see "Active" in the "John Smith" row
|
|
64
|
+
* Example #2: Then I should see "In Stock" in the "Product A" row
|
|
65
|
+
* Example #3: Then we should see "Processing" in the "Order #12345" row
|
|
66
|
+
* Example #4: And I should see "Admin" in the "john.smith@example.com" row
|
|
67
|
+
* Example #5: Then I should not see "Admin" in the "Jane Doe" row
|
|
68
|
+
* Example #6: Then I should not see "Out of Stock" in the "Product A" row
|
|
69
|
+
* Example #7: And I should not see "Inactive" in the "john.smith@example.com" row
|
|
70
|
+
*
|
|
71
|
+
*/
|
|
72
|
+
Then(/^(I |we )*should( not)* see "([^"]*)?" in( the)* "([^"]*)?" row$/, async function (pronounCase, notCase, expectedText, theCase, rowIdentifier) {
|
|
73
|
+
const found = await this.page.evaluate(({ id, text }) => {
|
|
74
|
+
const rows = Array.from(document.querySelectorAll('tr'));
|
|
75
|
+
return rows.filter(r => r.textContent.includes(id)).some(r => r.textContent.includes(text));
|
|
76
|
+
}, { id: rowIdentifier, text: expectedText });
|
|
77
|
+
if (notCase) {
|
|
78
|
+
assert.ok(!found, `Found "${expectedText}" in the "${rowIdentifier}" row, but it should not be there.`);
|
|
79
|
+
} else {
|
|
80
|
+
assert.ok(found, `Could not find "${expectedText}" in the "${rowIdentifier}" row.`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Element existence + content
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Assert that an element with a given label exists (or does not) on the page.
|
|
90
|
+
*
|
|
91
|
+
* Example #1: Then I should see a "Username" element
|
|
92
|
+
* Example #2: Then I should not see a "Username" element
|
|
93
|
+
* Example #3: And we should see an "Email" element
|
|
94
|
+
*
|
|
95
|
+
*/
|
|
96
|
+
Then(/^(I |we )*should( not)* see (a|an) "([^"]*)?" element$/, async function (pronounCase, notCase, aAnCase, element) {
|
|
97
|
+
if (notCase) {
|
|
98
|
+
const text = await this.page.locator('body').textContent() || '';
|
|
99
|
+
assert.ok(!text.includes(element), `Page should NOT contain element text "${element}" but it does.`);
|
|
100
|
+
} else {
|
|
101
|
+
const loc = this.page.getByText(element, { exact: true }).first();
|
|
102
|
+
const forAttr = await loc.getAttribute('for').catch(() => null);
|
|
103
|
+
if (forAttr) {
|
|
104
|
+
await this.page.locator('#' + forAttr).waitFor({ state: 'visible', timeout: 3000 });
|
|
105
|
+
} else {
|
|
106
|
+
await loc.waitFor({ state: 'visible', timeout: 3000 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Assert that an element, identified by attribute, exists (or does not) on the page.
|
|
113
|
+
*
|
|
114
|
+
* Example #1: Then I should see a "uname" element by its "id" attr
|
|
115
|
+
* Example #2: Then I should see a "pwordcss" element by attr
|
|
116
|
+
* Example #3: Then I should not see an "emailId" element by its "id" attr
|
|
117
|
+
* Example #4: And I should not see a "countryCss" element by attr
|
|
118
|
+
*
|
|
119
|
+
*/
|
|
120
|
+
Then(/^(I |we )*should( not)* see (a|an) "([^"]*)?" element by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (pronounCase, notCase, aAnCase, attrValue, itsCase, attr, attrWord) {
|
|
121
|
+
const loc = this.page.locator(buildSelector(attrValue, attr));
|
|
122
|
+
if (notCase) {
|
|
123
|
+
assert.strictEqual(await loc.count(), 0, `Element should NOT be present but it is.`);
|
|
124
|
+
} else {
|
|
125
|
+
await loc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Assert that an element (found by its label) contains or does not contain text.
|
|
131
|
+
*
|
|
132
|
+
* Example #1: Then I should see "John Smith" in the "Username" element
|
|
133
|
+
* Example #2: Then I should not see "Joe Smith" in the "Username" element
|
|
134
|
+
* Example #3: And we should see "1234" in the "Password" element
|
|
135
|
+
*
|
|
136
|
+
*/
|
|
137
|
+
Then(/^(I |we )*should( not)* see "([^"]*)?" in( the)* "([^"]*)?" element$/, async function (pronounCase, notCase, expectedText, theCase, element) {
|
|
138
|
+
const forAttr = await this.page.getByText(element, { exact: true }).getAttribute('for').catch(() => null);
|
|
139
|
+
const loc = this.page.locator(forAttr ? '#' + forAttr : element).first();
|
|
140
|
+
await loc.waitFor({ timeout: 5000 });
|
|
141
|
+
const content = await getLocatorText(loc);
|
|
142
|
+
if (notCase) {
|
|
143
|
+
assert.ok(!content.includes(expectedText), `Element should NOT contain "${expectedText}" but it does.`);
|
|
144
|
+
} else {
|
|
145
|
+
assert.ok(content.includes(expectedText), `Element should contain "${expectedText}" but it does not.`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Assert that an element (found by its attribute) contains or does not contain text.
|
|
151
|
+
*
|
|
152
|
+
* Example #1: Then I should see "John Smith" in the "uname" element by its "id" attr
|
|
153
|
+
* Example #2: Then I should see "1234" in the "pwordcss" element by attr
|
|
154
|
+
* Example #3: Then I should not see "John Smith" in the "uname" element by its "id" attr
|
|
155
|
+
* Example #4: Then I should not see "1234" in the "pwordcss" element by attr
|
|
156
|
+
*
|
|
157
|
+
*/
|
|
158
|
+
Then(/^(I |we )*should( not)* see "([^"]*)?" in( the)* "([^"]*)?" element by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (pronounCase, notCase, expectedText, theCase, attrValue, itsCase, attr, attrWord) {
|
|
159
|
+
const loc = this.page.locator(buildSelector(attrValue, attr)).first();
|
|
160
|
+
await loc.waitFor({ timeout: 10000 });
|
|
161
|
+
const content = await getLocatorText(loc);
|
|
162
|
+
if (notCase) {
|
|
163
|
+
assert.ok(!content.includes(expectedText), `Element should NOT contain "${expectedText}" but it does.`);
|
|
164
|
+
} else {
|
|
165
|
+
assert.ok(content.includes(expectedText), `Element should contain "${expectedText}" but it does not.`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Assert that an element's text content does or does not match a regex.
|
|
171
|
+
*
|
|
172
|
+
* Example #1: Then I should see text matching "\d{4}" in the "#year" element
|
|
173
|
+
* Example #2: Then I should see text matching "\$\d+\.\d{2}" in the "#price" element
|
|
174
|
+
* Example #3: Then I should not see text matching "Error" in the "#status" element
|
|
175
|
+
* Example #4: Then we should see text matching "^[A-Z][a-z]+ [A-Z][a-z]+$" in the "#full-name" element
|
|
176
|
+
* Example #5: Then I should see text matching "(0?[1-9]|[12][0-9]|3[01])[\/\-](0?[1-9]|1[012])[\/\-]\d{4}" in the "#date" element
|
|
177
|
+
*
|
|
178
|
+
*/
|
|
179
|
+
Then(/^(I |we )*should( not)* see text matching "([^"]*)?" in( the)* "([^"]*)?" element$/, async function (pronounCase, notCase, textPattern, theCase, element) {
|
|
180
|
+
const loc = this.page.locator(element).first();
|
|
181
|
+
await loc.waitFor({ timeout: 5000 });
|
|
182
|
+
const text = await getLocatorText(loc);
|
|
183
|
+
const regex = new RegExp(textPattern);
|
|
184
|
+
if (notCase) {
|
|
185
|
+
assert.ok(!regex.test(text), `Element "${element}" text should NOT match "${textPattern}" but it does.`);
|
|
186
|
+
} else {
|
|
187
|
+
assert.ok(regex.test(text), `Element "${element}" text should match "${textPattern}" but it does not.`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Assert that an element has or does not have a specific CSS property.
|
|
193
|
+
*
|
|
194
|
+
* Example #1: Then the "body" element should contain "color:white;"
|
|
195
|
+
* Example #2: Then the "body" element should not contain "color:white;"
|
|
196
|
+
* Example #3: Then the "#uname" element should not contain "border:solid 5px red;"
|
|
197
|
+
* Example #4: Then the "pword" element should not contain "font-size: 26px;"
|
|
198
|
+
*
|
|
199
|
+
*/
|
|
200
|
+
Then(/^(the )*"([^"]*)?" element should( not)* contain "([^"]*)?"$/, async function (theCase, selectorRaw, notCase, elementCss) {
|
|
201
|
+
const cssClean = elementCss.replace(/;$/, '');
|
|
202
|
+
const colonIdx = cssClean.indexOf(':');
|
|
203
|
+
const cssProperty = cssClean.substring(0, colonIdx).trim();
|
|
204
|
+
const expectedValue = cssClean.substring(colonIdx + 1).trim();
|
|
205
|
+
|
|
206
|
+
const loc = this.page.locator(buildSelector(selectorRaw)).first();
|
|
207
|
+
await loc.waitFor({ timeout: 10000 });
|
|
208
|
+
const matches = await loc.evaluate((el, { prop, expectedVal }) => {
|
|
209
|
+
function colorToRgb(color) {
|
|
210
|
+
const canvas = document.createElement('canvas');
|
|
211
|
+
canvas.width = canvas.height = 1;
|
|
212
|
+
const ctx = canvas.getContext('2d');
|
|
213
|
+
ctx.fillStyle = 'rgb(1, 2, 3)';
|
|
214
|
+
ctx.fillStyle = color.trim();
|
|
215
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
216
|
+
const d = ctx.getImageData(0, 0, 1, 1).data;
|
|
217
|
+
return 'rgb(' + d[0] + ', ' + d[1] + ', ' + d[2] + ')';
|
|
218
|
+
}
|
|
219
|
+
function isValidColor(value) { return colorToRgb(value) !== 'rgb(1, 2, 3)'; }
|
|
220
|
+
function normalizeColorTokens(value) {
|
|
221
|
+
return value.replace(/rgb\([^)]+\)|rgba\([^)]+\)|#[0-9a-fA-F]+|\b[a-zA-Z]+\b/g,
|
|
222
|
+
(token) => isValidColor(token) ? colorToRgb(token) : token);
|
|
223
|
+
}
|
|
224
|
+
function tokenize(value) {
|
|
225
|
+
const tokens = [], regex = /rgb\([^)]+\)|rgba\([^)]+\)|[^\s]+/g;
|
|
226
|
+
let m;
|
|
227
|
+
while ((m = regex.exec(value)) !== null) tokens.push(m[0]);
|
|
228
|
+
return tokens;
|
|
229
|
+
}
|
|
230
|
+
const computed = window.getComputedStyle(el).getPropertyValue(prop).trim();
|
|
231
|
+
return tokenize(normalizeColorTokens(expectedVal.toLowerCase()))
|
|
232
|
+
.every(token => normalizeColorTokens(computed.toLowerCase()).includes(token));
|
|
233
|
+
}, { prop: cssProperty, expectedVal: expectedValue });
|
|
234
|
+
|
|
235
|
+
if (notCase) {
|
|
236
|
+
assert.ok(!matches, `Element should NOT have CSS "${cssProperty}: ${expectedValue}" but it does.`);
|
|
237
|
+
} else {
|
|
238
|
+
assert.ok(matches, `Element should have CSS "${cssProperty}: ${expectedValue}".`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Assert the exact number of elements matching a CSS selector.
|
|
244
|
+
*
|
|
245
|
+
* Example #1: Then I should see 3 "li" elements
|
|
246
|
+
* Example #2: Then I should see 1 "h1" element
|
|
247
|
+
* Example #3: Then we should see 5 ".card" elements
|
|
248
|
+
* Example #4: Then I should see 0 ".error" elements
|
|
249
|
+
* Example #5: And I should see 10 "table tr" elements
|
|
250
|
+
* Example #6: Then I should see 2 "nav a" elements
|
|
251
|
+
* Example #7: Then we should see 4 "ul li" elements
|
|
252
|
+
* Example #8: Then I should see 1 "form" element
|
|
253
|
+
* Example #9: And should see 6 ".product" elements
|
|
254
|
+
* Example #10: Then I should see 8 "[data-testid='row']" elements
|
|
255
|
+
*/
|
|
256
|
+
Then(/^(I |we )*should see (\d+) "([^"]*)" elements?$/, async function (pronounCase, num, selector) {
|
|
257
|
+
const expected = parseInt(num, 10);
|
|
258
|
+
const actual = await this.page.locator(selector).count();
|
|
259
|
+
if (actual !== expected) {
|
|
260
|
+
throw friendly(`Expected ${expected} "${selector}" element(s), got ${actual}.`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Link href
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Assert that a link (located by its visible text) contains the given URL.
|
|
270
|
+
*
|
|
271
|
+
* Example #1: Then the "Login" link should contain "/log-in"
|
|
272
|
+
* Example #2: And the "About Us" link should contain "/about"
|
|
273
|
+
* Example #3: Then "Home" link should contain "/"
|
|
274
|
+
*
|
|
275
|
+
*/
|
|
276
|
+
Then(/^(the )*"([^"]*)?" link should contain "([^"]*)?"$/, async function (theCase, element, url) {
|
|
277
|
+
const loc = this.page.getByText(element, { exact: true }).first();
|
|
278
|
+
await loc.waitFor({ timeout: 5000 });
|
|
279
|
+
const href = await loc.evaluate(el => el.href || el.getAttribute('href') || '');
|
|
280
|
+
assert.ok(href.includes(url), `Expected link "${element}" href to contain "${url}" but got "${href}"`);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Assert that a link located by its attribute contains the given URL.
|
|
285
|
+
*
|
|
286
|
+
* Example #1: Then the "#about-us-id" link should contain "about" by attr
|
|
287
|
+
* Example #2: And the "aboutUs" link should contain "about" by its "class" attribute
|
|
288
|
+
* Example #3: And the ".contactUs" link should contain "/contact-" by attr
|
|
289
|
+
*
|
|
290
|
+
*/
|
|
291
|
+
Then(/^(the )*"([^"]*)?" link should contain "([^"]*)?" by( its)*(?: "([^"]*)?")* (attribute|attr)$/, async function (theCase, attrValue, url, itsCase, attr, attrWord) {
|
|
292
|
+
const loc = this.page.locator(buildSelector(attrValue, attr)).first();
|
|
293
|
+
await loc.waitFor({ timeout: 5000 });
|
|
294
|
+
const href = await loc.evaluate(el => el.href || el.getAttribute('href') || '');
|
|
295
|
+
assert.ok(href.includes(url), `Expected element href to contain "${url}" but got "${href}"`);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Response body / status
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Assert that the rendered HTML response contains or does not contain text.
|
|
304
|
+
*
|
|
305
|
+
* Reads the entire `<html>` text — useful for checking text that lives in
|
|
306
|
+
* `<head>` (meta descriptions, JSON-LD), `<noscript>`, or hidden regions.
|
|
307
|
+
*
|
|
308
|
+
* Example #1: Then the response should contain "Welcome visitor"
|
|
309
|
+
* Example #2: Then the response should not contain "Access denied"
|
|
310
|
+
* Example #3: And the response should contain "<title>Home</title>"
|
|
311
|
+
* Example #4: Then the response should contain "<html"
|
|
312
|
+
* Example #5: When I am on "/about"
|
|
313
|
+
* Then the response should contain "Our mission"
|
|
314
|
+
* And the response should not contain "TODO"
|
|
315
|
+
*
|
|
316
|
+
*/
|
|
317
|
+
Then(/^(the )*response should( not)* contain "([^"]*)?"$/, async function (theCase, notCase, expectedText) {
|
|
318
|
+
const text = await this.page.locator('html').textContent() || '';
|
|
319
|
+
if (notCase) {
|
|
320
|
+
assert.ok(!text.includes(expectedText), `Response should NOT contain "${expectedText}" but it does.`);
|
|
321
|
+
} else {
|
|
322
|
+
assert.ok(text.includes(expectedText), `Response should contain "${expectedText}" but it does not.`);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Assert that the current page's response status is or is not a given code.
|
|
328
|
+
*
|
|
329
|
+
* Example #1: Then the response status code should be 200
|
|
330
|
+
* Example #2: And the response status code should not be 404
|
|
331
|
+
* Example #3: Then the response status code should be 301
|
|
332
|
+
*
|
|
333
|
+
*/
|
|
334
|
+
Then(/^(the )*response status code should( not)* be (\d+)$/, async function (theCase, notCase, expectedStatusCode) {
|
|
335
|
+
const currentURL = this.page.url();
|
|
336
|
+
try {
|
|
337
|
+
const response = await axios.get(currentURL);
|
|
338
|
+
const status = response.status;
|
|
339
|
+
if (notCase) {
|
|
340
|
+
assert.notStrictEqual(status, parseInt(expectedStatusCode), `Status code should NOT be ${expectedStatusCode}`);
|
|
341
|
+
} else {
|
|
342
|
+
assert.strictEqual(status, parseInt(expectedStatusCode), `Expected status ${expectedStatusCode} but got ${status}`);
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
const status = error.response ? error.response.status : null;
|
|
346
|
+
if (notCase) {
|
|
347
|
+
if (status !== null) assert.notStrictEqual(status, parseInt(expectedStatusCode));
|
|
348
|
+
} else {
|
|
349
|
+
assert.strictEqual(status, parseInt(expectedStatusCode), `Expected status ${expectedStatusCode} but got ${status}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { friendly } = require('./varbase-e2e');
|
|
4
|
+
|
|
5
|
+
// Authentication-state helpers built on Playwright's `storageState` API.
|
|
6
|
+
//
|
|
7
|
+
// Save the cookies + localStorage of an authenticated context to a JSON file
|
|
8
|
+
// once, then restore that snapshot in later scenarios so each test does not
|
|
9
|
+
// have to walk through the login UI. This is the single biggest speedup
|
|
10
|
+
// available for suites that touch authenticated pages — typical savings of
|
|
11
|
+
// 1-3 seconds per scenario.
|
|
12
|
+
//
|
|
13
|
+
// Recommended layout:
|
|
14
|
+
//
|
|
15
|
+
// tests/auth/admin.json
|
|
16
|
+
// tests/auth/editor.json
|
|
17
|
+
// tests/auth/customer.json
|
|
18
|
+
//
|
|
19
|
+
// Recommended setup feature: tag a one-shot scenario `@auth-setup`, run it
|
|
20
|
+
// once via `npx cucumber-js --tags @auth-setup` after every credential
|
|
21
|
+
// rotation, and let everyday scenarios restore the snapshot.
|
|
22
|
+
|
|
23
|
+
const { Given, When } = require('@cucumber/cucumber');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
|
|
27
|
+
function resolveStoragePath(p) {
|
|
28
|
+
return path.isAbsolute(p) ? p : path.join(process.cwd(), p);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Save the current browser context's cookies + localStorage to a JSON file.
|
|
33
|
+
*
|
|
34
|
+
* Run this AFTER a successful interactive login. The destination file is
|
|
35
|
+
* created (with parent directories) if it does not exist; existing files are
|
|
36
|
+
* overwritten.
|
|
37
|
+
*
|
|
38
|
+
* Example #1: When I save the auth state to "tests/auth/admin.json"
|
|
39
|
+
* Example #2: Given I am on "/login"
|
|
40
|
+
* When I fill in "admin@example.com" for "Email"
|
|
41
|
+
* And I fill in "secret" for "Password"
|
|
42
|
+
* And I press "Sign in"
|
|
43
|
+
* And I wait until the URL contains "/dashboard"
|
|
44
|
+
* Then I save the auth state to "tests/auth/admin.json"
|
|
45
|
+
* Example #3: When I save the auth state to "tests/auth/editor.json"
|
|
46
|
+
* Example #4: When we save the auth state to "tests/auth/customer.json"
|
|
47
|
+
* Example #5: When I save the auth state to "/tmp/admin-state.json"
|
|
48
|
+
*
|
|
49
|
+
*/
|
|
50
|
+
When(/^(I |we )*save the auth state to "([^"]*)"$/, async function (pronoun, target) {
|
|
51
|
+
const file = resolveStoragePath(target);
|
|
52
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
53
|
+
await this.context.storageState({ path: file });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Restore cookies + localStorage from a previously saved JSON file.
|
|
58
|
+
*
|
|
59
|
+
* The current browser context is closed and a fresh one is opened with the
|
|
60
|
+
* saved state. The varbase-e2e init script (AJAX/timer/mutation tracker) is
|
|
61
|
+
* re-installed automatically so smart waits keep working. Subsequent steps
|
|
62
|
+
* proceed against the restored, authenticated session.
|
|
63
|
+
*
|
|
64
|
+
* Example #1: Given I restore the auth state from "tests/auth/admin.json"
|
|
65
|
+
* Example #2: Given I restore the auth state from "tests/auth/editor.json"
|
|
66
|
+
* And I am on "/admin/products"
|
|
67
|
+
* Example #3: Given I restore the auth state from "tests/auth/customer.json"
|
|
68
|
+
* And I am on "/account/orders"
|
|
69
|
+
* Example #4: Given we restore the auth state from "tests/auth/admin.json"
|
|
70
|
+
* Example #5: Given I restore the auth state from "tests/auth/admin.json"
|
|
71
|
+
* And I am on "/dashboard"
|
|
72
|
+
* Then "<#user-list>" should have a count of 5 within 5 seconds
|
|
73
|
+
*
|
|
74
|
+
*/
|
|
75
|
+
Given(/^(I |we )*restore the auth state from "([^"]*)"$/, async function (pronoun, source) {
|
|
76
|
+
const file = resolveStoragePath(source);
|
|
77
|
+
if (!fs.existsSync(file)) {
|
|
78
|
+
throw friendly(`Auth state file not found: ${file}`);
|
|
79
|
+
}
|
|
80
|
+
const playwrightConfig = require(path.join(process.cwd(), 'playwright.config'));
|
|
81
|
+
const newContext = await this.playwrightBrowser.newContext({
|
|
82
|
+
...playwrightConfig.contextOptions,
|
|
83
|
+
storageState: file,
|
|
84
|
+
});
|
|
85
|
+
await newContext.addInitScript(() => {
|
|
86
|
+
if (window.__varbaseE2eAjaxInstalled) return;
|
|
87
|
+
window.__varbaseE2eAjaxInstalled = true;
|
|
88
|
+
window.__varbaseE2eAjaxCount = 0;
|
|
89
|
+
window.__varbaseE2ePendingTimers = 0;
|
|
90
|
+
window.__varbaseE2eLastMutation = Date.now();
|
|
91
|
+
});
|
|
92
|
+
if (this.context) await this.context.close();
|
|
93
|
+
this.context = newContext;
|
|
94
|
+
this.page = await this.context.newPage();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Clear cookies and localStorage / sessionStorage for the current context.
|
|
99
|
+
*
|
|
100
|
+
* Use to anonymize a session mid-scenario, e.g. after asserting a logged-in
|
|
101
|
+
* action you may want to verify the same path returns 302 / login form for
|
|
102
|
+
* an anonymous visitor.
|
|
103
|
+
*
|
|
104
|
+
* Example #1: Given I clear the auth state
|
|
105
|
+
* Example #2: Given I restore the auth state from "tests/auth/admin.json"
|
|
106
|
+
* And I am on "/dashboard"
|
|
107
|
+
* Given I clear the auth state
|
|
108
|
+
* When I am on "/dashboard"
|
|
109
|
+
* Then I should see "Sign in"
|
|
110
|
+
* Example #3: Given we clear the auth state
|
|
111
|
+
* Example #4: When I press "Logout"
|
|
112
|
+
* And I clear the auth state
|
|
113
|
+
* Example #5: Given I clear the auth state
|
|
114
|
+
* When I am on the homepage
|
|
115
|
+
* Then I should see "Login"
|
|
116
|
+
*
|
|
117
|
+
*/
|
|
118
|
+
Given(/^(I |we )*clear the auth state$/, async function () {
|
|
119
|
+
await this.context.clearCookies();
|
|
120
|
+
try {
|
|
121
|
+
await this.page.evaluate(() => {
|
|
122
|
+
try { localStorage.clear(); } catch { /* no-op */ }
|
|
123
|
+
try { sessionStorage.clear(); } catch { /* no-op */ }
|
|
124
|
+
});
|
|
125
|
+
} catch { /* page may not be on a real URL yet */ }
|
|
126
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { friendly } = require('./varbase-e2e');
|
|
4
|
+
|
|
5
|
+
// Clock / time-mocking steps backed by Playwright's `page.clock` API.
|
|
6
|
+
|
|
7
|
+
const { Given, When } = require('@cucumber/cucumber');
|
|
8
|
+
|
|
9
|
+
const ISO_HINT = 'install the fake clock first via "Given the system time is \\"YYYY-MM-DDTHH:MM:SSZ\\"".';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Install a fake clock anchored at an ISO 8601 instant.
|
|
13
|
+
*
|
|
14
|
+
* Example #1: Given the system time is "2026-05-08T10:00:00Z"
|
|
15
|
+
* Example #2: Given the system time is "2026-01-01T00:00:00Z"
|
|
16
|
+
* Example #3: Given the system time is "2026-12-31T23:59:55Z"
|
|
17
|
+
* Example #4: Given the system time is "2026-05-08T10:00:00Z"
|
|
18
|
+
* Example #5: Given the system time is "2026-07-04T12:00:00-04:00"
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
21
|
+
Given(/^the system time is "([^"]*)"$/, async function (iso) {
|
|
22
|
+
const d = new Date(iso);
|
|
23
|
+
if (isNaN(d.getTime())) {
|
|
24
|
+
throw friendly({
|
|
25
|
+
action: `set the system time to "${iso}"`,
|
|
26
|
+
hint: 'use an ISO 8601 date like "2026-05-08T10:00:00Z".',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
try { await this.page.clock.install({ time: d }); }
|
|
30
|
+
catch (e) { throw friendly({ action: `install a fake clock at "${iso}"`, cause: e, hint: 'open a page first via "Given I am on \\"/path\\"".' }); }
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Advance the fake clock forward by N milliseconds.
|
|
35
|
+
*
|
|
36
|
+
* Example #1: When I advance the clock by 500 ms
|
|
37
|
+
* Example #2: When I advance the clock by 1500 ms
|
|
38
|
+
* Example #3: When we advance the clock by 250 ms
|
|
39
|
+
* Example #4: And I advance the clock by 100 ms
|
|
40
|
+
* Example #5: When I advance the clock by 60000 ms
|
|
41
|
+
*
|
|
42
|
+
*/
|
|
43
|
+
When(/^(I |we )*advance the clock by (\d+) ?ms$/, async function (pronoun, ms) {
|
|
44
|
+
try { await this.page.clock.runFor(parseInt(ms, 10)); }
|
|
45
|
+
catch (e) { throw friendly({ action: `advance the clock by ${ms} ms`, cause: e, hint: ISO_HINT }); }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Advance the fake clock forward by N seconds.
|
|
50
|
+
*
|
|
51
|
+
* Example #1: When I advance the clock by 30 seconds
|
|
52
|
+
* Example #2: When I advance the clock by 5 seconds
|
|
53
|
+
* Example #3: When we advance the clock by 1 second
|
|
54
|
+
* Example #4: And I advance the clock by 10 seconds
|
|
55
|
+
* Example #5: When I advance the clock by 90 seconds
|
|
56
|
+
*
|
|
57
|
+
*/
|
|
58
|
+
When(/^(I |we )*advance the clock by (\d+) seconds?$/, async function (pronoun, seconds) {
|
|
59
|
+
try { await this.page.clock.runFor(parseInt(seconds, 10) * 1000); }
|
|
60
|
+
catch (e) { throw friendly({ action: `advance the clock by ${seconds} second(s)`, cause: e, hint: ISO_HINT }); }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Advance the fake clock forward by N minutes.
|
|
65
|
+
*
|
|
66
|
+
* Example #1: When I advance the clock by 5 minutes
|
|
67
|
+
* Example #2: When I advance the clock by 1 minute
|
|
68
|
+
* Example #3: When we advance the clock by 14 minutes
|
|
69
|
+
* Example #4: And I advance the clock by 30 minutes
|
|
70
|
+
* Example #5: When I advance the clock by 60 minutes
|
|
71
|
+
*
|
|
72
|
+
*/
|
|
73
|
+
When(/^(I |we )*advance the clock by (\d+) minutes?$/, async function (pronoun, minutes) {
|
|
74
|
+
try { await this.page.clock.runFor(parseInt(minutes, 10) * 60 * 1000); }
|
|
75
|
+
catch (e) { throw friendly({ action: `advance the clock by ${minutes} minute(s)`, cause: e, hint: ISO_HINT }); }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Pause the fake clock.
|
|
80
|
+
*
|
|
81
|
+
* Example #1: When I pause the clock
|
|
82
|
+
* Example #2: When I pause the clock
|
|
83
|
+
* Example #3: When we pause the clock
|
|
84
|
+
* Example #4: And I pause the clock
|
|
85
|
+
* Example #5: When I pause the clock
|
|
86
|
+
*
|
|
87
|
+
*/
|
|
88
|
+
When(/^(I |we )*pause the clock$/, async function () {
|
|
89
|
+
try { await this.page.clock.pauseAt(await this.page.evaluate(() => Date.now())); }
|
|
90
|
+
catch (e) { throw friendly({ action: 'pause the clock', cause: e, hint: ISO_HINT }); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resume the fake clock.
|
|
95
|
+
*
|
|
96
|
+
* Example #1: When I resume the clock
|
|
97
|
+
* Example #2: When I resume the clock
|
|
98
|
+
* Example #3: When we resume the clock
|
|
99
|
+
* Example #4: And I resume the clock
|
|
100
|
+
* Example #5: When I resume the clock
|
|
101
|
+
*
|
|
102
|
+
*/
|
|
103
|
+
When(/^(I |we )*resume the clock$/, async function () {
|
|
104
|
+
try { await this.page.clock.resume(); }
|
|
105
|
+
catch (e) { throw friendly({ action: 'resume the clock', cause: e, hint: 'pause the clock first via "When I pause the clock".' }); }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set the fake clock to a specific time.
|
|
110
|
+
*
|
|
111
|
+
* Example #1: When I set the system time to "2026-05-08T12:30:00Z"
|
|
112
|
+
* Example #2: When I set the system time to "2026-12-31T23:59:55Z"
|
|
113
|
+
* Example #3: When we set the system time to "2026-01-01T00:00:00Z"
|
|
114
|
+
* Example #4: And I set the system time to "2026-07-04T08:00:00-04:00"
|
|
115
|
+
* Example #5: When I set the system time to "2027-01-01T00:00:00Z"
|
|
116
|
+
*
|
|
117
|
+
*/
|
|
118
|
+
When(/^(I |we )*set the system time to "([^"]*)"$/, async function (pronoun, iso) {
|
|
119
|
+
const d = new Date(iso);
|
|
120
|
+
if (isNaN(d.getTime())) {
|
|
121
|
+
throw friendly({
|
|
122
|
+
action: `set the system time to "${iso}"`,
|
|
123
|
+
hint: 'use an ISO 8601 date like "2026-05-08T12:30:00Z".',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
try { await this.page.clock.setSystemTime(d); }
|
|
127
|
+
catch (e) { throw friendly({ action: `set the system time to "${iso}"`, cause: e, hint: ISO_HINT }); }
|
|
128
|
+
});
|