@testivai/witness-playwright 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -138,7 +138,8 @@ function mergeTestConfig(projectConfig, testConfig) {
138
138
  // the selectors (masked for a long time by the diff engine's cumulated
139
139
  // threshold absorbing the tiny leaked pixels).
140
140
  ignoreSelectors: testConfig.ignoreSelectors,
141
- stabilize: testConfig.stabilize
141
+ stabilize: testConfig.stabilize,
142
+ mask: testConfig.mask
142
143
  };
143
144
  }
144
145
  /**
package/dist/snapshot.js CHANGED
@@ -408,6 +408,61 @@ async function snapshot(page, testInfo, name, config) {
408
408
  console.warn('[TestivAI] DOM capture failed (noise-hint will be unavailable):', err);
409
409
  }
410
410
  }
411
+ // Mask support: record per-call mask specs and capture geometry for
412
+ // every selector mask (config + per-call) via getBoundingClientRect.
413
+ // The DOM snapshot carries no layout, so comparison-time selector
414
+ // masks depend on these rects; when absent, the compare degrades to
415
+ // a visible warning. Best-effort — never breaks the capture.
416
+ try {
417
+ const perCallMasks = effectiveConfig.mask ?? [];
418
+ const configMaskEntries = readConfigMasks(process.cwd());
419
+ const selectorMasks = [
420
+ ...configMaskEntries.filter((m) => typeof m === 'string'),
421
+ ...perCallMasks.filter((m) => typeof m === 'string'),
422
+ ];
423
+ const uniqueSelectors = [...new Set(selectorMasks)];
424
+ let maskRects = [];
425
+ if (uniqueSelectors.length > 0) {
426
+ maskRects = await page.evaluate((selectors) => {
427
+ const dpr = window.devicePixelRatio || 1;
428
+ const out = [];
429
+ for (const sel of selectors) {
430
+ try {
431
+ document.querySelectorAll(sel).forEach((el) => {
432
+ const r = el.getBoundingClientRect();
433
+ if (r.width > 0 && r.height > 0) {
434
+ out.push({
435
+ selector: sel,
436
+ x: Math.round((r.x + window.scrollX) * dpr),
437
+ y: Math.round((r.y + window.scrollY) * dpr),
438
+ width: Math.round(r.width * dpr),
439
+ height: Math.round(r.height * dpr),
440
+ });
441
+ }
442
+ });
443
+ }
444
+ catch {
445
+ // invalid selector — the compare side will warn
446
+ }
447
+ }
448
+ return out;
449
+ }, uniqueSelectors);
450
+ }
451
+ const geometricCallMasks = perCallMasks.filter((m) => typeof m !== 'string');
452
+ const callSelectorMasks = perCallMasks.filter((m) => typeof m === 'string');
453
+ if (geometricCallMasks.length > 0 || callSelectorMasks.length > 0 || maskRects.length > 0) {
454
+ await fs.writeJson(path.join(localSnapshotDir, 'metadata.json'), {
455
+ name: snapshotName,
456
+ timestamp: new Date(timestamp).toISOString(),
457
+ ...(geometricCallMasks.length > 0 ? { masks: geometricCallMasks } : {}),
458
+ ...(callSelectorMasks.length > 0 ? { maskSelectors: callSelectorMasks } : {}),
459
+ ...(maskRects.length > 0 ? { maskRects } : {}),
460
+ });
461
+ }
462
+ }
463
+ catch {
464
+ // metadata is an enhancement; the screenshot path never depends on it
465
+ }
411
466
  }
412
467
  // 2. Dump page structure (HTML) - skip in local mode
413
468
  // @renamed: domPath → structurePath (IP protection)
@@ -709,3 +764,17 @@ async function snapshot(page, testInfo, name, config) {
709
764
  structureAnalysis
710
765
  });
711
766
  }
767
+ /**
768
+ * Read the global `mask` list from .testivai/config.json (selector strings
769
+ * and geometric objects). Returns [] when absent or malformed.
770
+ */
771
+ function readConfigMasks(projectRoot) {
772
+ try {
773
+ const raw = fs.readFileSync(path.join(projectRoot, '.testivai', 'config.json'), 'utf-8');
774
+ const parsed = JSON.parse(raw);
775
+ return Array.isArray(parsed?.mask) ? parsed.mask : [];
776
+ }
777
+ catch {
778
+ return [];
779
+ }
780
+ }
package/dist/types.d.ts CHANGED
@@ -239,6 +239,18 @@ export interface TestivAIConfig {
239
239
  * or in `testivai.config.ts`. Per-snapshot values win.
240
240
  */
241
241
  stabilize?: boolean;
242
+ /**
243
+ * Masks for this snapshot: areas excluded from the pixel diff and
244
+ * hatched in the report (auditable — never silent). Each entry is a
245
+ * CSS selector string (geometry recorded at capture time) or a
246
+ * geometric region — px numbers, 0–1 ratios, "NN%" strings, or a
247
+ * single-edge shorthand like { top: 24 }.
248
+ *
249
+ * Merged with the global `mask` list from `.testivai/config.json`.
250
+ * Unlike `ignoreSelectors` (which hides elements during capture),
251
+ * masks act at comparison time and stay visible in the diff output.
252
+ */
253
+ mask?: Array<string | Record<string, number | string>>;
242
254
  }
243
255
  /**
244
256
  * Layout/Bounding box data for an element
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testivai/witness-playwright",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Playwright sensor for Testivai Visual Regression Test system",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,7 +48,7 @@
48
48
  "sharp": "^0.34.5",
49
49
  "simple-git": "^3.21.0",
50
50
  "@testivai/common": "0.2.2",
51
- "@testivai/witness": "1.3.0"
51
+ "@testivai/witness": "1.4.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@playwright/test": "^1.60.0",