@manufac/general-tests 0.0.1-alpha → 0.0.2
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/dist/specs/desktop-only/browser-zoom.spec.js +13 -5
- package/dist/specs/mobile-only/ios-safari-toolbar.spec.d.ts +1 -0
- package/dist/specs/mobile-only/ios-safari-toolbar.spec.js +62 -0
- package/dist/specs/mobile-only/ios-safe-areas.spec.d.ts +1 -0
- package/dist/specs/mobile-only/ios-safe-areas.spec.js +36 -0
- package/dist/specs/mobile-only/navigation.spec.js +17 -7
- package/dist/specs/mobile-only/orientation.spec.js +9 -6
- package/dist/specs/mobile-only/soft-keyboard.spec.js +11 -2
- package/dist/specs/mobile-only/touch-targets.spec.js +1 -1
- package/dist/specs/shared/forms/autofill.spec.js +15 -3
- package/dist/specs/shared/forms/double-submit.spec.js +1 -15
- package/dist/specs/shared/forms/input-edge.spec.js +15 -5
- package/dist/specs/shared/forms/invalid-input.spec.js +30 -11
- package/dist/specs/shared/forms/native-inputs.spec.d.ts +1 -0
- package/dist/specs/shared/forms/native-inputs.spec.js +37 -0
- package/dist/specs/shared/forms/required-field.spec.js +7 -2
- package/dist/specs/shared/forms/thankyou-confirm.spec.js +3 -4
- package/dist/specs/shared/hero-fold.spec.js +24 -5
- package/dist/specs/shared/landing-page.spec.js +69 -0
- package/dist/specs/shared/media-embeds/third-party-embeds.spec.js +5 -2
- package/dist/specs/sticky-utils.d.ts +0 -5
- package/dist/specs/sticky-utils.js +49 -32
- package/dist/specs/utils.d.ts +55 -15
- package/dist/specs/utils.js +357 -63
- package/package.json +9 -1
|
@@ -4,15 +4,23 @@ const fixtures_1 = require("../../fixtures");
|
|
|
4
4
|
const utils_1 = require("../utils");
|
|
5
5
|
const test_1 = require("@playwright/test");
|
|
6
6
|
(0, fixtures_1.test)("RS-07 Browser zoom (desktop) at 200%", async ({ page, targetPage }) => {
|
|
7
|
-
|
|
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
|
+
});
|
|
8
15
|
await page.goto(targetPage);
|
|
9
|
-
await page.evaluate((zoom) => {
|
|
10
|
-
document.documentElement.style.zoom = zoom;
|
|
11
|
-
}, utils_1.ResponsiveConfig.ZoomLevel);
|
|
12
16
|
await page.waitForTimeout(utils_1.ResponsiveConfig.ZoomSettleDelayMs);
|
|
13
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);
|
|
14
22
|
const brokenText = await (0, utils_1.findTruncatedOrOverflowingText)(page);
|
|
15
23
|
(0, test_1.expect)(brokenText, `Text overlapping/clipped at 200% zoom:\n${brokenText.join("\n")}`).toEqual([]);
|
|
16
|
-
const overlappingTargets = await (0, utils_1.
|
|
24
|
+
const overlappingTargets = await (0, utils_1.findOverlappingElements)(page, utils_1.TappableElementsSelector);
|
|
17
25
|
(0, test_1.expect)(overlappingTargets, `Interactive elements unreachable (overlapping) at 200% zoom:\n${overlappingTargets.join("\n")}`).toEqual([]);
|
|
18
26
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
});
|
|
@@ -6,10 +6,9 @@ fixtures_1.test.describe("Mobile Navigation", () => {
|
|
|
6
6
|
(0, fixtures_1.test)("CT-05: Hamburger/mobile nav", async ({ page, targetPage }) => {
|
|
7
7
|
await page.goto(targetPage);
|
|
8
8
|
const hamburger = page
|
|
9
|
-
.getByRole("button", { name: /menu|navigation/i })
|
|
10
|
-
.or(page.getByRole("button", { expanded: false }))
|
|
11
|
-
.or(page.getByRole("button", { expanded: true }))
|
|
9
|
+
.getByRole("button", { name: /menu|navigation|hamburger/i })
|
|
12
10
|
.or(page.locator(".hamburger, .menu-toggle"))
|
|
11
|
+
.or(page.getByRole("banner").getByRole("button"))
|
|
13
12
|
.first();
|
|
14
13
|
const count = await hamburger.count();
|
|
15
14
|
if (count === 0) {
|
|
@@ -21,10 +20,21 @@ fixtures_1.test.describe("Mobile Navigation", () => {
|
|
|
21
20
|
}
|
|
22
21
|
await hamburger.click();
|
|
23
22
|
await page.waitForTimeout(500); // Wait for transition
|
|
24
|
-
const
|
|
25
|
-
|
|
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;
|
|
26
37
|
});
|
|
27
|
-
|
|
28
|
-
(0, test_1.expect)(isHidden, "Page behind the open mobile menu should not be scrollable").toBe(true);
|
|
38
|
+
(0, test_1.expect)(isScrollLocked, "Page behind the open mobile menu should not be scrollable").toBe(true);
|
|
29
39
|
});
|
|
30
40
|
});
|
|
@@ -11,30 +11,33 @@ const test_1 = require("@playwright/test");
|
|
|
11
11
|
}
|
|
12
12
|
const { width, height } = portraitViewport;
|
|
13
13
|
const { scrollHeight, clientHeight } = await (0, utils_1.getScrollMetrics)(page);
|
|
14
|
-
// A page only truly scrolls when its content is taller than the viewport, not merely > 0px.
|
|
15
14
|
const isScrollable = scrollHeight > clientHeight;
|
|
16
15
|
const scrollTarget = isScrollable ? Math.floor(scrollHeight / 2) : 0;
|
|
17
|
-
const scrollRatioBefore = isScrollable ? scrollTarget / scrollHeight : 0;
|
|
18
16
|
if (isScrollable) {
|
|
19
|
-
// window.scrollTo guarantees landing at the target, unlike a wheel-delta scroll gesture.
|
|
20
17
|
await page.evaluate((y) => {
|
|
21
18
|
window.scrollTo(0, y);
|
|
22
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 });
|
|
23
25
|
}
|
|
26
|
+
const baselineMetrics = await (0, utils_1.getScrollMetrics)(page);
|
|
27
|
+
const scrollRatioBefore = isScrollable ? baselineMetrics.scrollY / baselineMetrics.scrollHeight : 0;
|
|
24
28
|
const phases = [
|
|
25
29
|
{ name: "landscape", viewport: { width: height, height: width } },
|
|
26
30
|
{ name: "portrait", viewport: { width, height } },
|
|
27
31
|
];
|
|
28
32
|
for (const phase of phases) {
|
|
29
33
|
await page.setViewportSize(phase.viewport);
|
|
30
|
-
//
|
|
34
|
+
// Allow any smooth-scroll/reflow triggered by the viewport change to settle.
|
|
35
|
+
await page.waitForTimeout(300);
|
|
31
36
|
const cutoffElements = await (0, utils_1.findElementsCutoffAtViewportEdge)(page);
|
|
32
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([]);
|
|
33
|
-
// ...and no two elements overlapping each other.
|
|
34
38
|
const overlappingElements = await (0, utils_1.findOverlappingElements)(page, utils_1.ContentOverlapSelector);
|
|
35
39
|
(0, test_1.expect)(overlappingElements, `Found overlapping element(s) after rotating to ${phase.name}:\n${overlappingElements.join("\n")}`).toEqual([]);
|
|
36
40
|
if (isScrollable) {
|
|
37
|
-
// Scroll ratio (not absolute position) should stay stable since page height changes with orientation.
|
|
38
41
|
const currentMetrics = await (0, utils_1.getScrollMetrics)(page);
|
|
39
42
|
const scrollRatioAfter = currentMetrics.scrollY / currentMetrics.scrollHeight;
|
|
40
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);
|
|
@@ -7,11 +7,15 @@ fixtures_1.test.describe("Forms - Soft Keyboard", () => {
|
|
|
7
7
|
utils_1.KeyboardFields.forEach((keyboardField) => {
|
|
8
8
|
(0, fixtures_1.test)(`FM-05 ${keyboardField.label.source} field opens the correct soft keyboard`, async ({ page, targetPage }) => {
|
|
9
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
|
+
}
|
|
10
15
|
const viewport = page.viewportSize();
|
|
11
16
|
if (viewport === null) {
|
|
12
17
|
throw new Error("Viewport size must be set before running the soft keyboard test");
|
|
13
18
|
}
|
|
14
|
-
const field = page.getByLabel(keyboardField.label);
|
|
15
19
|
await field.click();
|
|
16
20
|
// NOTE: Playwright cannot render a real device's on-screen keyboard, so the
|
|
17
21
|
// originally-specified check ("email field opens the @ keyboard, phone field
|
|
@@ -20,10 +24,15 @@ fixtures_1.test.describe("Forms - Soft Keyboard", () => {
|
|
|
20
24
|
const [inputMode, inputType] = await Promise.all([field.getAttribute("inputmode"), field.getAttribute("type")]);
|
|
21
25
|
const hasCorrectKeyboardHint = keyboardField.expectedInputMode.test(inputMode ?? "") || keyboardField.expectedInputMode.test(inputType ?? "");
|
|
22
26
|
(0, test_1.expect)(hasCorrectKeyboardHint, `${keyboardField.label.source} field should have an inputmode/type that opens the correct soft keyboard`).toBe(true);
|
|
23
|
-
// A real on-screen keyboard typically covers roughly the bottom 40% of a mobile viewport.
|
|
27
|
+
// Note: A real on-screen keyboard typically covers roughly the bottom 40% of a mobile viewport.
|
|
24
28
|
// Playwright can't render an actual OS keyboard, so this is approximated by shrinking the
|
|
25
29
|
// viewport after focusing the field, turning "field shouldn't be hidden behind the keyboard"
|
|
26
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.
|
|
27
36
|
const visibleHeight = Math.round(viewport.height * (1 - utils_1.ResponsiveConfig.KeyboardOcclusionRatio));
|
|
28
37
|
await page.setViewportSize({ width: viewport.width, height: visibleHeight });
|
|
29
38
|
const fieldBox = await field.boundingBox();
|
|
@@ -7,6 +7,6 @@ const test_1 = require("@playwright/test");
|
|
|
7
7
|
await page.goto(targetPage);
|
|
8
8
|
const undersizedTargets = await (0, utils_1.findUndersizedTouchTargets)(page, utils_1.TouchTargetSizeSelector);
|
|
9
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.
|
|
10
|
+
const overlappingTargets = await (0, utils_1.findOverlappingElements)(page, utils_1.TappableElementsSelector);
|
|
11
11
|
(0, test_1.expect)(overlappingTargets, `Overlapping tap targets risk triggering the wrong element:\n${overlappingTargets.join("\n")}`).toEqual([]);
|
|
12
12
|
});
|
|
@@ -6,11 +6,23 @@ const test_1 = require("@playwright/test");
|
|
|
6
6
|
fixtures_1.test.describe("Forms - Autofill", () => {
|
|
7
7
|
(0, fixtures_1.test)("FM-06 Form fields expose autocomplete hints for autofill compatibility", async ({ page, targetPage }) => {
|
|
8
8
|
await page.goto(targetPage);
|
|
9
|
-
|
|
10
|
-
|
|
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) {
|
|
11
23
|
const autocompleteValue = await field.getAttribute("autocomplete");
|
|
12
24
|
const hasCorrectAutocomplete = autofillField.expectedAutocomplete.test(autocompleteValue ?? "");
|
|
13
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);
|
|
14
|
-
}
|
|
26
|
+
}
|
|
15
27
|
});
|
|
16
28
|
});
|
|
@@ -11,21 +11,7 @@ fixtures_1.test.describe("Forms - Double Submit", () => {
|
|
|
11
11
|
if (submitButtonCount === 0) {
|
|
12
12
|
fixtures_1.test.skip(true, "No submit button present on this page");
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
const nameFieldCount = await nameField.count();
|
|
16
|
-
if (nameFieldCount > 0) {
|
|
17
|
-
await nameField.first().fill(utils_1.ValidUserInfo.ValidTestName);
|
|
18
|
-
}
|
|
19
|
-
const emailField = page.getByLabel(/email/i);
|
|
20
|
-
const emailFieldCount = await emailField.count();
|
|
21
|
-
if (emailFieldCount > 0) {
|
|
22
|
-
await emailField.first().fill(utils_1.ValidUserInfo.ValidTestEmail);
|
|
23
|
-
}
|
|
24
|
-
const phoneField = page.getByLabel(/phone/i);
|
|
25
|
-
const phoneFieldCount = await phoneField.count();
|
|
26
|
-
if (phoneFieldCount > 0) {
|
|
27
|
-
await phoneField.first().fill(utils_1.ValidUserInfo.ValidTestPhone);
|
|
28
|
-
}
|
|
14
|
+
await (0, utils_1.fillFormFields)(page);
|
|
29
15
|
const targetSubmitButton = submitButton.first();
|
|
30
16
|
const firstClick = targetSubmitButton.click();
|
|
31
17
|
await targetSubmitButton.click({ force: true });
|
|
@@ -6,9 +6,14 @@ const test_1 = require("@playwright/test");
|
|
|
6
6
|
fixtures_1.test.describe("Forms - Input Edge Cases", () => {
|
|
7
7
|
(0, fixtures_1.test)("FM-08 Name field accepts a 200+ character string without breaking layout", async ({ page, targetPage }) => {
|
|
8
8
|
await page.goto(targetPage);
|
|
9
|
-
|
|
9
|
+
const nameField = page.getByLabel(/name/i);
|
|
10
|
+
const nameFieldCount = await nameField.count();
|
|
11
|
+
if (nameFieldCount === 0) {
|
|
12
|
+
fixtures_1.test.skip(true, "No Name field present on this page");
|
|
13
|
+
}
|
|
14
|
+
await (0, utils_1.fillFormFields)(page);
|
|
10
15
|
const longName = "A".repeat(utils_1.ValidUserInfo.LongNameCharacterCount);
|
|
11
|
-
await
|
|
16
|
+
await nameField.first().fill(longName);
|
|
12
17
|
const isOverflowingBeforeSubmit = await (0, utils_1.hasHorizontalScrollbar)(page);
|
|
13
18
|
(0, test_1.expect)(isOverflowingBeforeSubmit, "Page should not have overflowing/broken layout after a very long name is entered").toBe(false);
|
|
14
19
|
const submitButton = page.getByRole("button", { name: /submit/i });
|
|
@@ -21,8 +26,13 @@ fixtures_1.test.describe("Forms - Input Edge Cases", () => {
|
|
|
21
26
|
});
|
|
22
27
|
(0, fixtures_1.test)("FM-08 Name field with emoji is accepted or cleanly rejected", async ({ page, targetPage, consoleErrors }) => {
|
|
23
28
|
await page.goto(targetPage);
|
|
24
|
-
|
|
25
|
-
const
|
|
29
|
+
const nameField = page.getByLabel(/name/i);
|
|
30
|
+
const nameFieldCount = await nameField.count();
|
|
31
|
+
if (nameFieldCount === 0) {
|
|
32
|
+
fixtures_1.test.skip(true, "No Name field present on this page");
|
|
33
|
+
}
|
|
34
|
+
await nameField.first().fill(utils_1.ValidUserInfo.EmojiTestName);
|
|
35
|
+
const nameValue = await nameField.first().inputValue();
|
|
26
36
|
const isEmojiAccepted = nameValue === utils_1.ValidUserInfo.EmojiTestName;
|
|
27
37
|
// Indicates the emoji character was not preserved in the field value (stripped or cleared by the input).
|
|
28
38
|
const isEmojiRejected = nameValue === "" || !nameValue.includes("😀");
|
|
@@ -31,7 +41,7 @@ fixtures_1.test.describe("Forms - Input Edge Cases", () => {
|
|
|
31
41
|
});
|
|
32
42
|
(0, fixtures_1.test)("FM-08 Email with leading/trailing spaces is trimmed and submits successfully", async ({ page, targetPage }) => {
|
|
33
43
|
await page.goto(targetPage);
|
|
34
|
-
await (0, utils_1.
|
|
44
|
+
await (0, utils_1.fillFormFields)(page);
|
|
35
45
|
const emailField = page.getByLabel(/email/i);
|
|
36
46
|
await emailField.fill(utils_1.ValidUserInfo.PaddedTestEmail);
|
|
37
47
|
await emailField.blur();
|
|
@@ -3,22 +3,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const fixtures_1 = require("../../../fixtures");
|
|
4
4
|
const utils_1 = require("../../utils");
|
|
5
5
|
const test_1 = require("@playwright/test");
|
|
6
|
-
// Ref: https://playwright.dev/docs/test-parameterize#parameterized-tests
|
|
7
6
|
fixtures_1.test.describe("Forms - Invalid Input", () => {
|
|
8
7
|
utils_1.InvalidInputCases.forEach((invalidCase) => {
|
|
9
|
-
(0, fixtures_1.test)(`FM-03 Invalid input "${invalidCase.invalidValue}" in ${invalidCase.fieldLabel.
|
|
8
|
+
(0, fixtures_1.test)(`FM-03 Invalid input "${invalidCase.invalidValue}" in ${invalidCase.fieldLabel.toString()} field is rejected (if present)`, async ({ page, targetPage, }) => {
|
|
10
9
|
await page.goto(targetPage);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
fixtures_1.test.skip(true, `No ${invalidCase.fieldLabel.source} field present on this page`);
|
|
10
|
+
const form = page.locator("form");
|
|
11
|
+
if ((await form.count()) === 0) {
|
|
12
|
+
fixtures_1.test.skip(true, "No form present on this page");
|
|
15
13
|
}
|
|
14
|
+
await form.first().scrollIntoViewIfNeeded();
|
|
15
|
+
const fieldLocator = form.getByLabel(invalidCase.fieldLabel);
|
|
16
|
+
if ((await fieldLocator.count()) === 0 || !(await fieldLocator.isVisible())) {
|
|
17
|
+
fixtures_1.test.skip(true, `${invalidCase.fieldLabel.toString()} field is not present on this form`);
|
|
18
|
+
}
|
|
19
|
+
// Fill valid data in all other required fields
|
|
16
20
|
await (0, utils_1.fillOtherRequiredFieldsValidly)(page, invalidCase.fieldLabel);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// Fill invalid value in the field under test
|
|
22
|
+
await fieldLocator.first().fill(invalidCase.invalidValue);
|
|
23
|
+
// Attempt submit
|
|
24
|
+
const submitButton = form.getByRole("button", {
|
|
25
|
+
name: /(schedule discovery call|submit|send|book|request|get in touch)/i,
|
|
26
|
+
});
|
|
27
|
+
await submitButton.click();
|
|
28
|
+
// Assert rejection - confirmation should NOT appear
|
|
29
|
+
const confirmation = page.getByText(/thank you|success|confirmation/i);
|
|
30
|
+
await (0, test_1.expect)(confirmation).not.toBeVisible({ timeout: 5000 });
|
|
31
|
+
// Assert validation message or invalid state
|
|
32
|
+
await (0, test_1.expect)(async () => {
|
|
33
|
+
const validationError = page.getByText(invalidCase.expectedErrorText);
|
|
34
|
+
const isErrorVisible = await validationError.isVisible();
|
|
35
|
+
const isAriaValid = (await fieldLocator.first().getAttribute("aria-invalid")) !== "true";
|
|
36
|
+
const isNativelyValid = await fieldLocator.first().evaluate((node) => {
|
|
37
|
+
return node.checkValidity();
|
|
38
|
+
});
|
|
39
|
+
(0, test_1.expect)(isErrorVisible || !isAriaValid || !isNativelyValid).toBeTruthy();
|
|
40
|
+
}).toPass({ timeout: 5000 });
|
|
22
41
|
});
|
|
23
42
|
});
|
|
24
43
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
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("Forms - Native Inputs", () => {
|
|
6
|
+
(0, fixtures_1.test)("XB-04 Date and number inputs reject invalid values", async ({ page, targetPage }) => {
|
|
7
|
+
await page.goto(targetPage);
|
|
8
|
+
// Locate all date and number inputs
|
|
9
|
+
const nativeInputs = page.locator('input[type="date"], input[type="number"]');
|
|
10
|
+
const inputCount = await nativeInputs.count();
|
|
11
|
+
if (inputCount === 0) {
|
|
12
|
+
fixtures_1.test.skip(true, "No date or number inputs found on the page");
|
|
13
|
+
}
|
|
14
|
+
const allInputs = await nativeInputs.all();
|
|
15
|
+
const invalidValues = {
|
|
16
|
+
number: "notanumber",
|
|
17
|
+
date: "02302026",
|
|
18
|
+
};
|
|
19
|
+
for (const inputLocator of allInputs) {
|
|
20
|
+
const inputType = await inputLocator.getAttribute("type");
|
|
21
|
+
const invalidValue = inputType !== null ? (invalidValues[inputType] ?? "") : "";
|
|
22
|
+
// We use click and keyboard.type to bypass Playwright's strict fill checks for invalid types
|
|
23
|
+
// (Playwright's fill() throws if you try to put letters in a number field).
|
|
24
|
+
// This simulates a user typing invalid data to see if the browser's native UI rejects it.
|
|
25
|
+
await inputLocator.click();
|
|
26
|
+
await page.keyboard.type(invalidValue);
|
|
27
|
+
const isRejectedByBrowser = await inputLocator.evaluate((element) => {
|
|
28
|
+
// If the browser natively rejects the input because of its type constraint,
|
|
29
|
+
// the value property will often be an empty string, or validity.badInput will be true.
|
|
30
|
+
const isBadInput = element.validity.badInput;
|
|
31
|
+
const isValueRejected = element.value === "";
|
|
32
|
+
return isBadInput || isValueRejected;
|
|
33
|
+
});
|
|
34
|
+
(0, test_1.expect)(isRejectedByBrowser, `Input of type "${inputType ?? "unknown"}" should reject invalid value "${invalidValue}"`).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -5,7 +5,7 @@ const utils_1 = require("../../utils");
|
|
|
5
5
|
const test_1 = require("@playwright/test");
|
|
6
6
|
// Ref: https://playwright.dev/docs/test-parameterize#parameterized-tests
|
|
7
7
|
fixtures_1.test.describe("Forms", () => {
|
|
8
|
-
utils_1.
|
|
8
|
+
utils_1.FieldValueMappings.forEach((targetField) => {
|
|
9
9
|
(0, fixtures_1.test)(`FM-02 Required field validation blocks empty ${targetField.label.source}`, async ({ page, targetPage, }) => {
|
|
10
10
|
await page.goto(targetPage);
|
|
11
11
|
const fieldLocator = page.getByLabel(targetField.label);
|
|
@@ -13,7 +13,12 @@ fixtures_1.test.describe("Forms", () => {
|
|
|
13
13
|
if (fieldCount === 0) {
|
|
14
14
|
fixtures_1.test.skip(true, `No ${targetField.label.source} field present on this page`);
|
|
15
15
|
}
|
|
16
|
-
await (0, utils_1.
|
|
16
|
+
const isRequired = await (0, utils_1.isFieldRequired)(fieldLocator.first());
|
|
17
|
+
const isEmailField = targetField.label.source === "email";
|
|
18
|
+
if (!isRequired && !isEmailField) {
|
|
19
|
+
fixtures_1.test.skip(true, `${targetField.label.source} field is not marked required on this page`);
|
|
20
|
+
}
|
|
21
|
+
await (0, utils_1.fillFormFields)(page);
|
|
17
22
|
await fieldLocator.first().clear();
|
|
18
23
|
const submitButtons = page.locator('button[type="submit"], input[type="submit"]');
|
|
19
24
|
await submitButtons.first().click();
|
|
@@ -6,12 +6,10 @@ const test_1 = require("@playwright/test");
|
|
|
6
6
|
fixtures_1.test.describe("Forms - Thank You Confirm", () => {
|
|
7
7
|
(0, fixtures_1.test)("FM-07 Confirmation state matches spec and does not resubmit on Back/Forward", async ({ page, targetPage }) => {
|
|
8
8
|
await page.goto(targetPage);
|
|
9
|
-
await
|
|
10
|
-
await page.getByLabel(/email/i).fill(utils_1.ValidUserInfo.ValidTestEmail);
|
|
11
|
-
await page.getByLabel(/phone/i).fill(utils_1.ValidUserInfo.ValidTestPhone);
|
|
9
|
+
await (0, utils_1.fillFormFields)(page);
|
|
12
10
|
await page.getByRole("button", { name: /submit/i }).click();
|
|
13
11
|
const confirmation = page.getByText(/thank you|success|confirmation/i);
|
|
14
|
-
await (0, test_1.expect)(confirmation, "Confirmation message/page did not appear or did not match the design spec").toBeVisible();
|
|
12
|
+
await (0, test_1.expect)(confirmation, "Confirmation message/page did not appear or did not match the design spec").toBeVisible({ timeout: 10_000 });
|
|
15
13
|
let hasResubmitDialog = false;
|
|
16
14
|
page.on("dialog", (dialog) => {
|
|
17
15
|
hasResubmitDialog = true;
|
|
@@ -21,5 +19,6 @@ fixtures_1.test.describe("Forms - Thank You Confirm", () => {
|
|
|
21
19
|
(0, test_1.expect)(hasResubmitDialog, 'Browser Back should NOT show a "resubmit form?" dialog').toBe(false);
|
|
22
20
|
await page.goForward();
|
|
23
21
|
await (0, test_1.expect)(confirmation, "Confirmation should still be shown after pressing Forward").toBeVisible();
|
|
22
|
+
(0, test_1.expect)(hasResubmitDialog, 'Browser Forward should NOT show a "resubmit form?" dialog').toBe(false);
|
|
24
23
|
});
|
|
25
24
|
});
|
|
@@ -9,11 +9,30 @@ const test_1 = require("@playwright/test");
|
|
|
9
9
|
await (0, test_1.expect)(headline, "Main headline not fully within the fold").toBeInViewport({
|
|
10
10
|
ratio: 1,
|
|
11
11
|
});
|
|
12
|
-
const ctaRegex = /get started|book a demo|contact us|try|sign up|request|start|buy|explore|join|register|learn|get|access|download|schedule|book/i;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const ctaRegex = /get started|book a demo|contact us|try|sign up|request|start|buy|explore|join|register|learn|get|access|download|schedule|book|discuss/i;
|
|
13
|
+
// Restrict to elements that appear before the first <form> on the page, so the hero's
|
|
14
|
+
// CTA can never accidentally resolve to a submit button belonging to the contact form.
|
|
15
|
+
// boundingBox() auto-waits for a matching element, so it must only be called once a form
|
|
16
|
+
// is confirmed present — otherwise a page with no form at all would hang until timeout.
|
|
17
|
+
const contactForm = page.locator("form").first();
|
|
18
|
+
const hasContactForm = (await contactForm.count()) > 0;
|
|
19
|
+
const formBoundingBox = hasContactForm ? await contactForm.boundingBox() : null;
|
|
20
|
+
const allMatches = page.getByRole("link", { name: ctaRegex }).or(page.getByRole("button", { name: ctaRegex }));
|
|
21
|
+
const matchCount = await allMatches.count();
|
|
22
|
+
let primaryCTA = allMatches.first();
|
|
23
|
+
if (formBoundingBox !== null) {
|
|
24
|
+
let isFound = false;
|
|
25
|
+
for (let index = 0; index < matchCount; index++) {
|
|
26
|
+
if (!isFound) {
|
|
27
|
+
const candidate = allMatches.nth(index);
|
|
28
|
+
const candidateBox = await candidate.boundingBox();
|
|
29
|
+
if (candidateBox !== null && candidateBox.y < formBoundingBox.y) {
|
|
30
|
+
primaryCTA = candidate;
|
|
31
|
+
isFound = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
17
36
|
await (0, test_1.expect)(primaryCTA, "Primary CTA not visible without scrolling").toBeVisible();
|
|
18
37
|
await (0, test_1.expect)(primaryCTA, "Primary CTA not fully within the fold").toBeInViewport({
|
|
19
38
|
ratio: 1,
|
|
@@ -4,6 +4,14 @@ const fixtures_1 = require("../../fixtures");
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
fixtures_1.test.describe("Landing Page Automated Smoke Suite", () => {
|
|
6
6
|
(0, fixtures_1.test)("LP-01: Page returns 200, renders H1, and has no console errors", async ({ page, consoleErrors, targetPage, }) => {
|
|
7
|
+
let failedNetworkRequests = 0;
|
|
8
|
+
page.on("response", (response) => {
|
|
9
|
+
const status = response.status();
|
|
10
|
+
// Track any 4xx or 5xx responses (except for the 404 test itself, but this is LP-01)
|
|
11
|
+
if (status >= 400) {
|
|
12
|
+
failedNetworkRequests += 1;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
7
15
|
const response = await page.goto(targetPage);
|
|
8
16
|
// Page returns 200
|
|
9
17
|
(0, test_1.expect)(response?.ok()).toBe(true);
|
|
@@ -12,6 +20,67 @@ fixtures_1.test.describe("Landing Page Automated Smoke Suite", () => {
|
|
|
12
20
|
await (0, test_1.expect)(h1).toBeVisible();
|
|
13
21
|
// No console errors on load
|
|
14
22
|
(0, test_1.expect)(consoleErrors).toHaveLength(0);
|
|
23
|
+
// No failed asset/network requests
|
|
24
|
+
(0, test_1.expect)(failedNetworkRequests, "Page should load without any failed network requests (404/500)").toBe(0);
|
|
25
|
+
});
|
|
26
|
+
(0, fixtures_1.test)("LP-02: Correct page metadata (title and favicon)", async ({ page, targetPage }) => {
|
|
27
|
+
await page.goto(targetPage);
|
|
28
|
+
// Title should not be empty, a raw URL, or a placeholder
|
|
29
|
+
const title = await page.title();
|
|
30
|
+
const hasTitle = title.trim().length > 0;
|
|
31
|
+
(0, test_1.expect)(hasTitle, "Page title should not be empty").toBe(true);
|
|
32
|
+
(0, test_1.expect)(title).not.toMatch(/^http/i);
|
|
33
|
+
(0, test_1.expect)(title).not.toMatch(/untitled/i);
|
|
34
|
+
// Favicon should be present
|
|
35
|
+
const favicon = page.locator('link[rel="icon"], link[rel="shortcut icon"]');
|
|
36
|
+
await (0, test_1.expect)(favicon.first()).toBeAttached();
|
|
37
|
+
});
|
|
38
|
+
(0, fixtures_1.test)("LP-03: HTTPS enforced", async ({ page, targetPage }) => {
|
|
39
|
+
// We can only reliably test HTTPS redirection if we have an absolute URL pointing to a remote environment.
|
|
40
|
+
// If targetPage is a relative path (like '/'), we skip because local dev servers usually run on HTTP.
|
|
41
|
+
const isRelativePath = targetPage.startsWith("/");
|
|
42
|
+
const isLocalhost = targetPage.includes("localhost");
|
|
43
|
+
const isLocalIP = targetPage.includes("127.0.0.1");
|
|
44
|
+
const isLocalEnvironment = isRelativePath || isLocalhost || isLocalIP;
|
|
45
|
+
if (isLocalEnvironment) {
|
|
46
|
+
fixtures_1.test.skip(true, "HTTPS redirection is only testable on remote staging/production environments");
|
|
47
|
+
}
|
|
48
|
+
let httpURL = targetPage;
|
|
49
|
+
const isHTTPS = targetPage.startsWith("https://");
|
|
50
|
+
const isHTTP = targetPage.startsWith("http://");
|
|
51
|
+
if (isHTTPS) {
|
|
52
|
+
httpURL = targetPage.replace("https://", "http://");
|
|
53
|
+
}
|
|
54
|
+
else if (!isHTTP) {
|
|
55
|
+
// isHTTP is true when the targetPage already explicitly defines the HTTP protocol
|
|
56
|
+
httpURL = `http://${targetPage}`;
|
|
57
|
+
}
|
|
58
|
+
await page.goto(httpURL);
|
|
59
|
+
// Address bar shows https:// (auto-redirected)
|
|
60
|
+
(0, test_1.expect)(page.url()).toMatch(/^https:\/\//);
|
|
61
|
+
});
|
|
62
|
+
(0, fixtures_1.test)("LP-04: 404/error handling", async ({ page, targetPage }) => {
|
|
63
|
+
let notFoundURL = `${targetPage}/thispagedoesnotexist`;
|
|
64
|
+
const hasTrailingSlash = targetPage.endsWith("/");
|
|
65
|
+
if (hasTrailingSlash) {
|
|
66
|
+
notFoundURL = `${targetPage}thispagedoesnotexist`;
|
|
67
|
+
}
|
|
68
|
+
await page.goto(notFoundURL);
|
|
69
|
+
// Check that it's a branded/styled 404 page, NOT a plain white server error page or a stack trace
|
|
70
|
+
const content = await page.content();
|
|
71
|
+
(0, test_1.expect)(content).not.toMatch(/stack trace/i);
|
|
72
|
+
// Look for common 404 indicators
|
|
73
|
+
const errorHeading = page.getByRole("heading").filter({ hasText: /404|not found|does not exist|not available/i });
|
|
74
|
+
const errorText = page.locator("text=/404|not found|does not exist|not available/i");
|
|
75
|
+
const hasErrorHeading = (await errorHeading.count()) > 0;
|
|
76
|
+
const hasErrorText = (await errorText.count()) > 0;
|
|
77
|
+
const isBranded404 = hasErrorHeading || hasErrorText;
|
|
78
|
+
(0, test_1.expect)(isBranded404, "Page should display a branded 404 error message").toBe(true);
|
|
79
|
+
// 404 page should have a link back to the home page
|
|
80
|
+
const homeLink = page.getByRole("link", { name: /home|back|return/i }).or(page.locator('a[href="/"]'));
|
|
81
|
+
const homeLinkCount = await homeLink.count();
|
|
82
|
+
const hasHomeLink = homeLinkCount > 0;
|
|
83
|
+
(0, test_1.expect)(hasHomeLink, "404 page should have a link back to the home page").toBe(true);
|
|
15
84
|
});
|
|
16
85
|
(0, fixtures_1.test)("RS-01: No horizontal overflow (responsive layout stability)", async ({ page, targetPage }) => {
|
|
17
86
|
await page.goto(targetPage);
|