@manufac/general-tests 0.0.1-alpha → 0.0.1
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 +1 -1
|
@@ -11,8 +11,11 @@ fixtures_1.test.describe("Media Embeds - Third Party", () => {
|
|
|
11
11
|
fixtures_1.test.skip(embedCount === 0, "No third-party embeds present on this page");
|
|
12
12
|
const isOverflowing = await (0, utils_1.hasHorizontalScrollbar)(page);
|
|
13
13
|
(0, test_1.expect)(isOverflowing, "Page should not have horizontal overflow with embeds present").toBe(false);
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Note: MD-03 specifies that broken embeds shouldn't leave a "giant empty gap [that] breaks the layout".
|
|
15
|
+
// Detecting "blank because the embed failed to load" vs "legitimately blank/white content" purely
|
|
16
|
+
// from the outer element's geometry isn't reliably possible without provider-specific knowledge.
|
|
17
|
+
// If an embed shrinks to 0x0, that is considered a graceful failure, not a bug.
|
|
18
|
+
// This is a known automation limitation (P2). We rely on the layout/overflow checks above and below.
|
|
16
19
|
const heading = page.getByRole("heading", { level: 1 }).first();
|
|
17
20
|
await (0, test_1.expect)(heading, "Main heading should remain visible with embeds present").toBeVisible();
|
|
18
21
|
for (let embedIndex = 0; embedIndex < embedCount; embedIndex++) {
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type { Page } from "@playwright/test";
|
|
2
|
-
export interface ScrollMetrics {
|
|
3
|
-
scrollHeight: number;
|
|
4
|
-
scrollY: number;
|
|
5
|
-
}
|
|
6
|
-
export declare function getScrollMetrics(page: Page): Promise<ScrollMetrics>;
|
|
7
2
|
export interface StickyIssues {
|
|
8
3
|
stickyElementCount: number;
|
|
9
4
|
flicker: string[];
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getScrollMetrics = getScrollMetrics;
|
|
4
3
|
exports.checkStickyElements = checkStickyElements;
|
|
5
4
|
const utils_1 = require("./utils");
|
|
6
|
-
async function getScrollMetrics(page) {
|
|
7
|
-
return page.evaluate(() => {
|
|
8
|
-
return {
|
|
9
|
-
scrollHeight: document.documentElement.scrollHeight,
|
|
10
|
-
scrollY: window.scrollY,
|
|
11
|
-
};
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
5
|
// Each page.evaluate call runs in a fresh browser-side context, so a DOM element found in one
|
|
15
6
|
// can't be referenced from the next — this attribute gives each sticky a stable ID to re-select
|
|
16
7
|
// by across the many separate evaluate calls this check makes while scrolling.
|
|
@@ -66,24 +57,46 @@ async function sampleStickyPositions(page) {
|
|
|
66
57
|
});
|
|
67
58
|
}, { markerAttribute: StickyMarkerAttribute, contentSelector: ContentSelector });
|
|
68
59
|
}
|
|
69
|
-
async function
|
|
60
|
+
async function waitForScrollToSettle(page) {
|
|
61
|
+
let previousScrollY = await page.evaluate(() => {
|
|
62
|
+
return window.scrollY;
|
|
63
|
+
});
|
|
64
|
+
let isSettled = false;
|
|
65
|
+
let attempt = 0;
|
|
66
|
+
while (!isSettled && attempt < 20) {
|
|
67
|
+
await page.waitForTimeout(100);
|
|
68
|
+
const currentScrollY = await page.evaluate(() => {
|
|
69
|
+
return window.scrollY;
|
|
70
|
+
});
|
|
71
|
+
isSettled = Math.abs(currentScrollY - previousScrollY) < 1;
|
|
72
|
+
previousScrollY = currentScrollY;
|
|
73
|
+
attempt += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function collectStickySamples(page, steps) {
|
|
70
77
|
const samplesByElement = new Map();
|
|
71
78
|
const coversContent = [];
|
|
72
|
-
const
|
|
73
|
-
return
|
|
79
|
+
const maxScrollY = await page.evaluate(() => {
|
|
80
|
+
return document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
|
81
|
+
});
|
|
82
|
+
const down = Array.from({ length: steps + 1 }, (_, stepIndex) => {
|
|
83
|
+
return Math.min(Math.floor((maxScrollY / steps) * stepIndex), maxScrollY);
|
|
74
84
|
});
|
|
75
|
-
const up = Array.from({ length: steps + 1 }, (_,
|
|
76
|
-
return Math.floor((
|
|
85
|
+
const up = Array.from({ length: steps + 1 }, (_, stepIndex) => {
|
|
86
|
+
return Math.min(Math.floor((maxScrollY / steps) * (steps - stepIndex)), maxScrollY);
|
|
77
87
|
});
|
|
78
|
-
for (const
|
|
88
|
+
for (const targetScrollY of [...down, ...up]) {
|
|
79
89
|
await page.evaluate((y) => {
|
|
80
90
|
window.scrollTo(0, y);
|
|
81
|
-
},
|
|
82
|
-
await page
|
|
91
|
+
}, targetScrollY);
|
|
92
|
+
await waitForScrollToSettle(page);
|
|
93
|
+
const actualScrollY = await page.evaluate(() => {
|
|
94
|
+
return window.scrollY;
|
|
95
|
+
});
|
|
83
96
|
const sample = await sampleStickyPositions(page);
|
|
84
97
|
for (const entry of sample) {
|
|
85
98
|
const samples = samplesByElement.get(entry.stickyID) ?? [];
|
|
86
|
-
samples.push({ scrollY, top: entry.top });
|
|
99
|
+
samples.push({ scrollY: actualScrollY, top: entry.top });
|
|
87
100
|
samplesByElement.set(entry.stickyID, samples);
|
|
88
101
|
coversContent.push(...entry.covered);
|
|
89
102
|
}
|
|
@@ -94,12 +107,17 @@ async function collectStickySamples(page, scrollHeight, steps) {
|
|
|
94
107
|
// ways: it's currently pinned, so top stays put (delta ~0); or it's still in normal flow (not yet
|
|
95
108
|
// engaged), so top moves in exact lockstep with scroll (delta ~ -scrollDelta). Anything else is an
|
|
96
109
|
// unexplained jump — the actual flicker/jank this check is meant to catch.
|
|
110
|
+
//
|
|
111
|
+
// Limitation (RS-06 / #162): This is a false-positive risk for JS-driven sticky toggles where the
|
|
112
|
+
// CSS `position` value changes (e.g. from static to fixed). A discrete state change causes a large
|
|
113
|
+
// top jump which computeFlicker currently flags as a flicker. A full fix requires tracking `position`
|
|
114
|
+
// and ignoring jumps when the position value changes. Tracked as a P2 limitation for a future pass.
|
|
97
115
|
function computeFlicker(samplesByElement) {
|
|
98
116
|
const flicker = [];
|
|
99
117
|
for (const [stickyID, samples] of samplesByElement) {
|
|
100
|
-
for (let
|
|
101
|
-
const topDelta = samples[
|
|
102
|
-
const scrollDelta = samples[
|
|
118
|
+
for (let sampleIndex = 1; sampleIndex < samples.length; sampleIndex++) {
|
|
119
|
+
const topDelta = samples[sampleIndex].top - samples[sampleIndex - 1].top;
|
|
120
|
+
const scrollDelta = samples[sampleIndex].scrollY - samples[sampleIndex - 1].scrollY;
|
|
103
121
|
const isPinned = Math.abs(topDelta) <= utils_1.ResponsiveConfig.PinFlickerTolerancePX;
|
|
104
122
|
const isFollowingScroll = Math.abs(topDelta + scrollDelta) <= utils_1.ResponsiveConfig.PinFlickerTolerancePX;
|
|
105
123
|
if (!isPinned && !isFollowingScroll) {
|
|
@@ -121,15 +139,15 @@ async function findFooterOverlaps(page) {
|
|
|
121
139
|
const stickies = await page.$$(`[${StickyMarkerAttribute}]`);
|
|
122
140
|
for (const sticky of stickies) {
|
|
123
141
|
const rect = await sticky.boundingBox();
|
|
124
|
-
if (footerRect
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
if (footerRect !== null && rect !== null) {
|
|
143
|
+
const overlapWidth = Math.min(rect.x + rect.width, footerRect.x + footerRect.width) - Math.max(rect.x, footerRect.x);
|
|
144
|
+
const overlapHeight = Math.min(rect.y + rect.height, footerRect.y + footerRect.height) - Math.max(rect.y, footerRect.y);
|
|
145
|
+
if (overlapWidth > 0 && overlapHeight > 0) {
|
|
146
|
+
const description = await sticky.evaluate((element) => {
|
|
147
|
+
return `<${element.tagName.toLowerCase()} class="${element.className}">`;
|
|
148
|
+
});
|
|
149
|
+
conflicts.push(description);
|
|
150
|
+
}
|
|
133
151
|
}
|
|
134
152
|
}
|
|
135
153
|
}
|
|
@@ -142,9 +160,8 @@ async function checkStickyElements(page) {
|
|
|
142
160
|
result = { stickyElementCount: 0, flicker: [], coversContent: [], overlapsFooter: [] };
|
|
143
161
|
}
|
|
144
162
|
else {
|
|
145
|
-
const { scrollHeight } = await getScrollMetrics(page);
|
|
146
163
|
const steps = 20;
|
|
147
|
-
const { samplesByElement, coversContent } = await collectStickySamples(page,
|
|
164
|
+
const { samplesByElement, coversContent } = await collectStickySamples(page, steps);
|
|
148
165
|
const flicker = computeFlicker(samplesByElement);
|
|
149
166
|
const overlapsFooter = await findFooterOverlaps(page);
|
|
150
167
|
result = {
|
package/dist/specs/utils.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export declare const ResponsiveConfig: {
|
|
|
4
4
|
readonly ScrollStepDelayMs: 100;
|
|
5
5
|
readonly ViewPortWidth: 1440;
|
|
6
6
|
readonly ViewPortHeight: 900;
|
|
7
|
-
readonly
|
|
8
|
-
readonly ZoomSettleDelayMs:
|
|
7
|
+
readonly ZoomFactor: 2;
|
|
8
|
+
readonly ZoomSettleDelayMs: 300;
|
|
9
9
|
readonly UpscaleTolerance: 1.05;
|
|
10
10
|
readonly OversizeTolerance: 2;
|
|
11
11
|
readonly ScrollDriftTolerance: 0.15;
|
|
@@ -13,6 +13,7 @@ export declare const ResponsiveConfig: {
|
|
|
13
13
|
readonly PinFlickerTolerancePX: 2;
|
|
14
14
|
readonly MinTouchTargetSizePX: 44;
|
|
15
15
|
readonly KeyboardOcclusionRatio: 0.4;
|
|
16
|
+
readonly CutoffTolerancePX: 5;
|
|
16
17
|
};
|
|
17
18
|
export interface ScrollMetrics {
|
|
18
19
|
scrollHeight: number;
|
|
@@ -41,7 +42,6 @@ export declare function getScrollMetrics(page: Page): Promise<ScrollMetrics>;
|
|
|
41
42
|
export declare function scrollThroughPage(page: Page): Promise<void>;
|
|
42
43
|
export declare function findTruncatedOrOverflowingText(page: Page): Promise<string[]>;
|
|
43
44
|
export declare function findOverlappingElements(page: Page, containerSelector: string): Promise<string[]>;
|
|
44
|
-
export declare function findOverlappingTapTargets(page: Page): Promise<string[]>;
|
|
45
45
|
export declare const ContentOverlapSelector: string;
|
|
46
46
|
export interface ImageSizeIssue {
|
|
47
47
|
src: string;
|
|
@@ -66,28 +66,55 @@ export interface ViewportSize {
|
|
|
66
66
|
width: number;
|
|
67
67
|
height: number;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
interface RenderedElementSnapshot extends ElementDescriptor {
|
|
70
|
+
rect: ElementRect;
|
|
71
|
+
isFixedOrSticky: boolean;
|
|
72
|
+
}
|
|
73
|
+
export declare function isCutoffAtViewportEdge(snapshot: RenderedElementSnapshot, viewport: ViewportSize): boolean;
|
|
70
74
|
export declare function findElementsCutoffAtViewportEdge(page: Page, containerSelector?: string): Promise<string[]>;
|
|
71
75
|
export declare function findUndersizedBodyText(page: Page): Promise<string[]>;
|
|
72
76
|
export declare function findUndersizedTouchTargets(page: Page, containerSelector: string): Promise<string[]>;
|
|
73
77
|
export declare function hasHorizontalScrollbar(page: Page): Promise<boolean>;
|
|
74
|
-
export declare function findCollapsedEmbeds(page: Page): Promise<string[]>;
|
|
75
78
|
export declare function isVideoAutoplaying(video: Locator): Promise<boolean>;
|
|
76
79
|
export declare function isVideoMuted(video: Locator): Promise<boolean>;
|
|
77
|
-
export
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
export interface UserInfoShape {
|
|
81
|
+
ValidTestName: string;
|
|
82
|
+
ValidTestEmail: string;
|
|
83
|
+
ValidTestPhone: string;
|
|
84
|
+
ValidTestCompany: string;
|
|
85
|
+
ValidTestProjectDescription: string;
|
|
86
|
+
ValidTestPreferredDate: string;
|
|
87
|
+
ValidTestPreferredTime: string;
|
|
88
|
+
LongNameCharacterCount: number;
|
|
89
|
+
EmojiTestName: string;
|
|
90
|
+
PaddedTestEmail: string;
|
|
91
|
+
InvalidTestEmail: string;
|
|
92
|
+
}
|
|
93
|
+
export declare const ValidUserInfo: UserInfoShape;
|
|
85
94
|
export interface RequiredField {
|
|
86
95
|
label: RegExp;
|
|
87
96
|
validValue: string;
|
|
88
97
|
}
|
|
89
|
-
export declare
|
|
90
|
-
export declare
|
|
98
|
+
export declare function isFieldRequired(fieldLocator: Locator): Promise<boolean>;
|
|
99
|
+
export declare const FieldInteractions: {
|
|
100
|
+
readonly Fill: "fill";
|
|
101
|
+
readonly Datetime: "datetime";
|
|
102
|
+
};
|
|
103
|
+
export type FieldInteraction = (typeof FieldInteractions)[keyof typeof FieldInteractions];
|
|
104
|
+
export interface FieldValueMapping {
|
|
105
|
+
label: RegExp;
|
|
106
|
+
validValue: string;
|
|
107
|
+
interaction: FieldInteraction;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Maps a detected field (by its label) to the valid test value used
|
|
111
|
+
* to fill it. Presence here does NOT imply the field is required —
|
|
112
|
+
* required-ness is determined dynamically per field via
|
|
113
|
+
* `isFieldRequired`. Email is the only field that is always required.
|
|
114
|
+
*/
|
|
115
|
+
export declare const FieldValueMappings: FieldValueMapping[];
|
|
116
|
+
export declare function fillDateTimePicker(locator: Locator, fallbackValue: string): Promise<void>;
|
|
117
|
+
export declare function fillFormFields(page: Page): Promise<void>;
|
|
91
118
|
export declare function hasVisibleErrorNearField(fieldLocator: Locator): Promise<boolean>;
|
|
92
119
|
export interface InvalidInputCase {
|
|
93
120
|
fieldLabel: RegExp;
|
|
@@ -117,3 +144,16 @@ export declare function swipeToPreviousSlide(carousel: Locator): Promise<boolean
|
|
|
117
144
|
export declare function traverseSlidesForward(carousel: Locator, carouselIndex: number, isMobile: boolean): Promise<void>;
|
|
118
145
|
export declare function checkSwipeBackward(carousel: Locator, carouselIndex: number): Promise<void>;
|
|
119
146
|
export declare function checkAllDots(carousel: Locator, carouselIndex: number): Promise<void>;
|
|
147
|
+
export declare const SafeAreaZones: {
|
|
148
|
+
readonly Top: "top";
|
|
149
|
+
readonly Bottom: "bottom";
|
|
150
|
+
readonly Left: "left";
|
|
151
|
+
readonly Right: "right";
|
|
152
|
+
};
|
|
153
|
+
export type SafeAreaZone = (typeof SafeAreaZones)[keyof typeof SafeAreaZones];
|
|
154
|
+
export interface SafeAreaOverlap extends ElementDescriptor {
|
|
155
|
+
text: string;
|
|
156
|
+
overlapZone: SafeAreaZone;
|
|
157
|
+
}
|
|
158
|
+
export declare function findSafeAreaOverlaps(page: Page): Promise<SafeAreaOverlap[]>;
|
|
159
|
+
export {};
|