@testivai/witness 1.0.1 → 1.0.3

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.
Files changed (92) hide show
  1. package/README.md +44 -13
  2. package/dist/baselines/index.d.ts +6 -0
  3. package/dist/baselines/index.d.ts.map +1 -0
  4. package/dist/baselines/index.js +9 -0
  5. package/dist/baselines/index.js.map +1 -0
  6. package/dist/baselines/store.d.ts +100 -0
  7. package/dist/baselines/store.d.ts.map +1 -0
  8. package/dist/baselines/store.js +298 -0
  9. package/dist/baselines/store.js.map +1 -0
  10. package/dist/bin/testivai.js +2 -0
  11. package/dist/bin/testivai.js.map +1 -1
  12. package/dist/browser/binding.d.ts.map +1 -1
  13. package/dist/browser/binding.js +173 -42
  14. package/dist/browser/binding.js.map +1 -1
  15. package/dist/browser/capture.d.ts.map +1 -1
  16. package/dist/browser/capture.js +7 -4
  17. package/dist/browser/capture.js.map +1 -1
  18. package/dist/commands/approve.d.ts +3 -0
  19. package/dist/commands/approve.d.ts.map +1 -0
  20. package/dist/commands/approve.js +94 -0
  21. package/dist/commands/approve.js.map +1 -0
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/init.js +53 -0
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/commands/run.d.ts.map +1 -1
  26. package/dist/commands/run.js +98 -0
  27. package/dist/commands/run.js.map +1 -1
  28. package/dist/config/index.d.ts +6 -0
  29. package/dist/config/index.d.ts.map +1 -0
  30. package/dist/config/index.js +14 -0
  31. package/dist/config/index.js.map +1 -0
  32. package/dist/config/local-config.d.ts +54 -0
  33. package/dist/config/local-config.d.ts.map +1 -0
  34. package/dist/config/local-config.js +130 -0
  35. package/dist/config/local-config.js.map +1 -0
  36. package/dist/diff/diff.d.ts +27 -0
  37. package/dist/diff/diff.d.ts.map +1 -0
  38. package/dist/diff/diff.js +216 -0
  39. package/dist/diff/diff.js.map +1 -0
  40. package/dist/diff/dom-diff.d.ts +85 -0
  41. package/dist/diff/dom-diff.d.ts.map +1 -0
  42. package/dist/diff/dom-diff.js +291 -0
  43. package/dist/diff/dom-diff.js.map +1 -0
  44. package/dist/diff/ignore.d.ts +22 -0
  45. package/dist/diff/ignore.d.ts.map +1 -0
  46. package/dist/diff/ignore.js +43 -0
  47. package/dist/diff/ignore.js.map +1 -0
  48. package/dist/diff/index.d.ts +18 -0
  49. package/dist/diff/index.d.ts.map +1 -0
  50. package/dist/diff/index.js +25 -0
  51. package/dist/diff/index.js.map +1 -0
  52. package/dist/diff/minimap.d.ts +23 -0
  53. package/dist/diff/minimap.d.ts.map +1 -0
  54. package/dist/diff/minimap.js +43 -0
  55. package/dist/diff/minimap.js.map +1 -0
  56. package/dist/diff/regions.d.ts +23 -0
  57. package/dist/diff/regions.d.ts.map +1 -0
  58. package/dist/diff/regions.js +129 -0
  59. package/dist/diff/regions.js.map +1 -0
  60. package/dist/diff/resize.d.ts +27 -0
  61. package/dist/diff/resize.d.ts.map +1 -0
  62. package/dist/diff/resize.js +82 -0
  63. package/dist/diff/resize.js.map +1 -0
  64. package/dist/diff/types.d.ts +79 -0
  65. package/dist/diff/types.d.ts.map +1 -0
  66. package/dist/diff/types.js +10 -0
  67. package/dist/diff/types.js.map +1 -0
  68. package/dist/index.d.ts +8 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +23 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/report/compare.d.ts +18 -0
  73. package/dist/report/compare.d.ts.map +1 -0
  74. package/dist/report/compare.js +193 -0
  75. package/dist/report/compare.js.map +1 -0
  76. package/dist/report/generator.d.ts +31 -0
  77. package/dist/report/generator.d.ts.map +1 -0
  78. package/dist/report/generator.js +123 -0
  79. package/dist/report/generator.js.map +1 -0
  80. package/dist/report/index.d.ts +10 -0
  81. package/dist/report/index.d.ts.map +1 -0
  82. package/dist/report/index.js +13 -0
  83. package/dist/report/index.js.map +1 -0
  84. package/dist/report/results.d.ts +56 -0
  85. package/dist/report/results.d.ts.map +1 -0
  86. package/dist/report/results.js +8 -0
  87. package/dist/report/results.js.map +1 -0
  88. package/dist/report/template.d.ts +14 -0
  89. package/dist/report/template.d.ts.map +1 -0
  90. package/dist/report/template.js +275 -0
  91. package/dist/report/template.js.map +1 -0
  92. package/package.json +58 -15
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * TestivAI Diff Engine — Core comparison loop
4
+ *
5
+ * Originally forked from pixel-buffer-diff (Pbd) by @p01, licensed under MIT.
6
+ * https://github.com/p01/pixel-buffer-diff
7
+ *
8
+ * The core pixel comparison algorithm is preserved exactly.
9
+ * Extended with: ignore regions, size mismatch handling, region detection,
10
+ * and enriched DiffResult.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.diff = diff;
14
+ const ignore_1 = require("./ignore");
15
+ const resize_1 = require("./resize");
16
+ const regions_1 = require("./regions");
17
+ const minimap_1 = require("./minimap");
18
+ // ── Pbd constants (unchanged) ───────────────────────────────────────────────
19
+ const MINIMAP_SCALE = 128;
20
+ const COLOR32_ADDED = 0x03f00cc00;
21
+ const COLOR32_REMOVED = 0x03f0000ff;
22
+ const COLOR32_MINIMAP = 0x0207f0000;
23
+ const HASH_SPREAD = 0x0f0731337;
24
+ // ── Default options ─────────────────────────────────────────────────────────
25
+ const DEFAULT_OPTIONS = {
26
+ threshold: 0.1,
27
+ cumulatedThreshold: 0.5,
28
+ enableMinimap: false,
29
+ detectRegions: false,
30
+ };
31
+ /**
32
+ * Compare two pixel buffers and produce a diff image.
33
+ *
34
+ * Drop-in compatible with pixelmatch when called as:
35
+ * diff(img1, img2, output, w, h) → returns DiffResult
36
+ *
37
+ * @param baseline8 - Baseline image pixel data (RGBA, 4 bytes per pixel)
38
+ * @param candidate8 - Candidate image pixel data (RGBA, 4 bytes per pixel)
39
+ * @param diff8 - Output buffer for the diff image (same size as inputs)
40
+ * @param width - Width of the images in pixels
41
+ * @param height - Height of the images in pixels
42
+ * @param options - Comparison options
43
+ * @returns DiffResult with counts, percentages, and optional regions
44
+ */
45
+ function diff(baseline8, candidate8, diff8, width, height, options = {}) {
46
+ const opts = { ...DEFAULT_OPTIONS, ...options };
47
+ // ── Size mismatch handling ──────────────────────────────────────────────
48
+ let sizeMismatch;
49
+ let effectiveWidth = width;
50
+ let effectiveHeight = height;
51
+ let effectiveBaseline = baseline8;
52
+ let effectiveCandidate = candidate8;
53
+ // If caller provided handleSizeMismatch and dimensions were embedded,
54
+ // we need external width/height per buffer. For the simple API the caller
55
+ // is expected to have already normalized — but we expose normalizeDimensions
56
+ // as a public utility. Here we just validate.
57
+ const expectedLen = width * height * 4;
58
+ if (baseline8.length !== expectedLen || candidate8.length !== expectedLen) {
59
+ if (opts.handleSizeMismatch) {
60
+ // Infer baseline dimensions from baseline8.length and supplied width
61
+ const baselineHeight = baseline8.length / (width * 4);
62
+ const candidateHeight = candidate8.length / (width * 4);
63
+ // If widths differ, we need both dimensions — use normalizeDimensions
64
+ const bw = width;
65
+ const bh = Math.round(baselineHeight);
66
+ const cw = width;
67
+ const ch = Math.round(candidateHeight);
68
+ const normalized = (0, resize_1.normalizeDimensions)(baseline8, bw, bh, candidate8, cw, ch, opts.handleSizeMismatch);
69
+ effectiveBaseline = normalized.normalizedBaseline;
70
+ effectiveCandidate = normalized.normalizedCandidate;
71
+ effectiveWidth = normalized.width;
72
+ effectiveHeight = normalized.height;
73
+ sizeMismatch = normalized.sizeMismatch;
74
+ // Re-create diff buffer if size changed
75
+ if (diff8.length !== effectiveWidth * effectiveHeight * 4) {
76
+ diff8 = new Uint8ClampedArray(effectiveWidth * effectiveHeight * 4);
77
+ }
78
+ }
79
+ else {
80
+ throw new Error(`Buffer size mismatch: expected ${expectedLen} bytes (${width}×${height}×4), ` +
81
+ `got baseline=${baseline8.length}, candidate=${candidate8.length}. ` +
82
+ `Set handleSizeMismatch option to 'pad', 'crop', or 'error'.`);
83
+ }
84
+ }
85
+ // ── Ignore regions ──────────────────────────────────────────────────────
86
+ if (opts.ignoreRegions && opts.ignoreRegions.length > 0) {
87
+ // Clone buffers so we don't mutate originals
88
+ effectiveBaseline = new Uint8ClampedArray(effectiveBaseline);
89
+ effectiveCandidate = new Uint8ClampedArray(effectiveCandidate);
90
+ (0, ignore_1.applyIgnoreRegions)(effectiveBaseline, effectiveCandidate, effectiveWidth, effectiveHeight, opts.ignoreRegions);
91
+ }
92
+ // ── Core Pbd diff loop (basic mode — pixel-level YIQ comparison) ──────
93
+ const raw = pbdDiffCore(effectiveBaseline, effectiveCandidate, diff8, effectiveWidth, effectiveHeight, opts.threshold, opts.cumulatedThreshold, opts.enableMinimap);
94
+ // ── Compute extended result ─────────────────────────────────────────────
95
+ const totalPixels = effectiveWidth * effectiveHeight;
96
+ const diffPercent = totalPixels > 0 ? (raw.diff / totalPixels) * 100 : 0;
97
+ const isIdentical = raw.diff === 0;
98
+ const result = {
99
+ diffCount: raw.diff,
100
+ cumulatedDiff: raw.cumulatedDiff,
101
+ hash: raw.hash,
102
+ diffPercent,
103
+ totalPixels,
104
+ isIdentical,
105
+ };
106
+ if (sizeMismatch) {
107
+ result.sizeMismatch = sizeMismatch;
108
+ }
109
+ // ── Region detection ────────────────────────────────────────────────────
110
+ if (opts.detectRegions && !isIdentical) {
111
+ result.regions = (0, regions_1.detectRegions)(diff8, effectiveWidth, effectiveHeight);
112
+ }
113
+ return result;
114
+ }
115
+ /**
116
+ * Core Pbd diff loop — pixel-level YIQ comparison.
117
+ * Algorithm is preserved exactly from pixel-buffer-diff.
118
+ * @internal
119
+ */
120
+ function pbdDiffCore(baseline8, candidate8, diff8, width, height, threshold, cumulatedThreshold, enableMinimap) {
121
+ const area = width * height;
122
+ const b8l = baseline8.length;
123
+ // Maximum acceptable square distance between two colors;
124
+ // 35215 is the maximum possible value for the YIQ difference metric
125
+ const deltaThreshold = threshold * threshold * 35215;
126
+ // Use ArrayBuffer views for 32-bit access
127
+ const bBuffer = baseline8.buffer;
128
+ const bOffset = baseline8.byteOffset;
129
+ const cBuffer = candidate8.buffer;
130
+ const cOffset = candidate8.byteOffset;
131
+ const dBuffer = diff8.buffer;
132
+ const dOffset = diff8.byteOffset;
133
+ const baseline32 = new Uint32Array(bBuffer, bOffset, area);
134
+ const candidate32 = new Uint32Array(cBuffer, cOffset, area);
135
+ const diff32 = new Uint32Array(dBuffer, dOffset, area);
136
+ let b8i = 0;
137
+ let b32i = 0;
138
+ let d32i = 0;
139
+ let diffCount = 0;
140
+ let hash = 0;
141
+ let hashStart = 0;
142
+ let cumulatedDiff = 0;
143
+ // Quick approx of color theme to figure if "new" pixels should be dark or light
144
+ let averageBrightness = 0;
145
+ const brightnessSamples = Math.ceil(Math.sqrt(area) / 128);
146
+ const b8iStep = (b8l / brightnessSamples) & -4;
147
+ let sampleIdx = 0;
148
+ for (let i = 0; i < brightnessSamples; i++) {
149
+ averageBrightness +=
150
+ (0.299 * (baseline8[sampleIdx] + candidate8[sampleIdx]) +
151
+ 0.587 * (baseline8[sampleIdx + 1] + candidate8[sampleIdx + 1]) +
152
+ 0.114 * (baseline8[sampleIdx + 2] + candidate8[sampleIdx + 2])) /
153
+ brightnessSamples /
154
+ 2;
155
+ sampleIdx += b8iStep;
156
+ }
157
+ const isDarkTheme = averageBrightness < 128;
158
+ const color32Added = isDarkTheme ? COLOR32_ADDED : COLOR32_REMOVED;
159
+ const color32Removed = isDarkTheme ? COLOR32_REMOVED : COLOR32_ADDED;
160
+ // Minimap tracking
161
+ const miniHeight = Math.ceil(height / MINIMAP_SCALE);
162
+ const miniWidth = Math.ceil(width / MINIMAP_SCALE);
163
+ const miniMap = new Uint8ClampedArray(miniWidth * miniHeight);
164
+ const maxDimension = Math.max(width, height);
165
+ const maxMiniDimension = Math.max(miniWidth, miniHeight);
166
+ const axisMiniIndex = new Uint32Array(maxDimension);
167
+ let miniIndex = 0;
168
+ for (let i = 0; i < maxMiniDimension; i++) {
169
+ axisMiniIndex.fill(i, miniIndex, Math.min(miniIndex + MINIMAP_SCALE, maxDimension));
170
+ miniIndex += MINIMAP_SCALE;
171
+ }
172
+ // ── Per-pixel comparison ────────────────────────────────────────────────
173
+ for (let y = 0; y < height; y++) {
174
+ const miniIndexY = axisMiniIndex[y] * miniWidth;
175
+ let hashIndex = (y ^ HASH_SPREAD) * HASH_SPREAD;
176
+ for (let x = 0; x < width; x++, d32i++, b32i++, b8i += 4, hashIndex++) {
177
+ // Quick check against the Uint32
178
+ if (baseline32[b32i] === candidate32[b32i]) {
179
+ continue;
180
+ }
181
+ // Get the r,g,b -> y,i,q => YIQ square delta
182
+ const dr = candidate8[b8i] - baseline8[b8i];
183
+ const dg = candidate8[b8i + 1] - baseline8[b8i + 1];
184
+ const db = candidate8[b8i + 2] - baseline8[b8i + 2];
185
+ const dy = dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
186
+ const di = dr * 0.59597799 - dg * 0.27417610 - db * 0.32180189;
187
+ const dq = dr * 0.21147017 - dg * 0.52261711 + db * 0.31114694;
188
+ const delta = dy * dy * 0.5053 + di * di * 0.299 + dq * dq * 0.1957;
189
+ if (delta > deltaThreshold) {
190
+ miniMap[miniIndexY + axisMiniIndex[x]]++;
191
+ diffCount++;
192
+ const dyAbs = Math.abs(dy);
193
+ cumulatedDiff += dyAbs;
194
+ diff32[d32i] =
195
+ (dy > 0 ? color32Added : color32Removed) +
196
+ (Math.min(192, dyAbs * 8) << 24);
197
+ if (hash === 0) {
198
+ hashStart = hashIndex;
199
+ }
200
+ hash += hashIndex;
201
+ }
202
+ }
203
+ }
204
+ hash -= hashStart;
205
+ cumulatedDiff /= 256;
206
+ // Apply minimap overlay
207
+ if (enableMinimap) {
208
+ (0, minimap_1.applyMinimap)(diff32, miniMap, miniWidth, miniHeight, width, height, MINIMAP_SCALE, COLOR32_MINIMAP);
209
+ }
210
+ // If cumulated diff is within threshold, treat as identical
211
+ if (cumulatedDiff <= cumulatedThreshold) {
212
+ return { diff: 0, cumulatedDiff: 0, hash: 0 };
213
+ }
214
+ return { diff: diffCount, cumulatedDiff, hash };
215
+ }
216
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/diff/diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * TestivAI DOM Diff — lightweight noise-hint comparison
3
+ *
4
+ * Compares two HTML strings (typically `document.documentElement.outerHTML`
5
+ * captured at snapshot time) and returns a small structural-change summary.
6
+ *
7
+ * This is intentionally NOT a full DOM tree differ — those bring 30 KB+ of
8
+ * dependencies and complex semantics we don't need. The job here is to
9
+ * answer one question per snapshot:
10
+ *
11
+ * "When pixels differ, is the underlying DOM also different?"
12
+ *
13
+ * If pixels differ but DOM is unchanged, the difference is almost certainly
14
+ * render noise (font rendering, anti-aliasing, sub-pixel layout). The HTML
15
+ * report uses this as a noise hint.
16
+ *
17
+ * Approach (zero-dep, ~120 LOC of tokenizer):
18
+ * 1. Stream-tokenize each HTML into a flat list of open events
19
+ * (`{ tag, attrs: Record<string, string> }`).
20
+ * 2. Compare via multiset operations:
21
+ * - tags only in candidate → added
22
+ * - tags only in baseline → removed
23
+ * - same tag pairs that differ in attributes → attributeChanges
24
+ *
25
+ * Tradeoffs:
26
+ * - We treat element ORDER as soft. Two pages with the same elements in
27
+ * a different order register as zero diff in tag counts but may show up
28
+ * as attribute changes if reordering changes attributes (uncommon).
29
+ * This is acceptable because the SAME page captured twice should
30
+ * normally produce identical token lists.
31
+ * - Text content is ignored. Visual regression on text wording is what
32
+ * pixels are for — we don't want to flag every dynamic timestamp.
33
+ * - Comments and processing instructions are ignored.
34
+ *
35
+ * Self-closing void elements per HTML spec are handled.
36
+ */
37
+ /** Result of a DOM diff comparison. */
38
+ export interface DomDiffResult {
39
+ /** True if the two HTML strings are structurally different. */
40
+ domChanged: boolean;
41
+ /** Per-bucket counts; null when domChanged is false. */
42
+ summary: DomDiffSummary | null;
43
+ }
44
+ export interface DomDiffSummary {
45
+ /** Tag occurrences present in candidate but not baseline. */
46
+ added: number;
47
+ /** Tag occurrences present in baseline but not candidate. */
48
+ removed: number;
49
+ /** Tag pairs present in both but with different attributes. */
50
+ attributeChanges: number;
51
+ }
52
+ interface OpenEvent {
53
+ tag: string;
54
+ /** Attributes serialized as `key=value` pairs, sorted, joined by '\0'. */
55
+ attrSig: string;
56
+ }
57
+ /**
58
+ * Compare two HTML strings.
59
+ *
60
+ * Returns `{ domChanged: false, summary: null }` for fast paths:
61
+ * - Both inputs identical bytes
62
+ * - Both inputs missing/empty (e.g., DOM capture wasn't enabled)
63
+ *
64
+ * Otherwise tokenizes both and produces a summary.
65
+ */
66
+ export declare function domDiff(baselineHtml: string | null | undefined, candidateHtml: string | null | undefined): DomDiffResult;
67
+ /**
68
+ * Stream-tokenize HTML into open events.
69
+ *
70
+ * Skips: doctype, comments, processing instructions, CDATA, text nodes,
71
+ * script/style content (handled as opaque). Close tags are dropped.
72
+ *
73
+ * Robust against malformed HTML — we never throw; on weird input we
74
+ * just stop emitting events. Worst case = false "no DOM signal".
75
+ */
76
+ declare function tokenize(html: string): OpenEvent[];
77
+ /**
78
+ * Internal export only used in tests; exposes the tokenizer for white-box
79
+ * verification. Not part of the package public API.
80
+ */
81
+ export declare const __internals__: {
82
+ tokenize: typeof tokenize;
83
+ };
84
+ export {};
85
+ //# sourceMappingURL=dom-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom-diff.d.ts","sourceRoot":"","sources":["../../src/diff/dom-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAOH,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,SAAS;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,aAAa,CAyDxH;AAuBD;;;;;;;;GAQG;AACH,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,CAwE3C;AAsFD;;;GAGG;AACH,eAAO,MAAM,aAAa;;CAAe,CAAC"}
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ /**
3
+ * TestivAI DOM Diff — lightweight noise-hint comparison
4
+ *
5
+ * Compares two HTML strings (typically `document.documentElement.outerHTML`
6
+ * captured at snapshot time) and returns a small structural-change summary.
7
+ *
8
+ * This is intentionally NOT a full DOM tree differ — those bring 30 KB+ of
9
+ * dependencies and complex semantics we don't need. The job here is to
10
+ * answer one question per snapshot:
11
+ *
12
+ * "When pixels differ, is the underlying DOM also different?"
13
+ *
14
+ * If pixels differ but DOM is unchanged, the difference is almost certainly
15
+ * render noise (font rendering, anti-aliasing, sub-pixel layout). The HTML
16
+ * report uses this as a noise hint.
17
+ *
18
+ * Approach (zero-dep, ~120 LOC of tokenizer):
19
+ * 1. Stream-tokenize each HTML into a flat list of open events
20
+ * (`{ tag, attrs: Record<string, string> }`).
21
+ * 2. Compare via multiset operations:
22
+ * - tags only in candidate → added
23
+ * - tags only in baseline → removed
24
+ * - same tag pairs that differ in attributes → attributeChanges
25
+ *
26
+ * Tradeoffs:
27
+ * - We treat element ORDER as soft. Two pages with the same elements in
28
+ * a different order register as zero diff in tag counts but may show up
29
+ * as attribute changes if reordering changes attributes (uncommon).
30
+ * This is acceptable because the SAME page captured twice should
31
+ * normally produce identical token lists.
32
+ * - Text content is ignored. Visual regression on text wording is what
33
+ * pixels are for — we don't want to flag every dynamic timestamp.
34
+ * - Comments and processing instructions are ignored.
35
+ *
36
+ * Self-closing void elements per HTML spec are handled.
37
+ */
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.__internals__ = void 0;
40
+ exports.domDiff = domDiff;
41
+ const VOID_ELEMENTS = new Set([
42
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
43
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
44
+ ]);
45
+ /**
46
+ * Compare two HTML strings.
47
+ *
48
+ * Returns `{ domChanged: false, summary: null }` for fast paths:
49
+ * - Both inputs identical bytes
50
+ * - Both inputs missing/empty (e.g., DOM capture wasn't enabled)
51
+ *
52
+ * Otherwise tokenizes both and produces a summary.
53
+ */
54
+ function domDiff(baselineHtml, candidateHtml) {
55
+ // Fast path: missing data on either side → no signal
56
+ if (!baselineHtml || !candidateHtml) {
57
+ return { domChanged: false, summary: null };
58
+ }
59
+ // Fast path: byte-identical
60
+ if (baselineHtml === candidateHtml) {
61
+ return { domChanged: false, summary: null };
62
+ }
63
+ const baselineEvents = tokenize(baselineHtml);
64
+ const candidateEvents = tokenize(candidateHtml);
65
+ // Bucket by tag name; within each bucket, count attrSig multisets
66
+ const baselineBuckets = bucketize(baselineEvents);
67
+ const candidateBuckets = bucketize(candidateEvents);
68
+ let added = 0;
69
+ let removed = 0;
70
+ let attributeChanges = 0;
71
+ const allTags = new Set([...baselineBuckets.keys(), ...candidateBuckets.keys()]);
72
+ for (const tag of allTags) {
73
+ const baselineForTag = baselineBuckets.get(tag) ?? new Map();
74
+ const candidateForTag = candidateBuckets.get(tag) ?? new Map();
75
+ const baselineCount = sumValues(baselineForTag);
76
+ const candidateCount = sumValues(candidateForTag);
77
+ if (candidateCount > baselineCount) {
78
+ added += candidateCount - baselineCount;
79
+ }
80
+ else if (baselineCount > candidateCount) {
81
+ removed += baselineCount - candidateCount;
82
+ }
83
+ // Attribute changes = matching tag pairs whose attrSig differs.
84
+ // We compute by intersecting the two attrSig multisets and counting
85
+ // anything that doesn't match. Each unmatched-but-paired pair counts
86
+ // once toward attributeChanges; raw added/removed of the tag itself
87
+ // is already counted above, so we subtract that delta.
88
+ const overlap = Math.min(baselineCount, candidateCount);
89
+ let matchingPairs = 0;
90
+ for (const [sig, candCount] of candidateForTag) {
91
+ const baseCount = baselineForTag.get(sig) ?? 0;
92
+ matchingPairs += Math.min(baseCount, candCount);
93
+ }
94
+ attributeChanges += overlap - matchingPairs;
95
+ }
96
+ const domChanged = added > 0 || removed > 0 || attributeChanges > 0;
97
+ return {
98
+ domChanged,
99
+ summary: domChanged ? { added, removed, attributeChanges } : null,
100
+ };
101
+ }
102
+ // ── internals ──────────────────────────────────────────────────────────────
103
+ function bucketize(events) {
104
+ const buckets = new Map();
105
+ for (const ev of events) {
106
+ let inner = buckets.get(ev.tag);
107
+ if (!inner) {
108
+ inner = new Map();
109
+ buckets.set(ev.tag, inner);
110
+ }
111
+ inner.set(ev.attrSig, (inner.get(ev.attrSig) ?? 0) + 1);
112
+ }
113
+ return buckets;
114
+ }
115
+ function sumValues(m) {
116
+ let total = 0;
117
+ for (const v of m.values())
118
+ total += v;
119
+ return total;
120
+ }
121
+ /**
122
+ * Stream-tokenize HTML into open events.
123
+ *
124
+ * Skips: doctype, comments, processing instructions, CDATA, text nodes,
125
+ * script/style content (handled as opaque). Close tags are dropped.
126
+ *
127
+ * Robust against malformed HTML — we never throw; on weird input we
128
+ * just stop emitting events. Worst case = false "no DOM signal".
129
+ */
130
+ function tokenize(html) {
131
+ const events = [];
132
+ const len = html.length;
133
+ let i = 0;
134
+ while (i < len) {
135
+ // Find next '<'
136
+ const lt = html.indexOf('<', i);
137
+ if (lt < 0)
138
+ break;
139
+ i = lt + 1;
140
+ if (i >= len)
141
+ break;
142
+ const c = html[i];
143
+ // Comment <!-- ... -->
144
+ if (c === '!' && html.startsWith('!--', i)) {
145
+ const end = html.indexOf('-->', i + 3);
146
+ if (end < 0)
147
+ break;
148
+ i = end + 3;
149
+ continue;
150
+ }
151
+ // Doctype <!doctype ...>
152
+ if (c === '!') {
153
+ const end = html.indexOf('>', i + 1);
154
+ if (end < 0)
155
+ break;
156
+ i = end + 1;
157
+ continue;
158
+ }
159
+ // CDATA <![CDATA[ ... ]]> (rare in HTML; skip)
160
+ // Already handled by the c === '!' branch above.
161
+ // Processing instruction <? ... ?>
162
+ if (c === '?') {
163
+ const end = html.indexOf('?>', i + 1);
164
+ if (end < 0)
165
+ break;
166
+ i = end + 2;
167
+ continue;
168
+ }
169
+ // Close tag </tagname>
170
+ if (c === '/') {
171
+ const end = html.indexOf('>', i + 1);
172
+ if (end < 0)
173
+ break;
174
+ i = end + 1;
175
+ continue;
176
+ }
177
+ // Open tag <tagname ...>
178
+ const tagEnd = findTagEnd(html, i);
179
+ if (tagEnd < 0)
180
+ break;
181
+ const raw = html.slice(i, tagEnd);
182
+ const ev = parseOpenTag(raw);
183
+ if (ev) {
184
+ events.push(ev);
185
+ // For script/style, skip body opaquely until matching close
186
+ if (ev.tag === 'script' || ev.tag === 'style') {
187
+ const closeNeedle = `</${ev.tag}`;
188
+ const closeIdx = indexOfCaseInsensitive(html, closeNeedle, tagEnd + 1);
189
+ if (closeIdx < 0)
190
+ break;
191
+ const gtAfter = html.indexOf('>', closeIdx);
192
+ i = gtAfter < 0 ? len : gtAfter + 1;
193
+ continue;
194
+ }
195
+ }
196
+ i = tagEnd + 1;
197
+ }
198
+ return events;
199
+ }
200
+ /**
201
+ * Find the index of '>' that closes the open tag starting at `start`,
202
+ * handling quoted attribute values which may contain '>'.
203
+ */
204
+ function findTagEnd(html, start) {
205
+ let i = start;
206
+ let inQuote = null;
207
+ const len = html.length;
208
+ while (i < len) {
209
+ const c = html[i];
210
+ if (inQuote) {
211
+ if (c === inQuote)
212
+ inQuote = null;
213
+ }
214
+ else {
215
+ if (c === '"' || c === "'")
216
+ inQuote = c;
217
+ else if (c === '>')
218
+ return i;
219
+ }
220
+ i++;
221
+ }
222
+ return -1;
223
+ }
224
+ /**
225
+ * Parse the raw inside of a `<...>` (excluding the angle brackets) into
226
+ * an OpenEvent. Returns null for the empty / malformed case.
227
+ */
228
+ function parseOpenTag(raw) {
229
+ // Strip trailing '/' for self-closing forms <br/>
230
+ const trimmed = raw.trim().replace(/\/$/, '').trim();
231
+ if (!trimmed)
232
+ return null;
233
+ // First whitespace-or-end → tag name
234
+ const m = /^([a-zA-Z][a-zA-Z0-9-]*)/.exec(trimmed);
235
+ if (!m)
236
+ return null;
237
+ const tag = m[1].toLowerCase();
238
+ let rest = trimmed.slice(m[0].length).trim();
239
+ const attrs = {};
240
+ while (rest.length > 0) {
241
+ const am = /^([a-zA-Z_:][a-zA-Z0-9_:.-]*)/.exec(rest);
242
+ if (!am)
243
+ break;
244
+ const name = am[1].toLowerCase();
245
+ rest = rest.slice(am[0].length);
246
+ let value = '';
247
+ if (rest.startsWith('=')) {
248
+ rest = rest.slice(1).trimStart();
249
+ if (rest.startsWith('"') || rest.startsWith("'")) {
250
+ const quote = rest[0];
251
+ const end = rest.indexOf(quote, 1);
252
+ if (end < 0)
253
+ break;
254
+ value = rest.slice(1, end);
255
+ rest = rest.slice(end + 1);
256
+ }
257
+ else {
258
+ const ws = rest.search(/\s/);
259
+ if (ws < 0) {
260
+ value = rest;
261
+ rest = '';
262
+ }
263
+ else {
264
+ value = rest.slice(0, ws);
265
+ rest = rest.slice(ws);
266
+ }
267
+ }
268
+ }
269
+ attrs[name] = value;
270
+ rest = rest.trimStart();
271
+ }
272
+ // Build a stable, sorted attribute signature.
273
+ // Some attributes are inherently dynamic (id like 'react-aria-:r1:'); we
274
+ // don't try to be smart about that here — the signature is stable across
275
+ // identical pages but flags any attr-value drift.
276
+ const keys = Object.keys(attrs).sort();
277
+ const attrSig = keys.map((k) => `${k}=${attrs[k]}`).join('\0');
278
+ return { tag, attrSig };
279
+ }
280
+ function indexOfCaseInsensitive(haystack, needle, fromIndex) {
281
+ // Cheap case-insensitive search; needle is short ('</script' or '</style')
282
+ const h = haystack.toLowerCase();
283
+ const n = needle.toLowerCase();
284
+ return h.indexOf(n, fromIndex);
285
+ }
286
+ /**
287
+ * Internal export only used in tests; exposes the tokenizer for white-box
288
+ * verification. Not part of the package public API.
289
+ */
290
+ exports.__internals__ = { tokenize };
291
+ //# sourceMappingURL=dom-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom-diff.js","sourceRoot":"","sources":["../../src/diff/dom-diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * TestivAI Diff Engine — Ignore regions
3
+ *
4
+ * Before the diff loop runs, copy candidate pixel values into the
5
+ * baseline buffer for ignored regions. This makes ignored areas
6
+ * appear identical to the differ.
7
+ */
8
+ import { IgnoreRegion } from './types';
9
+ /**
10
+ * Mask ignored regions by copying candidate pixels into baseline.
11
+ * After this, the diff loop will see no difference in those areas.
12
+ *
13
+ * Mutates `baseline` in place.
14
+ *
15
+ * @param baseline - Baseline pixel buffer (will be mutated)
16
+ * @param candidate - Candidate pixel buffer
17
+ * @param width - Image width
18
+ * @param height - Image height
19
+ * @param regions - Regions to ignore
20
+ */
21
+ export declare function applyIgnoreRegions(baseline: Uint8Array | Uint8ClampedArray, candidate: Uint8Array | Uint8ClampedArray, width: number, height: number, regions: IgnoreRegion[]): void;
22
+ //# sourceMappingURL=ignore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ignore.d.ts","sourceRoot":"","sources":["../../src/diff/ignore.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,UAAU,GAAG,iBAAiB,EACxC,SAAS,EAAE,UAAU,GAAG,iBAAiB,EACzC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,YAAY,EAAE,GACtB,IAAI,CAoBN"}