@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.
@@ -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
- const collapsedEmbeds = await (0, utils_1.findCollapsedEmbeds)(page);
15
- (0, test_1.expect)(collapsedEmbeds, `Embed(s) collapsed to zero size:\n${collapsedEmbeds.join("\n")}`).toEqual([]);
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 collectStickySamples(page, scrollHeight, steps) {
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 down = Array.from({ length: steps + 1 }, (_, i) => {
73
- return Math.floor((scrollHeight / steps) * i);
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 }, (_, i) => {
76
- return Math.floor((scrollHeight / steps) * (steps - i));
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 scrollY of [...down, ...up]) {
88
+ for (const targetScrollY of [...down, ...up]) {
79
89
  await page.evaluate((y) => {
80
90
  window.scrollTo(0, y);
81
- }, scrollY);
82
- await page.waitForTimeout(100);
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 i = 1; i < samples.length; i++) {
101
- const topDelta = samples[i].top - samples[i - 1].top;
102
- const scrollDelta = samples[i].scrollY - samples[i - 1].scrollY;
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 === null || rect === null)
125
- continue;
126
- const overlapWidth = Math.min(rect.x + rect.width, footerRect.x + footerRect.width) - Math.max(rect.x, footerRect.x);
127
- const overlapHeight = Math.min(rect.y + rect.height, footerRect.y + footerRect.height) - Math.max(rect.y, footerRect.y);
128
- if (overlapWidth > 0 && overlapHeight > 0) {
129
- const description = await sticky.evaluate((element) => {
130
- return `<${element.tagName.toLowerCase()} class="${element.className}">`;
131
- });
132
- conflicts.push(description);
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, scrollHeight, steps);
164
+ const { samplesByElement, coversContent } = await collectStickySamples(page, steps);
148
165
  const flicker = computeFlicker(samplesByElement);
149
166
  const overlapsFooter = await findFooterOverlaps(page);
150
167
  result = {
@@ -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 ZoomLevel: "200%";
8
- readonly ZoomSettleDelayMs: 200;
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
- export declare function isCutoffAtViewportEdge(rect: ElementRect, viewport: ViewportSize): boolean;
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 declare const ValidUserInfo: {
78
- readonly ValidTestName: "Jane Doe";
79
- readonly ValidTestEmail: "test@test.com";
80
- readonly ValidTestPhone: "9876543210";
81
- readonly LongNameCharacterCount: 250;
82
- readonly EmojiTestName: "Jane 😀 Doe";
83
- readonly PaddedTestEmail: " test@test.com ";
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 const RequiredFields: RequiredField[];
90
- export declare function fillAllFieldsValidly(page: Page): Promise<void>;
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 {};