@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.
@@ -1,12 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KeyboardFields = exports.AutofillFields = exports.InvalidInputCases = exports.RequiredFields = exports.ValidUserInfo = exports.ContentOverlapSelector = exports.TouchTargetSizeSelector = exports.TappableElementsSelector = exports.ResponsiveConfig = void 0;
3
+ exports.SafeAreaZones = exports.KeyboardFields = exports.AutofillFields = exports.InvalidInputCases = exports.FieldValueMappings = exports.FieldInteractions = exports.ValidUserInfo = exports.ContentOverlapSelector = exports.TouchTargetSizeSelector = exports.TappableElementsSelector = exports.ResponsiveConfig = void 0;
4
4
  exports.describeElement = describeElement;
5
5
  exports.getScrollMetrics = getScrollMetrics;
6
6
  exports.scrollThroughPage = scrollThroughPage;
7
7
  exports.findTruncatedOrOverflowingText = findTruncatedOrOverflowingText;
8
8
  exports.findOverlappingElements = findOverlappingElements;
9
- exports.findOverlappingTapTargets = findOverlappingTapTargets;
10
9
  exports.findImageSizeIssues = findImageSizeIssues;
11
10
  exports.isImageUpscaled = isImageUpscaled;
12
11
  exports.isImageOversized = isImageOversized;
@@ -15,10 +14,11 @@ exports.findElementsCutoffAtViewportEdge = findElementsCutoffAtViewportEdge;
15
14
  exports.findUndersizedBodyText = findUndersizedBodyText;
16
15
  exports.findUndersizedTouchTargets = findUndersizedTouchTargets;
17
16
  exports.hasHorizontalScrollbar = hasHorizontalScrollbar;
18
- exports.findCollapsedEmbeds = findCollapsedEmbeds;
19
17
  exports.isVideoAutoplaying = isVideoAutoplaying;
20
18
  exports.isVideoMuted = isVideoMuted;
21
- exports.fillAllFieldsValidly = fillAllFieldsValidly;
19
+ exports.isFieldRequired = isFieldRequired;
20
+ exports.fillDateTimePicker = fillDateTimePicker;
21
+ exports.fillFormFields = fillFormFields;
22
22
  exports.hasVisibleErrorNearField = hasVisibleErrorNearField;
23
23
  exports.fillOtherRequiredFieldsValidly = fillOtherRequiredFieldsValidly;
24
24
  exports.getActiveSlideIndex = getActiveSlideIndex;
@@ -32,14 +32,15 @@ exports.swipeToPreviousSlide = swipeToPreviousSlide;
32
32
  exports.traverseSlidesForward = traverseSlidesForward;
33
33
  exports.checkSwipeBackward = checkSwipeBackward;
34
34
  exports.checkAllDots = checkAllDots;
35
+ exports.findSafeAreaOverlaps = findSafeAreaOverlaps;
35
36
  const test_1 = require("@playwright/test");
