@manufac/general-tests 0.5.0 → 0.9.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.
Files changed (62) hide show
  1. package/README.md +88 -0
  2. package/dist/config/projects.d.ts +3 -1
  3. package/dist/config/projects.js +4 -2
  4. package/dist/fixtures/index.d.ts +2 -1
  5. package/dist/fixtures/index.js +1 -0
  6. package/dist/schema/index.d.ts +13 -0
  7. package/dist/schema/index.js +7 -0
  8. package/dist/specs/cta-destinations.spec.js +65 -0
  9. package/dist/specs/page-visual-regression.spec.js +68 -0
  10. package/dist/specs/{visual-regression.spec.js → storybook-visual-regression.spec.js} +7 -0
  11. package/dist/specs/utils.d.ts +4 -158
  12. package/dist/specs/utils.js +31 -857
  13. package/package.json +1 -1
  14. package/dist/specs/desktop-only/browser-zoom.spec.js +0 -26
  15. package/dist/specs/mobile-only/ios-safari-toolbar.spec.js +0 -62
  16. package/dist/specs/mobile-only/ios-safe-areas.spec.js +0 -36
  17. package/dist/specs/mobile-only/navigation.spec.d.ts +0 -1
  18. package/dist/specs/mobile-only/navigation.spec.js +0 -40
  19. package/dist/specs/mobile-only/orientation.spec.d.ts +0 -1
  20. package/dist/specs/mobile-only/orientation.spec.js +0 -46
  21. package/dist/specs/mobile-only/readability.spec.d.ts +0 -1
  22. package/dist/specs/mobile-only/readability.spec.js +0 -14
  23. package/dist/specs/mobile-only/soft-keyboard.spec.d.ts +0 -1
  24. package/dist/specs/mobile-only/soft-keyboard.spec.js +0 -43
  25. package/dist/specs/mobile-only/touch-targets.spec.d.ts +0 -1
  26. package/dist/specs/mobile-only/touch-targets.spec.js +0 -12
  27. package/dist/specs/shared/cta.spec.d.ts +0 -1
  28. package/dist/specs/shared/cta.spec.js +0 -199
  29. package/dist/specs/shared/forms/autofill.spec.d.ts +0 -1
  30. package/dist/specs/shared/forms/autofill.spec.js +0 -28
  31. package/dist/specs/shared/forms/double-submit.spec.d.ts +0 -1
  32. package/dist/specs/shared/forms/double-submit.spec.js +0 -25
  33. package/dist/specs/shared/forms/input-edge.spec.d.ts +0 -1
  34. package/dist/specs/shared/forms/input-edge.spec.js +0 -60
  35. package/dist/specs/shared/forms/invalid-input.spec.d.ts +0 -1
  36. package/dist/specs/shared/forms/invalid-input.spec.js +0 -41
  37. package/dist/specs/shared/forms/native-inputs.spec.d.ts +0 -1
  38. package/dist/specs/shared/forms/native-inputs.spec.js +0 -37
  39. package/dist/specs/shared/forms/required-field.spec.d.ts +0 -1
  40. package/dist/specs/shared/forms/required-field.spec.js +0 -39
  41. package/dist/specs/shared/forms/thankyou-confirm.spec.d.ts +0 -1
  42. package/dist/specs/shared/forms/thankyou-confirm.spec.js +0 -34
  43. package/dist/specs/shared/hero-fold.spec.d.ts +0 -1
  44. package/dist/specs/shared/hero-fold.spec.js +0 -40
  45. package/dist/specs/shared/image-quality.spec.d.ts +0 -1
  46. package/dist/specs/shared/image-quality.spec.js +0 -13
  47. package/dist/specs/shared/media-embeds/carousel-slider.spec.d.ts +0 -1
  48. package/dist/specs/shared/media-embeds/carousel-slider.spec.js +0 -31
  49. package/dist/specs/shared/media-embeds/cookie-consent.spec.d.ts +0 -1
  50. package/dist/specs/shared/media-embeds/cookie-consent.spec.js +0 -27
  51. package/dist/specs/shared/media-embeds/third-party-embeds.spec.d.ts +0 -1
  52. package/dist/specs/shared/media-embeds/third-party-embeds.spec.js +0 -27
  53. package/dist/specs/shared/media-embeds/video-playback.spec.d.ts +0 -1
  54. package/dist/specs/shared/media-embeds/video-playback.spec.js +0 -54
  55. package/dist/specs/shared/sticky-elements.spec.d.ts +0 -1
  56. package/dist/specs/shared/sticky-elements.spec.js +0 -13
  57. package/dist/specs/sticky-utils.d.ts +0 -8
  58. package/dist/specs/sticky-utils.js +0 -175
  59. package/dist/specs/visual-regression.spec.d.ts +0 -1
  60. /package/dist/specs/{desktop-only/browser-zoom.spec.d.ts → cta-destinations.spec.d.ts} +0 -0
  61. /package/dist/specs/{mobile-only/ios-safari-toolbar.spec.d.ts → page-visual-regression.spec.d.ts} +0 -0
  62. /package/dist/specs/{mobile-only/ios-safe-areas.spec.d.ts → storybook-visual-regression.spec.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manufac/general-tests",
3
- "version": "0.5.0",
3
+ "version": "0.9.0",
4
4
  "description": "A library for general tests for landing pages",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,26 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const utils_1 = require("../utils");
5
- const test_1 = require("@playwright/test");
6
- (0, fixtures_1.test)("RS-07 Browser zoom (desktop) at 200%", async ({ page, targetPage }) => {
7
- // Real browser zoom shrinks the effective CSS layout viewport, which is what makes responsive
8
- // breakpoints react to it — the non-standard CSS `zoom` property only scales rendered output
9
- // and leaves window.innerWidth/matchMedia untouched, so it doesn't reproduce that behavior.
10
- // Dividing the viewport by the zoom factor reproduces the actual layout effect directly.
11
- await page.setViewportSize({
12
- width: Math.round(utils_1.ResponsiveConfig.ViewPortWidth / utils_1.ResponsiveConfig.ZoomFactor),
13
- height: Math.round(utils_1.ResponsiveConfig.ViewPortHeight / utils_1.ResponsiveConfig.ZoomFactor),
14
- });
15
- await page.goto(targetPage);
16
- await page.waitForTimeout(utils_1.ResponsiveConfig.ZoomSettleDelayMs);
17
- await (0, utils_1.scrollThroughPage)(page);
18
- // Some layouts transition position (e.g. a header offset tied to scroll state) on a short
19
- // CSS transition; without this, checks below can catch a mid-transition frame instead of
20
- // the actual settled layout.
21
- await page.waitForTimeout(utils_1.ResponsiveConfig.ZoomSettleDelayMs);
22
- const brokenText = await (0, utils_1.findTruncatedOrOverflowingText)(page);
23
- (0, test_1.expect)(brokenText, `Text overlapping/clipped at 200% zoom:\n${brokenText.join("\n")}`).toEqual([]);
24
- const overlappingTargets = await (0, utils_1.findOverlappingElements)(page, utils_1.TappableElementsSelector);
25
- (0, test_1.expect)(overlappingTargets, `Interactive elements unreachable (overlapping) at 200% zoom:\n${overlappingTargets.join("\n")}`).toEqual([]);
26
- });
@@ -1,62 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const sticky_utils_1 = require("../sticky-utils");
5
- const utils_1 = require("../utils");
6
- const test_1 = require("@playwright/test");
7
- // Safari toolbar height change is typically around 50px on modern iOS devices
8
- const ToolbarHeightDeltaPX = 50;
9
- fixtures_1.test.describe("Cross-Browser - iOS Safari Toolbar", () => {
10
- (0, fixtures_1.test)("XB-03 Page adapts to iOS Safari toolbar size changes smoothly", async ({ page, targetPage, browserName }) => {
11
- fixtures_1.test.slow(); // This test does two full scroll passes (40+ evaluates) and needs more than 30s
12
- const initialViewport = page.viewportSize();
13
- // true when the viewport width is below 768px (a mobile-sized device, not a desktop viewport)
14
- const isMobileViewport = initialViewport !== null && initialViewport.width < 768;
15
- if (browserName !== "webkit" || !isMobileViewport) {
16
- // test.skip aborts execution internally, so no return or else block is needed
17
- fixtures_1.test.skip(true, "Safari toolbar behavior test is only relevant for mobile viewports on iOS (WebKit)");
18
- }
19
- if (initialViewport === null) {
20
- throw new Error("Viewport size is required but was null");
21
- }
22
- await page.goto(targetPage);
23
- // Initial check - ensure no cut-off elements before we even start
24
- const initialCutoffElements = await (0, utils_1.findElementsCutoffAtViewportEdge)(page);
25
- (0, test_1.expect)(initialCutoffElements, `Elements were already cut off before resizing the viewport:\n${initialCutoffElements.join("\n")}`).toEqual([]);
26
- const initialInnerHeight = await page.evaluate(() => {
27
- return window.innerHeight;
28
- });
29
- // 1. Simulate scrolling down (toolbar shrinks -> viewport height INCREASES)
30
- const expandedHeight = initialViewport.height + ToolbarHeightDeltaPX;
31
- await page.setViewportSize({ width: initialViewport.width, height: expandedHeight });
32
- await page.waitForFunction((prevHeight) => {
33
- // Avoid strict equality (===) as mobile viewports can have fractional/scaled innerHeights
34
- return window.innerHeight > prevHeight;
35
- }, initialInnerHeight);
36
- const cutoffAfterExpand = await (0, utils_1.findElementsCutoffAtViewportEdge)(page);
37
- (0, test_1.expect)(cutoffAfterExpand, `Elements were cut off when the viewport expanded (simulating toolbar shrinking):\n${cutoffAfterExpand.join("\n")}`).toEqual([]);
38
- // Check sticky elements don't break after expansion
39
- const stickyIssuesAfterExpand = await (0, sticky_utils_1.checkStickyElements)(page);
40
- if (stickyIssuesAfterExpand.stickyElementCount > 0) {
41
- (0, test_1.expect)(stickyIssuesAfterExpand.flicker, `Sticky elements flickered/detached when viewport expanded:\n${stickyIssuesAfterExpand.flicker.join("\n")}`).toEqual([]);
42
- (0, test_1.expect)(stickyIssuesAfterExpand.coversContent, `Sticky elements covered content when viewport expanded:\n${stickyIssuesAfterExpand.coversContent.join("\n")}`).toEqual([]);
43
- }
44
- const afterExpandHeight = await page.evaluate(() => {
45
- return window.innerHeight;
46
- });
47
- // 2. Simulate scrolling up (toolbar expands -> viewport height DECREASES)
48
- const shrunkHeight = initialViewport.height - ToolbarHeightDeltaPX;
49
- await page.setViewportSize({ width: initialViewport.width, height: shrunkHeight });
50
- await page.waitForFunction((prevHeight) => {
51
- return window.innerHeight < prevHeight;
52
- }, afterExpandHeight);
53
- const cutoffAfterShrink = await (0, utils_1.findElementsCutoffAtViewportEdge)(page);
54
- (0, test_1.expect)(cutoffAfterShrink, `Elements were cut off when the viewport shrank (simulating toolbar expanding):\n${cutoffAfterShrink.join("\n")}`).toEqual([]);
55
- // Check sticky elements don't break after shrinking
56
- const stickyIssuesAfterShrink = await (0, sticky_utils_1.checkStickyElements)(page);
57
- if (stickyIssuesAfterShrink.stickyElementCount > 0) {
58
- (0, test_1.expect)(stickyIssuesAfterShrink.flicker, `Sticky elements flickered/detached when viewport shrank:\n${stickyIssuesAfterShrink.flicker.join("\n")}`).toEqual([]);
59
- (0, test_1.expect)(stickyIssuesAfterShrink.coversContent, `Sticky elements covered content when viewport shrank:\n${stickyIssuesAfterShrink.coversContent.join("\n")}`).toEqual([]);
60
- }
61
- });
62
- });
@@ -1,36 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const utils_1 = require("../utils");
5
- const test_1 = require("@playwright/test");
6
- fixtures_1.test.describe("Cross-Browser - iOS Safe Areas", () => {
7
- (0, fixtures_1.test)("XB-02 Content respects iOS safe areas in portrait and landscape", async ({ page, targetPage, browserName }) => {
8
- fixtures_1.test.skip(browserName !== "webkit", "Safe area test is only relevant for iOS (WebKit)");
9
- const viewportInfo = page.viewportSize();
10
- const isMobileViewport = viewportInfo !== null && viewportInfo.width < 768;
11
- if (!isMobileViewport) {
12
- fixtures_1.test.skip(true, "Safe area test is only relevant for mobile viewports");
13
- }
14
- await page.goto(targetPage);
15
- // Check Portrait
16
- const portraitOverlaps = await (0, utils_1.findSafeAreaOverlaps)(page);
17
- const portraitDescriptions = portraitOverlaps.map((overlap) => {
18
- return `${(0, utils_1.describeElement)(overlap)} overlaps ${overlap.overlapZone} safe area: "${overlap.text}"`;
19
- });
20
- (0, test_1.expect)(portraitOverlaps, `Elements should not overlap portrait safe areas:\n${portraitDescriptions.join("\n")}`).toEqual([]);
21
- // Check Landscape
22
- if (viewportInfo !== null) {
23
- // Rotate to landscape
24
- await page.setViewportSize({ width: viewportInfo.height, height: viewportInfo.width });
25
- await page.waitForFunction((expectedWidth) => {
26
- const isMatch = window.innerWidth === expectedWidth;
27
- return isMatch;
28
- }, viewportInfo.height);
29
- const landscapeOverlaps = await (0, utils_1.findSafeAreaOverlaps)(page);
30
- const landscapeDescriptions = landscapeOverlaps.map((overlap) => {
31
- return `${(0, utils_1.describeElement)(overlap)} overlaps ${overlap.overlapZone} safe area: "${overlap.text}"`;
32
- });
33
- (0, test_1.expect)(landscapeOverlaps, `Elements should not overlap landscape safe areas:\n${landscapeDescriptions.join("\n")}`).toEqual([]);
34
- }
35
- });
36
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const test_1 = require("@playwright/test");
5
- fixtures_1.test.describe("Mobile Navigation", () => {
6
- (0, fixtures_1.test)("CT-05: Hamburger/mobile nav", async ({ page, targetPage }) => {
7
- await page.goto(targetPage);
8
- const hamburger = page
9
- .getByRole("button", { name: /menu|navigation|hamburger/i })
10
- .or(page.locator(".hamburger, .menu-toggle"))
11
- .or(page.getByRole("banner").getByRole("button"))
12
- .first();
13
- const count = await hamburger.count();
14
- if (count === 0) {
15
- fixtures_1.test.skip(true, "No hamburger menu button found on the page");
16
- }
17
- const isVisible = await hamburger.isVisible();
18
- if (!isVisible) {
19
- fixtures_1.test.skip(true, "Hamburger menu button is not visible");
20
- }
21
- await hamburger.click();
22
- await page.waitForTimeout(500); // Wait for transition
23
- const isScrollLocked = await page.evaluate(() => {
24
- const { body } = document;
25
- const html = document.documentElement;
26
- const bodyOverflow = window.getComputedStyle(body).overflow;
27
- const htmlOverflow = window.getComputedStyle(html).overflow;
28
- const hasOverflowHidden = ["hidden", "clip"].includes(bodyOverflow) || ["hidden", "clip"].includes(htmlOverflow);
29
- const hasLockAttribute = body.hasAttribute("data-scroll-locked") || html.hasAttribute("data-scroll-locked");
30
- // Some libraries like react-remove-scroll (when removeScrollBar=false) lock scroll via JS events without CSS
31
- const wheelEvent = new WheelEvent("wheel", { deltaY: 100, bubbles: true, cancelable: true });
32
- document.body.dispatchEvent(wheelEvent);
33
- const touchEvent = new TouchEvent("touchmove", { bubbles: true, cancelable: true });
34
- document.body.dispatchEvent(touchEvent);
35
- const isLocked = hasOverflowHidden || hasLockAttribute || wheelEvent.defaultPrevented || touchEvent.defaultPrevented;
36
- return isLocked;
37
- });
38
- (0, test_1.expect)(isScrollLocked, "Page behind the open mobile menu should not be scrollable").toBe(true);
39
- });
40
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,46 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const utils_1 = require("../utils");
5
- const test_1 = require("@playwright/test");
6
- (0, fixtures_1.test)("RS-02 Orientation change (mobile) reflows without overlap and preserves scroll section", async ({ page, targetPage, }) => {
7
- await page.goto(targetPage);
8
- const portraitViewport = page.viewportSize();
9
- if (portraitViewport === null) {
10
- throw new Error("Viewport size must be set before running orientation test");
11
- }
12
- const { width, height } = portraitViewport;
13
- const { scrollHeight, clientHeight } = await (0, utils_1.getScrollMetrics)(page);
14
- const isScrollable = scrollHeight > clientHeight;
15
- const scrollTarget = isScrollable ? Math.floor(scrollHeight / 2) : 0;
16
- if (isScrollable) {
17
- await page.evaluate((y) => {
18
- window.scrollTo(0, y);
19
- }, scrollTarget);
20
- // scroll-behavior: smooth animates the scroll; wait for it to actually
21
- // land near the target before treating it as the baseline position.
22
- await page.waitForFunction((target) => {
23
- return Math.abs(window.scrollY - target) < 5;
24
- }, scrollTarget, { timeout: 2000 });
25
- }
26
- const baselineMetrics = await (0, utils_1.getScrollMetrics)(page);
27
- const scrollRatioBefore = isScrollable ? baselineMetrics.scrollY / baselineMetrics.scrollHeight : 0;
28
- const phases = [
29
- { name: "landscape", viewport: { width: height, height: width } },
30
- { name: "portrait", viewport: { width, height } },
31
- ];
32
- for (const phase of phases) {
33
- await page.setViewportSize(phase.viewport);
34
- // Allow any smooth-scroll/reflow triggered by the viewport change to settle.
35
- await page.waitForTimeout(300);
36
- const cutoffElements = await (0, utils_1.findElementsCutoffAtViewportEdge)(page);
37
- (0, test_1.expect)(cutoffElements, `Found element(s) cut off at the viewport edge after rotating to ${phase.name}:\n${cutoffElements.join("\n")}`).toEqual([]);
38
- const overlappingElements = await (0, utils_1.findOverlappingElements)(page, utils_1.ContentOverlapSelector);
39
- (0, test_1.expect)(overlappingElements, `Found overlapping element(s) after rotating to ${phase.name}:\n${overlappingElements.join("\n")}`).toEqual([]);
40
- if (isScrollable) {
41
- const currentMetrics = await (0, utils_1.getScrollMetrics)(page);
42
- const scrollRatioAfter = currentMetrics.scrollY / currentMetrics.scrollHeight;
43
- (0, test_1.expect)(Math.abs(scrollRatioAfter - scrollRatioBefore), `Scroll position jumped away from the original section after rotating to ${phase.name}`).toBeLessThan(utils_1.ResponsiveConfig.ScrollDriftTolerance);
44
- }
45
- }
46
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,14 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const utils_1 = require("../utils");
5
- const test_1 = require("@playwright/test");
6
- (0, fixtures_1.test)("RS-04 Text readability at 320px", async ({ page, targetPage }) => {
7
- await page.goto(targetPage);
8
- const undersizedText = await (0, utils_1.findUndersizedBodyText)(page);
9
- (0, test_1.expect)(undersizedText, `Found ${String(undersizedText.length)} body text element(s) below ${String(utils_1.ResponsiveConfig.MinBodyTextFontSizePX)}px at 320px width:\n${undersizedText.join("\n")}`).toEqual([]);
10
- const brokenText = await (0, utils_1.findTruncatedOrOverflowingText)(page);
11
- (0, test_1.expect)(brokenText, `Truncated or overflowing text at 320px width:\n${brokenText.join("\n")}`).toEqual([]);
12
- const isScrollable = await (0, utils_1.hasHorizontalScrollbar)(page);
13
- (0, test_1.expect)(isScrollable, "Horizontal scrollbar present at 320px — text requires horizontal scrolling to read").toBe(false);
14
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,43 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const utils_1 = require("../utils");
5
- const test_1 = require("@playwright/test");
6
- fixtures_1.test.describe("Forms - Soft Keyboard", () => {
7
- utils_1.KeyboardFields.forEach((keyboardField) => {
8
- (0, fixtures_1.test)(`FM-05 ${keyboardField.label.source} field opens the correct soft keyboard`, async ({ page, targetPage }) => {
9
- await page.goto(targetPage);
10
- const field = page.getByLabel(keyboardField.label);
11
- const fieldCount = await field.count();
12
- if (fieldCount === 0) {
13
- fixtures_1.test.skip(true, `No ${keyboardField.label.source} field present on this page`);
14
- }
15
- const viewport = page.viewportSize();
16
- if (viewport === null) {
17
- throw new Error("Viewport size must be set before running the soft keyboard test");
18
- }
19
- await field.click();
20
- // NOTE: Playwright cannot render a real device's on-screen keyboard, so the
21
- // originally-specified check ("email field opens the @ keyboard, phone field
22
- // opens the number pad") is verified indirectly via the input's inputmode/type
23
- // attribute, which is what drives the OS keyboard choice on a real device.
24
- const [inputMode, inputType] = await Promise.all([field.getAttribute("inputmode"), field.getAttribute("type")]);
25
- const hasCorrectKeyboardHint = keyboardField.expectedInputMode.test(inputMode ?? "") || keyboardField.expectedInputMode.test(inputType ?? "");
26
- (0, test_1.expect)(hasCorrectKeyboardHint, `${keyboardField.label.source} field should have an inputmode/type that opens the correct soft keyboard`).toBe(true);
27
- // Note: A real on-screen keyboard typically covers roughly the bottom 40% of a mobile viewport.
28
- // Playwright can't render an actual OS keyboard, so this is approximated by shrinking the
29
- // viewport after focusing the field, turning "field shouldn't be hidden behind the keyboard"
30
- // into a real geometry check instead of one that trivially always passes.
31
- //
32
- // Limitation (FM-05): This is a best-effort regression guard, not a substitute for real-device testing.
33
- // The 40% occlusion is a rough estimate and page.setViewportSize() doesn't replicate the
34
- // native viewport repositioning/scrolling behavior of real mobile browsers when keyboards open.
35
- // Manual verification on a real device remains the source of truth for FM-05.
36
- const visibleHeight = Math.round(viewport.height * (1 - utils_1.ResponsiveConfig.KeyboardOcclusionRatio));
37
- await page.setViewportSize({ width: viewport.width, height: visibleHeight });
38
- const fieldBox = await field.boundingBox();
39
- const isAboveKeyboard = fieldBox !== null && fieldBox.y >= 0 && fieldBox.y + fieldBox.height <= visibleHeight;
40
- (0, test_1.expect)(isAboveKeyboard, `${keyboardField.label.source} field should never be hidden behind the keyboard; the page should scroll it into view`).toBe(true);
41
- });
42
- });
43
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const utils_1 = require("../utils");
5
- const test_1 = require("@playwright/test");
6
- (0, fixtures_1.test)("RS-05 Touch targets (mobile) are at least 44x44px and do not overlap", async ({ page, targetPage }) => {
7
- await page.goto(targetPage);
8
- const undersizedTargets = await (0, utils_1.findUndersizedTouchTargets)(page, utils_1.TouchTargetSizeSelector);
9
- (0, test_1.expect)(undersizedTargets, `Touch targets below ${String(utils_1.ResponsiveConfig.MinTouchTargetSizePX)}x${String(utils_1.ResponsiveConfig.MinTouchTargetSizePX)}px:\n${undersizedTargets.join("\n")}`).toEqual([]);
10
- const overlappingTargets = await (0, utils_1.findOverlappingElements)(page, utils_1.TappableElementsSelector);
11
- (0, test_1.expect)(overlappingTargets, `Overlapping tap targets risk triggering the wrong element:\n${overlappingTargets.join("\n")}`).toEqual([]);
12
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,199 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../fixtures");
4
- const test_1 = require("@playwright/test");
5
- fixtures_1.test.describe("Landing Page - CTAs, Links & Navigation", () => {
6
- (0, fixtures_1.test)("CT-01: All CTAs functional", async ({ page, targetPage }) => {
7
- await page.goto(targetPage);
8
- // Look for elements with button role or common CTA link names using smart locators
9
- const ctas = page
10
- .getByRole("button", { name: /submit|send|join|subscribe|sign|start|contact|book|request|try/i })
11
- .or(page.getByRole("link", { name: /submit|send|join|subscribe|sign|start|contact|book|request|try/i }));
12
- const ctaCount = await ctas.count();
13
- (0, test_1.expect)(ctaCount, "Page should have at least some CTAs").toBeGreaterThan(0);
14
- for (let index = 0; index < ctaCount; index++) {
15
- const cta = ctas.nth(index);
16
- const tagName = await cta.evaluate((element) => {
17
- return element.tagName.toLowerCase();
18
- });
19
- switch (tagName) {
20
- case "a": {
21
- const href = await cta.getAttribute("href");
22
- (0, test_1.expect)(typeof href === "string", "CTA link must have an href").toBe(true);
23
- if (typeof href === "string") {
24
- (0, test_1.expect)(href.length > 0, "CTA href must not be empty").toBe(true);
25
- }
26
- const target = await cta.getAttribute("target");
27
- if (target !== null) {
28
- (0, test_1.expect)(target, "CTA target attribute should be _blank if present").toBe("_blank");
29
- }
30
- break;
31
- }
32
- case "button": {
33
- await (0, test_1.expect)(cta, "CTA button should be visible").toBeVisible();
34
- break;
35
- }
36
- }
37
- }
38
- });
39
- (0, fixtures_1.test)("CT-02: All links valid", async ({ page, request, targetPage }) => {
40
- await page.goto(targetPage);
41
- const links = page.getByRole("link");
42
- const linkCount = await links.count();
43
- (0, test_1.expect)(linkCount, "Page should have at least some links").toBeGreaterThan(0);
44
- let linkURLs = [];
45
- for (let index = 0; index < linkCount; index++) {
46
- const href = await links.nth(index).getAttribute("href");
47
- if (typeof href === "string") {
48
- // We only check internal links to avoid flaky tests due to external anti-bot protections
49
- if (href.startsWith("/")) {
50
- const isIncluded = linkURLs.includes(href);
51
- if (!isIncluded) {
52
- linkURLs = [...linkURLs, href];
53
- }
54
- }
55
- }
56
- }
57
- for (const linkURL of linkURLs) {
58
- const requestURL = new URL(linkURL, page.url()).toString();
59
- const response = await request.get(requestURL);
60
- const isResponseOK = response.ok();
61
- (0, test_1.expect)(isResponseOK, `Link ${linkURL} returned status ${response.status().toString()}`).toBe(true);
62
- }
63
- });
64
- (0, fixtures_1.test)("CT-03: Anchor scroll on mobile", async ({ page, targetPage }) => {
65
- await page.goto(targetPage);
66
- const links = page.getByRole("link");
67
- const count = await links.count();
68
- if (count === 0) {
69
- fixtures_1.test.skip(true, "No links found on the page");
70
- }
71
- let hasClicked = false;
72
- for (let index = 0; index < count; index++) {
73
- const href = await links.nth(index).getAttribute("href");
74
- if (typeof href === "string" && href.startsWith("#") && href.length > 1) {
75
- const targetID = href.substring(1);
76
- const targetElement = page.locator(`id=${targetID}`);
77
- const targetCount = await targetElement.count();
78
- if (targetCount > 0) {
79
- await links.nth(index).click();
80
- await (0, test_1.expect)(targetElement).toBeInViewport();
81
- hasClicked = true;
82
- break;
83
- }
84
- }
85
- }
86
- if (!hasClicked) {
87
- fixtures_1.test.skip(true, "No valid anchor links with existing targets found on the page");
88
- }
89
- });
90
- (0, fixtures_1.test)("CT-04: Phone/email links", async ({ page, targetPage }) => {
91
- await page.goto(targetPage);
92
- const links = page.getByRole("link");
93
- const count = await links.count();
94
- if (count === 0) {
95
- fixtures_1.test.skip(true, "No links found on the page to check for phone/email");
96
- }
97
- for (let index = 0; index < count; index++) {
98
- const href = await links.nth(index).getAttribute("href");
99
- if (typeof href === "string") {
100
- if (href.startsWith("tel:")) {
101
- const phoneRegex = /^tel:[+\d\-\s()]+$/;
102
- const isValid = phoneRegex.test(href);
103
- (0, test_1.expect)(isValid, `Phone link format should be valid: ${href}`).toBe(true);
104
- }
105
- else if (href.startsWith("mailto:")) {
106
- // Simple email validation for mailto format
107
- const emailRegex = /^mailto:[^@\s]+@[^@\s]+\.[^@\s]+.*$/;
108
- const isValid = emailRegex.test(href);
109
- (0, test_1.expect)(isValid, `Email link format should be valid: ${href}`).toBe(true);
110
- }
111
- }
112
- }
113
- });
114
- (0, fixtures_1.test)("CT-06: Hover vs touch states", async ({ page, isMobile, targetPage }) => {
115
- await page.goto(targetPage);
116
- const interactables = page.getByRole("button").or(page.getByRole("link"));
117
- const count = await interactables.count();
118
- if (count === 0) {
119
- fixtures_1.test.skip(true, "No buttons or links found to test interaction states");
120
- }
121
- const interactablesAll = await interactables.all();
122
- await Promise.all(interactablesAll.map(async (element, index) => {
123
- const isVisible = await element.isVisible();
124
- if (isVisible) {
125
- // Prevent navigation or unwanted side effects during interaction testing
126
- await element.evaluate((node) => {
127
- node.addEventListener("click", (event) => {
128
- event.preventDefault();
129
- event.stopPropagation();
130
- }, { once: true });
131
- });
132
- if (!isMobile) {
133
- const initialStyle = await element.evaluate((node) => {
134
- const style = window.getComputedStyle(node);
135
- return {
136
- bg: style.backgroundColor,
137
- color: style.color,
138
- opacity: style.opacity,
139
- border: style.borderColor,
140
- shadow: style.boxShadow,
141
- transform: style.transform,
142
- };
143
- });
144
- await element.hover();
145
- const hoverStyle = await element.evaluate((node) => {
146
- const style = window.getComputedStyle(node);
147
- return {
148
- bg: style.backgroundColor,
149
- color: style.color,
150
- opacity: style.opacity,
151
- border: style.borderColor,
152
- shadow: style.boxShadow,
153
- transform: style.transform,
154
- };
155
- });
156
- const isStyleChanged = initialStyle.bg !== hoverStyle.bg ||
157
- initialStyle.color !== hoverStyle.color ||
158
- initialStyle.opacity !== hoverStyle.opacity ||
159
- initialStyle.border !== hoverStyle.border ||
160
- initialStyle.shadow !== hoverStyle.shadow ||
161
- initialStyle.transform !== hoverStyle.transform;
162
- (0, test_1.expect)(isStyleChanged, `Hovering should change styling for element at index ${index.toString()}`).toBe(true);
163
- }
164
- else {
165
- const initialStyle = await element.evaluate((node) => {
166
- const style = window.getComputedStyle(node);
167
- return {
168
- bg: style.backgroundColor,
169
- color: style.color,
170
- opacity: style.opacity,
171
- border: style.borderColor,
172
- shadow: style.boxShadow,
173
- transform: style.transform,
174
- };
175
- });
176
- await element.tap();
177
- const finalStyle = await element.evaluate((node) => {
178
- const style = window.getComputedStyle(node);
179
- return {
180
- bg: style.backgroundColor,
181
- color: style.color,
182
- opacity: style.opacity,
183
- border: style.borderColor,
184
- shadow: style.boxShadow,
185
- transform: style.transform,
186
- };
187
- });
188
- const isStyleChanged = initialStyle.bg !== finalStyle.bg ||
189
- initialStyle.color !== finalStyle.color ||
190
- initialStyle.opacity !== finalStyle.opacity ||
191
- initialStyle.border !== finalStyle.border ||
192
- initialStyle.shadow !== finalStyle.shadow ||
193
- initialStyle.transform !== finalStyle.transform;
194
- (0, test_1.expect)(isStyleChanged, `Element at index ${index.toString()} should change styling after tap on mobile`).toBe(true);
195
- }
196
- }
197
- }));
198
- });
199
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,28 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../../fixtures");
4
- const utils_1 = require("../../utils");
5
- const test_1 = require("@playwright/test");
6
- fixtures_1.test.describe("Forms - Autofill", () => {
7
- (0, fixtures_1.test)("FM-06 Form fields expose autocomplete hints for autofill compatibility", async ({ page, targetPage }) => {
8
- await page.goto(targetPage);
9
- const form = page.locator("form");
10
- if ((await form.count()) === 0) {
11
- fixtures_1.test.skip(true, "No form present on this page");
12
- }
13
- await form.scrollIntoViewIfNeeded();
14
- const fieldEntries = await Promise.all(utils_1.AutofillFields.map(async (autofillField) => {
15
- const field = form.getByLabel(autofillField.label);
16
- const isVisible = (await field.count()) > 0 && (await field.isVisible());
17
- return { autofillField, field, isVisible };
18
- }));
19
- const visibleFieldEntries = fieldEntries.filter((entry) => {
20
- return entry.isVisible;
21
- });
22
- for (const { autofillField, field } of visibleFieldEntries) {
23
- const autocompleteValue = await field.getAttribute("autocomplete");
24
- const hasCorrectAutocomplete = autofillField.expectedAutocomplete.test(autocompleteValue ?? "");
25
- (0, test_1.expect)(hasCorrectAutocomplete, `${autofillField.label.source} field should have an autocomplete attribute matching ${autofillField.expectedAutocomplete.source} so browser autofill puts the value in the correct field`).toBe(true);
26
- }
27
- });
28
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,25 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const fixtures_1 = require("../../../fixtures");
4
- const utils_1 = require("../../utils");
5
- const test_1 = require("@playwright/test");
6
- fixtures_1.test.describe("Forms - Double Submit", () => {
7
- (0, fixtures_1.test)("FM-04 Double-submit protection allows only one submission", async ({ page, targetPage }) => {
8
- await page.goto(targetPage);
9
- const submitButton = page.locator('button[type="submit"], input[type="submit"]');
10
- const submitButtonCount = await submitButton.count();
11
- if (submitButtonCount === 0) {
12
- fixtures_1.test.skip(true, "No submit button present on this page");
13
- }
14
- await (0, utils_1.fillFormFields)(page);
15
- const targetSubmitButton = submitButton.first();
16
- const firstClick = targetSubmitButton.click();
17
- await targetSubmitButton.click({ force: true });
18
- await firstClick;
19
- const isEnabled = !(await targetSubmitButton.isDisabled());
20
- const ariaBusy = await targetSubmitButton.getAttribute("aria-busy");
21
- const isBusy = ariaBusy === "true";
22
- const hasDoubleSubmitProtection = !isEnabled || isBusy;
23
- (0, test_1.expect)(hasDoubleSubmitProtection, "Submit button should visibly disable or show a loading state after the first click").toBe(true);
24
- });
25
- });
@@ -1 +0,0 @@
1
- export {};