36
37
  exports.ResponsiveConfig = {
37
38
  ScrollSteps: 10,
38
39
  ScrollStepDelayMs: 100,
39
40
  ViewPortWidth: 1440,
40
41
  ViewPortHeight: 900,
41
- ZoomLevel: "200%",
42
- ZoomSettleDelayMs: 200,
42
+ ZoomFactor: 2,
43
+ ZoomSettleDelayMs: 300,
43
44
  UpscaleTolerance: 1.05,
44
45
  OversizeTolerance: 2,
45
46
  ScrollDriftTolerance: 0.15,
@@ -47,6 +48,7 @@ exports.ResponsiveConfig = {
47
48
  PinFlickerTolerancePX: 2,
48
49
  MinTouchTargetSizePX: 44,
49
50
  KeyboardOcclusionRatio: 0.4,
51
+ CutoffTolerancePX: 5,
50
52
  };
51
53
  // Formats a descriptor captured inside page.evaluate (see findElementsCutoffAtViewportEdge and
52
54
  // findUndersizedBodyText) into a readable tag selector, e.g. <p.hero.subtitle>.
@@ -113,13 +115,17 @@ async function findTruncatedOrOverflowingText(page) {
113
115
  const elements = Array.from(document.querySelectorAll("p, li, td, th, dt, dd, blockquote, figcaption, h1, h2, h3, h4, h5, h6"));
114
116
  for (const element of elements) {
115
117
  const text = element.textContent.trim();
116
- if (text.length === 0)
117
- continue;
118
- const style = window.getComputedStyle(element);
119
- const isEllipsisTruncated = style.textOverflow === "ellipsis" && style.overflow !== "visible";
120
- const isOverflowingHorizontally = element.scrollWidth > element.clientWidth + 1;
121
- if (isEllipsisTruncated || isOverflowingHorizontally) {
122
- broken.push(`<${element.tagName.toLowerCase()} class="${element.className}"> overflow=${style.overflow}, textOverflow=${style.textOverflow}: "${text.slice(0, 40)}"`);
118
+ if (text.length > 0) {
119
+ const style = window.getComputedStyle(element);
120
+ const isEllipsisTruncated = style.textOverflow === "ellipsis" && style.overflow !== "visible";
121
+ const isOverflowingHorizontally = element.scrollWidth > element.clientWidth + 1;
122
+ // Ellipsis truncation is a deliberate, visually-indicated design pattern (card previews,
123
+ // single-line labels) — not a bug. Only flag genuinely broken cases: text overflowing
124
+ // WITHOUT any truncation handling (spilling out unstyled), which is always unintentional.
125
+ const isUnhandledOverflow = isOverflowingHorizontally && !isEllipsisTruncated;
126
+ if (isUnhandledOverflow) {
127
+ broken.push(`<${element.tagName.toLowerCase()} class="${element.className}"> overflow=${style.overflow}, textOverflow=${style.textOverflow}: "${text.slice(0, 40)}"`);
128
+ }
123
129
  }
124
130
  }
125
131
  return broken;
@@ -127,9 +133,6 @@ async function findTruncatedOrOverflowingText(page) {
127
133
  }
128
134
  async function findOverlappingElements(page, containerSelector) {
129
135
  const overlaps = await page.evaluate((selector) => {
130
- // Only elements at least partially visible in the viewport are candidates — off-canvas nav
131
- // drawers, upcoming carousel slides, and similar patterns commonly overlap internally while
132
- // hidden off-screen, and that's not a bug (mirrors the check in findElementsCutoffAtViewportEdge).
133
136
  function isVisibleInViewport(element) {
134
137
  const rect = element.getBoundingClientRect();
135
138
  const hasDimensions = rect.width > 0 && rect.height > 0;
@@ -137,25 +140,75 @@ async function findOverlappingElements(page, containerSelector) {
137
140
  const verticalOverlap = Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0);
138
141
  return hasDimensions && horizontalOverlap > 0 && verticalOverlap > 0;
139
142
  }
143
+ // An element clipped by an ancestor's overflow:hidden/scroll/auto (e.g. a closed mobile
144
+ // drawer/menu duplicate) is intentionally hidden from view — not a real visual overlap.
145
+ // Defined inside this evaluate callback (rather than at module scope) since page.evaluate
146
+ // serializes only the callback itself — outer Node-side function references aren't
147
+ // available once this runs in the browser.
148
+ function isClippedByAncestorOverflow(element) {
149
+ const rect = element.getBoundingClientRect();
150
+ let parent = element.parentElement;
151
+ let isClipped = false;
152
+ while (parent !== null && !isClipped) {
153
+ const parentStyle = window.getComputedStyle(parent);
154
+ const isOverflowClipped = ["hidden", "scroll", "auto"].includes(parentStyle.overflowX) ||
155
+ ["hidden", "scroll", "auto"].includes(parentStyle.overflowY);
156
+ if (isOverflowClipped) {
157
+ const parentRect = parent.getBoundingClientRect();
158
+ const isOutsideParentBounds = rect.right <= parentRect.left ||
159
+ rect.left >= parentRect.right ||
160
+ rect.bottom <= parentRect.top ||
161
+ rect.top >= parentRect.bottom;
162
+ if (isOutsideParentBounds) {
163
+ isClipped = true;
164
+ }
165
+ }
166
+ parent = parent.parentElement;
167
+ }
168
+ return isClipped;
169
+ }
140
170
  const conflicts = [];
141
- const elements = Array.from(document.querySelectorAll(selector)).filter(isVisibleInViewport);
171
+ const elements = Array.from(document.querySelectorAll(selector))
172
+ .filter(isVisibleInViewport)
173
+ .filter((element) => {
174
+ return !isClippedByAncestorOverflow(element);
175
+ });
176
+ // Checks the element itself and its ancestors: a fixed/sticky container's own children are
177
+ // normally position:static/relative, but they still visually pin to the viewport along with
178
+ // it, so the pinning has to be detected up the ancestor chain, not just on the element itself.
179
+ function isPinnedToViewport(element) {
180
+ let current = element;
181
+ let isPinned = false;
182
+ while (current !== null && !isPinned) {
183
+ const { position } = window.getComputedStyle(current);
184
+ isPinned = position === "fixed" || position === "sticky";
185
+ current = current.parentElement;
186
+ }
187
+ return isPinned;
188
+ }
142
189
  for (let i = 0; i < elements.length; i++) {
143
190
  for (let index = i + 1; index < elements.length; index++) {
144
191
  const a = elements[i];
145
192
  const b = elements[index];
146
- if (a.contains(b) || b.contains(a))
147
- continue;
148
- const rectA = a.getBoundingClientRect();
149
- const rectB = b.getBoundingClientRect();
150
- const overlapWidth = Math.min(rectA.right, rectB.right) - Math.max(rectA.left, rectB.left);
151
- const overlapHeight = Math.min(rectA.bottom, rectB.bottom) - Math.max(rectA.top, rectB.top);
152
- if (overlapWidth > 0 && overlapHeight > 0) {
153
- conflicts.push({
154
- elementA: { tagName: a.tagName, className: a.className },
155
- elementB: { tagName: b.tagName, className: b.className },
156
- overlapWidth,
157
- overlapHeight,
158
- });
193
+ const isNested = a.contains(b) || b.contains(a);
194
+ // A fixed/sticky element (or something inside one) sitting over whatever content has
195
+ // scrolled beneath it is expected, inherent behavior — not a layout bug. Only flag
196
+ // overlaps between two elements that are both in normal flow (or both pinned, which
197
+ // would indicate a real stacking conflict between two pinned elements).
198
+ const isExpectedPinnedOverlap = isPinnedToViewport(a) !== isPinnedToViewport(b);
199
+ if (!isNested && !isExpectedPinnedOverlap) {
200
+ const rectA = a.getBoundingClientRect();
201
+ const rectB = b.getBoundingClientRect();
202
+ const overlapWidth = Math.min(rectA.right, rectB.right) - Math.max(rectA.left, rectB.left);
203
+ const overlapHeight = Math.min(rectA.bottom, rectB.bottom) - Math.max(rectA.top, rectB.top);
204
+ if (overlapWidth > 0 && overlapHeight > 0) {
205
+ conflicts.push({
206
+ elementA: { tagName: a.tagName, className: a.className },
207
+ elementB: { tagName: b.tagName, className: b.className },
208
+ overlapWidth,
209
+ overlapHeight,
210
+ });
211
+ }
159
212
  }
160
213
  }
161
214
  }
@@ -165,9 +218,6 @@ async function findOverlappingElements(page, containerSelector) {
165
218
  return `${overlap.elementA.tagName} overlaps ${overlap.elementB.tagName} by ${overlap.overlapWidth.toFixed(0)}x${overlap.overlapHeight.toFixed(0)}px`;
166
219
  });
167
220
  }
168
- async function findOverlappingTapTargets(page) {
169
- return findOverlappingElements(page, exports.TappableElementsSelector);
170
- }
171
221
  // Content elements worth checking for overlap after a layout-affecting change (e.g. orientation
172
222
  // change): tappable elements, text-bearing elements, and images.
173
223
  exports.ContentOverlapSelector = [
@@ -228,14 +278,22 @@ function isImageOversized(image) {
228
278
  // Cut off means straddling the viewport edge (partially visible), not positioned entirely
229
279
  // outside it — off-canvas nav drawers, upcoming carousel slides, and visually-hidden
230
280
  // screen-reader text are intentionally placed fully outside the viewport and are not bugs.
281
+ // Vertical straddling is only flagged for fixed/sticky elements: normal in-flow content
282
+ // straddles the top/bottom edge constantly while scrolling, which is expected, not a bug.
231
283
  // Pure geometry (no DOM APIs), so it runs in Node against a rect snapshot taken in the browser.
232
- function isCutoffAtViewportEdge(rect, viewport) {
284
+ function isCutoffAtViewportEdge(snapshot, viewport) {
285
+ const { rect } = snapshot;
233
286
  const hasDimensions = rect.width > 0 && rect.height > 0;
234
287
  const horizontalOverlap = Math.min(rect.right, viewport.width) - Math.max(rect.left, 0);
288
+ const isHorizontallyClipped = horizontalOverlap > 0 &&
289
+ (rect.left < -exports.ResponsiveConfig.CutoffTolerancePX ||
290
+ rect.right > viewport.width + exports.ResponsiveConfig.CutoffTolerancePX);
235
291
  const verticalOverlap = Math.min(rect.bottom, viewport.height) - Math.max(rect.top, 0);
236
- const overlapsViewport = horizontalOverlap > 0 && verticalOverlap > 0;
237
- const fullyInsideViewport = rect.left >= 0 && rect.right <= viewport.width && rect.top >= 0 && rect.bottom <= viewport.height;
238
- return hasDimensions && overlapsViewport && !fullyInsideViewport;
292
+ const isVerticallyClipped = snapshot.isFixedOrSticky &&
293
+ verticalOverlap > 0 &&
294
+ (rect.top < -exports.ResponsiveConfig.CutoffTolerancePX ||
295
+ rect.bottom > viewport.height + exports.ResponsiveConfig.CutoffTolerancePX);
296
+ return hasDimensions && (isHorizontallyClipped || isVerticallyClipped);
239
297
  }
240
298
  async function findElementsCutoffAtViewportEdge(page, containerSelector = "body *") {
241
299
  const { elements, viewport } = await page.evaluate((selector) => {
@@ -243,10 +301,41 @@ async function findElementsCutoffAtViewportEdge(page, containerSelector = "body
243
301
  const style = window.getComputedStyle(element);
244
302
  return style.display !== "none" && style.visibility !== "hidden" && Number.parseFloat(style.opacity) !== 0;
245
303
  }
304
+ // An element clipped by an ancestor's overflow:hidden/scroll/auto (e.g. a closed
305
+ // drawer/menu duplicate) is intentionally hidden from view — not a real cutoff.
306
+ // Defined inside this evaluate callback (rather than at module scope) since page.evaluate
307
+ // serializes only the callback itself — outer Node-side function references aren't
308
+ // available once this runs in the browser.
309
+ function isClippedByAncestorOverflow(element) {
310
+ const rect = element.getBoundingClientRect();
311
+ let parent = element.parentElement;
312
+ let isClipped = false;
313
+ while (parent !== null && !isClipped) {
314
+ const parentStyle = window.getComputedStyle(parent);
315
+ const isOverflowClipped = ["hidden", "scroll", "auto"].includes(parentStyle.overflowX) ||
316
+ ["hidden", "scroll", "auto"].includes(parentStyle.overflowY);
317
+ if (isOverflowClipped) {
318
+ const parentRect = parent.getBoundingClientRect();
319
+ const isOutsideParentBounds = rect.right <= parentRect.left ||
320
+ rect.left >= parentRect.right ||
321
+ rect.bottom <= parentRect.top ||
322
+ rect.top >= parentRect.bottom;
323
+ if (isOutsideParentBounds) {
324
+ isClipped = true;
325
+ }
326
+ }
327
+ parent = parent.parentElement;
328
+ }
329
+ return isClipped;
330
+ }
246
331
  const renderedElements = Array.from(document.querySelectorAll(selector))
247
332
  .filter(isRenderedElement)
333
+ .filter((element) => {
334
+ return !isClippedByAncestorOverflow(element);
335
+ })
248
336
  .map((element) => {
249
337
  const rect = element.getBoundingClientRect();
338
+ const style = window.getComputedStyle(element);
250
339
  return {
251
340
  tagName: element.tagName,
252
341
  className: element instanceof HTMLElement ? element.className : "",
@@ -258,13 +347,14 @@ async function findElementsCutoffAtViewportEdge(page, containerSelector = "body
258
347
  width: rect.width,
259
348
  height: rect.height,
260
349
  },
350
+ isFixedOrSticky: style.position === "fixed" || style.position === "sticky",
261
351
  };
262
352
  });
263
353
  return { elements: renderedElements, viewport: { width: window.innerWidth, height: window.innerHeight } };
264
354
  }, containerSelector);
265
355
  return elements
266
356
  .filter((element) => {
267
- return isCutoffAtViewportEdge(element.rect, viewport);
357
+ return isCutoffAtViewportEdge(element, viewport);
268
358
  })
269
359
  .map(describeElement);
270
360
  }
@@ -319,20 +409,6 @@ async function hasHorizontalScrollbar(page) {
319
409
  return document.documentElement.scrollWidth > document.documentElement.clientWidth;
320
410
  });
321
411
  }
322
- async function findCollapsedEmbeds(page) {
323
- return page.evaluate(() => {
324
- const embeds = Array.from(document.querySelectorAll("iframe, embed, object"));
325
- return embeds
326
- .filter((embed) => {
327
- const rect = embed.getBoundingClientRect();
328
- return rect.width === 0 || rect.height === 0;
329
- })
330
- .map((embed) => {
331
- const rect = embed.getBoundingClientRect();
332
- return `<${embed.tagName.toLowerCase()}> collapsed to ${rect.width.toFixed(0)}x${rect.height.toFixed(0)}px`;
333
- });
334
- });
335
- }
336
412
  async function isVideoAutoplaying(video) {
337
413
  const isPaused = await video.evaluate((element) => {
338
414
  return element.paused;
@@ -348,20 +424,110 @@ exports.ValidUserInfo = {
348
424
  ValidTestName: "Jane Doe",
349
425
  ValidTestEmail: "test@test.com",
350
426
  ValidTestPhone: "9876543210",
427
+ ValidTestCompany: "Acme Corp",
428
+ ValidTestProjectDescription: "This is a sample project description used for automated contact form testing.",
429
+ ValidTestPreferredDate: "2026-09-15",
430
+ ValidTestPreferredTime: "10:00 AM",
351
431
  LongNameCharacterCount: 250,
352
432
  EmojiTestName: "Jane 😀 Doe",
353
433
  PaddedTestEmail: " test@test.com ",
434
+ InvalidTestEmail: "not-an-email",
435
+ };
436
+ async function isFieldRequired(fieldLocator) {
437
+ const requiredAttribute = await fieldLocator.getAttribute("required");
438
+ const ariaRequired = await fieldLocator.getAttribute("aria-required");
439
+ const isMarkedRequired = requiredAttribute !== null || ariaRequired === "true";
440
+ return isMarkedRequired;
441
+ }
442
+ exports.FieldInteractions = {
443
+ Fill: "fill",
444
+ Datetime: "datetime",
354
445
  };
355
- exports.RequiredFields = [
356
- { label: /name/i, validValue: exports.ValidUserInfo.ValidTestName },
357
- { label: /email/i, validValue: exports.ValidUserInfo.ValidTestEmail },
358
- { label: /phone/i, validValue: exports.ValidUserInfo.ValidTestPhone },
446
+ /**
447
+ * Maps a detected field (by its label) to the valid test value used
448
+ * to fill it. Presence here does NOT imply the field is required —
449
+ * required-ness is determined dynamically per field via
450
+ * `isFieldRequired`. Email is the only field that is always required.
451
+ */
452
+ exports.FieldValueMappings = [
453
+ { label: /name/i, validValue: exports.ValidUserInfo.ValidTestName, interaction: exports.FieldInteractions.Fill },
454
+ { label: /email/i, validValue: exports.ValidUserInfo.ValidTestEmail, interaction: exports.FieldInteractions.Fill },
455
+ { label: /phone/i, validValue: exports.ValidUserInfo.ValidTestPhone, interaction: exports.FieldInteractions.Fill },
456
+ { label: /company/i, validValue: exports.ValidUserInfo.ValidTestCompany, interaction: exports.FieldInteractions.Fill },
457
+ {
458
+ label: /project|description/i,
459
+ validValue: exports.ValidUserInfo.ValidTestProjectDescription,
460
+ interaction: exports.FieldInteractions.Fill,
461
+ },
462
+ { label: /date/i, validValue: exports.ValidUserInfo.ValidTestPreferredDate, interaction: exports.FieldInteractions.Datetime },
463
+ { label: /time/i, validValue: exports.ValidUserInfo.ValidTestPreferredTime, interaction: exports.FieldInteractions.Datetime },
359
464
  ];
360
- async function fillAllFieldsValidly(page) {
361
- await Promise.all(exports.RequiredFields.map(async (field) => {
465
+ // Some date/time fields are plain inputs (a direct .fill() works); others are picker widgets that
466
+ // open a popup and reject typed text. This tries the plain-input path first, and only falls back
467
+ // to interacting with a popup if the value didn't actually take. The fallback uses WAI-ARIA roles
468
+ // (grid/gridcell for calendar days, spinbutton for numeric steppers) rather than any specific UI
469
+ // library's class names, since this package runs against apps that don't all use the same one —
470
+ // it degrades to a no-op if neither the plain-input path nor these ARIA patterns match anything.
471
+ async function fillDateTimePicker(locator, fallbackValue) {
472
+ await locator.fill(fallbackValue).catch(() => {
473
+ return undefined;
474
+ });
475
+ const filledValue = await locator.inputValue().catch(() => {
476
+ return "";
477
+ });
478
+ if (filledValue.length === 0) {
479
+ await locator.click();
480
+ const page = locator.page();
481
+ const dayCells = page.locator('[role="gridcell"]:not([aria-disabled="true"])');
482
+ const dayCellCount = await dayCells.count();
483
+ for (let index = 0; index < dayCellCount; index++) {
484
+ const cell = dayCells.nth(index);
485
+ if (await cell.isVisible()) {
486
+ await cell.click();
487
+ await page.waitForTimeout(300);
488
+ break;
489
+ }
490
+ }
491
+ const spinbuttons = page.locator('[role="spinbutton"]');
492
+ const spinbuttonCount = await spinbuttons.count();
493
+ for (let index = 0; index < spinbuttonCount; index++) {
494
+ const button = spinbuttons.nth(index);
495
+ // Guards against the element detaching mid-animation, same as the day-cell loop above.
496
+ const isStillPresent = await button.isVisible().catch(() => {
497
+ return false;
498
+ });
499
+ if (isStillPresent) {
500
+ await button.click();
501
+ await page.keyboard.press("ArrowUp");
502
+ await page.waitForTimeout(100);
503
+ }
504
+ }
505
+ const confirmButton = page.getByRole("button", { name: /submit|confirm|apply|done|ok/i });
506
+ const confirmButtonCount = await confirmButton.count();
507
+ let isConfirmed = false;
508
+ for (let index = 0; index < confirmButtonCount; index++) {
509
+ const button = confirmButton.nth(index);
510
+ if (await button.isVisible()) {
511
+ await button.click();
512
+ isConfirmed = true;
513
+ break;
514
+ }
515
+ }
516
+ if (!isConfirmed) {
517
+ await page.keyboard.press("Escape");
518
+ }
519
+ }
520
+ }
521
+ async function fillFormFields(page) {
522
+ await Promise.all(exports.FieldValueMappings.map(async (field) => {
362
523
  const locator = page.getByLabel(field.label);
363
524
  if ((await locator.count()) > 0) {
364
- await locator.first().fill(field.validValue);
525
+ if (field.interaction === exports.FieldInteractions.Datetime) {
526
+ await fillDateTimePicker(locator.first(), field.validValue);
527
+ }
528
+ else {
529
+ await locator.first().fill(field.validValue);
530
+ }
365
531
  }
366
532
  }));
367
533
  }
@@ -378,7 +544,13 @@ async function hasVisibleErrorNearField(fieldLocator) {
378
544
  const adjacentError = fieldLocator.page().locator(`[data-error-for="${fieldID}"]`);
379
545
  isAdjacentErrorVisible = await adjacentError.isVisible();
380
546
  }
381
- return isDescribedByErrorVisible || isAdjacentErrorVisible;
547
+ // Native browser validation (e.g. the built-in "Please fill out this field" bubble triggered
548
+ // by the `required` attribute) isn't a queryable DOM node, so it can't be caught by the checks
549
+ // above — but the field's own validity state still reflects it.
550
+ const isNativelyInvalid = await fieldLocator.evaluate((element) => {
551
+ return !element.validity.valid && element.validationMessage !== "";
552
+ });
553
+ return isDescribedByErrorVisible || isAdjacentErrorVisible || isNativelyInvalid;
382
554
  }
383
555
  exports.InvalidInputCases = [
384
556
  { fieldLabel: /email/i, invalidValue: "notanemail", expectedErrorText: /enter a valid email address/i },
@@ -388,9 +560,26 @@ exports.InvalidInputCases = [
388
560
  { fieldLabel: /phone/i, invalidValue: "12", expectedErrorText: /enter a valid phone number/i },
389
561
  ];
390
562
  async function fillOtherRequiredFieldsValidly(page, skipLabel) {
391
- if (!/name/i.test(skipLabel.source)) {
392
- await page.getByLabel(/name/i).fill(exports.ValidUserInfo.ValidTestName);
393
- }
563
+ await Promise.all(exports.FieldValueMappings.map(async (field) => {
564
+ const isSkippedField = field.label.source === skipLabel.source;
565
+ const locator = page.getByLabel(field.label);
566
+ let isFieldPresent = false;
567
+ if (!isSkippedField) {
568
+ isFieldPresent = (await locator.count()) > 0;
569
+ }
570
+ let isRequired = false;
571
+ if (isFieldPresent) {
572
+ isRequired = await isFieldRequired(locator.first());
573
+ }
574
+ if (isRequired) {
575
+ if (field.interaction === exports.FieldInteractions.Datetime) {
576
+ await fillDateTimePicker(locator.first(), field.validValue);
577
+ }
578
+ else {
579
+ await locator.first().fill(field.validValue);
580
+ }
581
+ }
582
+ }));
394
583
  }
395
584
  exports.AutofillFields = [
396
585
  { label: /name/i, expectedAutocomplete: /name/i },
@@ -527,3 +716,108 @@ async function checkAllDots(carousel, carouselIndex) {
527
716
  }
528
717
  }
529
718
  }
719
+ exports.SafeAreaZones = {
720
+ Top: "top",
721
+ Bottom: "bottom",
722
+ Left: "left",
723
+ Right: "right",
724
+ };
725
+ function getPadding(val) {
726
+ let numericValue = 0;
727
+ const parsed = Number.parseFloat(val);
728
+ if (!Number.isNaN(parsed)) {
729
+ numericValue = parsed;
730
+ }
731
+ return numericValue;
732
+ }
733
+ function isVisible(element) {
734
+ let visible;
735
+ const style = window.getComputedStyle(element);
736
+ if (style.display === "none" || style.visibility === "hidden" || Number.parseFloat(style.opacity) === 0) {
737
+ visible = false;
738
+ }
739
+ else {
740
+ const rect = element.getBoundingClientRect();
741
+ const hasDimensions = rect.width > 0 && rect.height > 0;
742
+ const horizontalOverlap = Math.min(rect.right, window.innerWidth) - Math.max(rect.left, 0);
743
+ const verticalOverlap = Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0);
744
+ visible = hasDimensions && horizontalOverlap > 0 && verticalOverlap > 0;
745
+ }
746
+ return visible;
747
+ }
748
+ async function findSafeAreaOverlaps(page) {
749
+ const contentSelector = `${exports.TappableElementsSelector}, p, h1, h2, h3, h4, h5, h6, li, span`;
750
+ return page.evaluate(({ selector }) => {
751
+ // Dynamically measure actual safe area insets from the browser environment
752
+ const div = document.createElement("div");
753
+ div.style.paddingTop = "env(safe-area-inset-top, 0px)";
754
+ div.style.paddingRight = "env(safe-area-inset-right, 0px)";
755
+ div.style.paddingBottom = "env(safe-area-inset-bottom, 0px)";
756
+ div.style.paddingLeft = "env(safe-area-inset-left, 0px)";
757
+ div.style.position = "absolute";
758
+ div.style.visibility = "hidden";
759
+ document.body.appendChild(div);
760
+ const computedStyle = window.getComputedStyle(div);
761
+ const { paddingTop } = computedStyle;
762
+ const { paddingRight } = computedStyle;
763
+ const { paddingBottom } = computedStyle;
764
+ const { paddingLeft } = computedStyle;
765
+ document.body.removeChild(div);
766
+ const insets = {
767
+ top: getPadding(paddingTop),
768
+ right: getPadding(paddingRight),
769
+ bottom: getPadding(paddingBottom),
770
+ left: getPadding(paddingLeft),
771
+ };
772
+ const overlaps = [];
773
+ const elements = Array.from(document.querySelectorAll(selector));
774
+ const visibleElements = elements.filter(isVisible);
775
+ for (const element of visibleElements) {
776
+ // Skip elements that are likely just layout containers or backgrounds if they don't have direct text
777
+ // (Though the selector restricts to text/tappable elements mostly)
778
+ const rect = element.getBoundingClientRect();
779
+ const style = window.getComputedStyle(element);
780
+ const pl = getPadding(style.paddingLeft);
781
+ const pr = getPadding(style.paddingRight);
782
+ const pt = getPadding(style.paddingTop);
783
+ const pb = getPadding(style.paddingBottom);
784
+ const contentLeft = rect.left + pl;
785
+ const contentRight = rect.right - pr;
786
+ const contentTop = rect.top + pt;
787
+ const contentBottom = rect.bottom - pb;
788
+ const viewportWidth = window.innerWidth;
789
+ const viewportHeight = window.innerHeight;
790
+ const isFixedOrSticky = style.position === "fixed" || style.position === "sticky";
791
+ const zones = [];
792
+ // Check if element content intersects with top safe area
793
+ if (insets.top > 0 && contentTop < insets.top && contentBottom > 0) {
794
+ zones.push("top");
795
+ }
796
+ // Check if element content intersects with bottom safe area
797
+ if (insets.bottom > 0 && contentBottom > viewportHeight - insets.bottom && contentTop < viewportHeight) {
798
+ // Normal static elements naturally scroll through the bottom safe area,
799
+ // so only flag fixed or sticky elements that overlap it on load.
800
+ if (isFixedOrSticky) {
801
+ zones.push("bottom");
802
+ }
803
+ }
804
+ // Check if element content intersects with left safe area
805
+ if (insets.left > 0 && contentLeft < insets.left && contentRight > 0) {
806
+ zones.push("left");
807
+ }
808
+ // Check if element content intersects with right safe area
809
+ if (insets.right > 0 && contentRight > viewportWidth - insets.right && contentLeft < viewportWidth) {
810
+ zones.push("right");
811
+ }
812
+ for (const zone of zones) {
813
+ overlaps.push({
814
+ tagName: element.tagName,
815
+ className: element.className,
816
+ text: element.textContent.trim().slice(0, 40),
817
+ overlapZone: zone,
818
+ });
819
+ }
820
+ }
821
+ return overlaps;
822
+ }, { selector: contentSelector });
823
+ }
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@manufac/general-tests",
3
- "version": "0.0.1-alpha",
3
+ "version": "0.0.2",
4
4
  "description": "A library for general tests for landing pages",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/manufac-analytics/sculptor.git",
8
+ "directory": "packages/tests"
9
+ },
5
10
  "license": "UNLICENSED",
6
11
  "exports": {
7
12
  "./*": "./dist/*"
@@ -26,5 +31,8 @@
26
31
  },
27
32
  "peerDependencies": {
28
33
  "@playwright/test": "^1.62.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
29
37
  }
30
38
  }