@testivai/witness 1.11.1 → 1.13.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.
Files changed (38) hide show
  1. package/README.md +17 -30
  2. package/dist/bin/testivai.js +2 -0
  3. package/dist/bin/testivai.js.map +1 -1
  4. package/dist/capture/element-map.d.ts +74 -0
  5. package/dist/capture/element-map.d.ts.map +1 -0
  6. package/dist/capture/element-map.js +153 -0
  7. package/dist/capture/element-map.js.map +1 -0
  8. package/dist/capture/settle.d.ts +78 -0
  9. package/dist/capture/settle.d.ts.map +1 -0
  10. package/dist/capture/settle.js +117 -0
  11. package/dist/capture/settle.js.map +1 -0
  12. package/dist/capture/shard.d.ts +51 -0
  13. package/dist/capture/shard.d.ts.map +1 -0
  14. package/dist/capture/shard.js +113 -0
  15. package/dist/capture/shard.js.map +1 -0
  16. package/dist/commands/auth.d.ts.map +1 -1
  17. package/dist/commands/auth.js +0 -2
  18. package/dist/commands/auth.js.map +1 -1
  19. package/dist/commands/exit-codes.d.ts +4 -4
  20. package/dist/commands/init.d.ts.map +1 -1
  21. package/dist/commands/init.js +22 -24
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/merge-captures.d.ts +65 -0
  24. package/dist/commands/merge-captures.d.ts.map +1 -0
  25. package/dist/commands/merge-captures.js +233 -0
  26. package/dist/commands/merge-captures.js.map +1 -0
  27. package/dist/commands/report.d.ts +5 -1
  28. package/dist/commands/report.d.ts.map +1 -1
  29. package/dist/commands/report.js +5 -1
  30. package/dist/commands/report.js.map +1 -1
  31. package/dist/commands/run.d.ts.map +1 -1
  32. package/dist/commands/run.js +0 -1
  33. package/dist/commands/run.js.map +1 -1
  34. package/dist/index.d.ts +6 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +21 -1
  37. package/dist/index.js.map +1 -1
  38. package/package.json +9 -6
package/README.md CHANGED
@@ -12,14 +12,13 @@ The TestivAI Witness SDK allows you to integrate visual regression testing into
12
12
  npm install -D @testivai/witness
13
13
  ```
14
14
 
15
- ## Quick Start (Local Mode — Free)
15
+ ## Quick Start
16
16
 
17
- TestivAI works in **local mode** without any API key or cloud account:
17
+ TestivAI runs entirely on your machine — no account, no server, nothing uploaded:
18
18
 
19
19
  1. **Initialize your project**
20
20
  ```bash
21
21
  npx testivai init
22
- # Select "Local Mode" when prompted
23
22
  ```
24
23
 
25
24
  2. **Add visual captures to your tests**
@@ -45,17 +44,12 @@ TestivAI works in **local mode** without any API key or cloud account:
45
44
  npx testivai approve --all
46
45
  ```
47
46
 
48
- ## Quick Start (Cloud Mode)
47
+ ## Sharing results with your team
49
48
 
50
- For team collaboration and hosted dashboards:
49
+ Everything stays on your machine. To get a diff in front of a teammate:
51
50
 
52
- 1. **Get an API key** at [testiv.ai](https://testiv.ai)
53
- 2. **Set your API key**
54
- ```bash
55
- export TESTIVAI_API_KEY=your-api-key
56
- ```
57
- 3. Follow steps 1–3 from local mode above
58
- 4. Results will upload to the cloud dashboard automatically
51
+ - **In a PR** — the [`mcbuddy/testivai-oss` action](https://github.com/mcbuddy/testivai-oss) posts the summary as a PR comment and uploads the report as a build artifact. Approve from the PR with a `/testivai approve` comment.
52
+ - **As a single file** — `npx testivai report --share` writes `visual-report/share.html` with every image inlined, so it works over chat or email with no server. Set `shareUploadCommand` in `.testivai/config.json` to pipe it to your own storage (S3, GCS, or anything with a CLI).
59
53
 
60
54
  ## Framework Integration
61
55
 
@@ -159,7 +153,7 @@ testivai run "npx wdio"
159
153
  - Page structure extraction
160
154
  - Layout analysis
161
155
  - Performance metrics
162
- 5. **Batch Upload**: All captures are batched and uploaded to TestivAI for analysis
156
+ 5. **Compare & report**: Captures are diffed against `.testivai/baselines/` and written to `visual-report/` (HTML + `results.json`)
163
157
 
164
158
  ## Configuration
165
159
 
@@ -169,12 +163,6 @@ Create a `testivai.config.ts` file in your project root:
169
163
  import type { WitnessConfig } from '@testivai/witness';
170
164
 
171
165
  const config: WitnessConfig = {
172
- // API key (set via TESTIVAI_API_KEY environment variable)
173
- // apiKey: 'your-api-key-here',
174
-
175
- // Project ID from TestivAI dashboard
176
- // projectId: 'your-project-id-here',
177
-
178
166
  // Chrome remote debugging port
179
167
  browserPort: 9222,
180
168
 
@@ -204,8 +192,9 @@ export default config;
204
192
  ### `npx testivai init`
205
193
  Initialize TestivAI in your project. Detects your framework and provides setup instructions.
206
194
 
207
- ### `npx testivai auth <api-key>`
208
- Authenticate with your TestivAI API key. Get your key from the [dashboard](https://dashboard.testiv.ai).
195
+ ### `npx testivai auth`
196
+ Deprecated. TestivAI runs fully locally — there is no account or API key. The command
197
+ now only prints a notice and exits.
209
198
 
210
199
  ### `npx testivai run <command>`
211
200
  Run your test command with automatic visual capture.
@@ -306,12 +295,12 @@ jobs:
306
295
  --disable-dev-shm-usage \
307
296
  --headless \
308
297
  --disable-gpu &
309
-
310
- - name: Authenticate
311
- run: testivai auth ${{ secrets.TESTIVAI_API_KEY }}
312
-
298
+
313
299
  - name: Run visual tests
314
300
  run: testivai run "npm test"
301
+
302
+ - name: Visual diff gate
303
+ run: npx testivai report --fail-on-diff
315
304
  ```
316
305
 
317
306
  ### Jenkins
@@ -342,10 +331,8 @@ pipeline {
342
331
 
343
332
  stage('Visual Tests') {
344
333
  steps {
345
- withCredentials([string(credentialsId: 'testivai-api-key', variable: 'API_KEY')]) {
346
- sh 'testivai auth $API_KEY'
347
- sh 'testivai run "npm test"'
348
- }
334
+ sh 'testivai run "npm test"'
335
+ sh 'npx testivai report --fail-on-diff'
349
336
  }
350
337
  }
351
338
  }
@@ -388,7 +375,7 @@ chrome --remote-debugging-port=9222
388
375
  - **Package size**: ~270KB (no browser binaries included)
389
376
  - **Memory usage**: ~50MB additional overhead
390
377
  - **Capture time**: ~100-500ms per snapshot
391
- - **Upload time**: Depends on network and snapshot size
378
+ - **Compare time**: ~50-200ms per snapshot (pixel + DOM diff)
392
379
 
393
380
  ## API Reference
394
381
 
@@ -11,6 +11,7 @@ const init_1 = require("../commands/init");
11
11
  const run_1 = require("../commands/run");
12
12
  const capture_1 = require("../commands/capture");
13
13
  const approve_1 = require("../commands/approve");
14
+ const merge_captures_1 = require("../commands/merge-captures");
14
15
  const report_1 = require("../commands/report");
15
16
  const packageJson = require('../../package.json');
16
17
  const program = new commander_1.Command();
@@ -46,6 +47,7 @@ program.addCommand(init_1.initCommand);
46
47
  program.addCommand(run_1.runCommand);
47
48
  program.addCommand(capture_1.witnessCommand);
48
49
  program.addCommand(approve_1.approveCommand);
50
+ program.addCommand(merge_captures_1.mergeCapturesCommand);
49
51
  program.addCommand(report_1.reportCommand);
50
52
  // Parse arguments
51
53
  program.parse();
@@ -1 +1 @@
1
- {"version":3,"file":"testivai.js","sourceRoot":"","sources":["../../src/bin/testivai.ts"],"names":[],"mappings":";;;;;;AAEA,yCAAoC;AACpC,kDAA0B;AAC1B,2CAA+C;AAC/C,2CAA+C;AAC/C,yCAA6C;AAC7C,iDAAqD;AACrD,iDAAqD;AACrD,+CAAmD;AAEnD,MAAM,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAElD,MAAM,OAAO,GAAG,IAAI,mBAAO,EAAE,CAAC;AAE9B,iBAAiB;AACjB,MAAM,UAAU,GAAG,GAAG,EAAE;IACtB,OAAO,CAAC,GAAG,EAAE,CAAC;IACd,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAC3C,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,EAAE,CAAC;AAChB,CAAC,CAAC;AAEF,OAAO;KACJ,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CAAC,qEAAqE,CAAC;KAClF,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,eAAe,EAAE,wBAAwB,CAAC;KACvE,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE;IACtB,wEAAwE;IACxE,kEAAkE;IAClE,MAAM,KAAK,GACT,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAChC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,UAAU,EAAE,CAAC;IACf,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,iBAAiB;AACjB,OAAO;KACJ,MAAM,CAAC,eAAe,EAAE,uBAAuB,CAAC;KAChD,MAAM,CAAC,aAAa,EAAE,gCAAgC,CAAC;KACvD,MAAM,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;AAE1C,eAAe;AACf,OAAO,CAAC,UAAU,CAAC,kBAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,kBAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,gBAAU,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,wBAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,wBAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,sBAAa,CAAC,CAAC;AAElC,kBAAkB;AAClB,OAAO,CAAC,KAAK,EAAE,CAAC;AAEhB,mCAAmC;AACnC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAClC,OAAO,CAAC,UAAU,EAAE,CAAC;AACvB,CAAC"}
1
+ {"version":3,"file":"testivai.js","sourceRoot":"","sources":["../../src/bin/testivai.ts"],"names":[],"mappings":";;;;;;AAEA,yCAAoC;AACpC,kDAA0B;AAC1B,2CAA+C;AAC/C,2CAA+C;AAC/C,yCAA6C;AAC7C,iDAAqD;AACrD,iDAAqD;AACrD,+DAAkE;AAClE,+CAAmD;AAEnD,MAAM,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAElD,MAAM,OAAO,GAAG,IAAI,mBAAO,EAAE,CAAC;AAE9B,iBAAiB;AACjB,MAAM,UAAU,GAAG,GAAG,EAAE;IACtB,OAAO,CAAC,GAAG,EAAE,CAAC;IACd,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAC3C,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,EAAE,CAAC;AAChB,CAAC,CAAC;AAEF,OAAO;KACJ,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CAAC,qEAAqE,CAAC;KAClF,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,eAAe,EAAE,wBAAwB,CAAC;KACvE,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE;IACtB,wEAAwE;IACxE,kEAAkE;IAClE,MAAM,KAAK,GACT,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAChC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,UAAU,EAAE,CAAC;IACf,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,iBAAiB;AACjB,OAAO;KACJ,MAAM,CAAC,eAAe,EAAE,uBAAuB,CAAC;KAChD,MAAM,CAAC,aAAa,EAAE,gCAAgC,CAAC;KACvD,MAAM,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;AAE1C,eAAe;AACf,OAAO,CAAC,UAAU,CAAC,kBAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,kBAAW,CAAC,CAAC;AAChC,OAAO,CAAC,UAAU,CAAC,gBAAU,CAAC,CAAC;AAC/B,OAAO,CAAC,UAAU,CAAC,wBAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,wBAAc,CAAC,CAAC;AACnC,OAAO,CAAC,UAAU,CAAC,qCAAoB,CAAC,CAAC;AACzC,OAAO,CAAC,UAAU,CAAC,sBAAa,CAAC,CAAC;AAElC,kBAAkB;AAClB,OAAO,CAAC,KAAK,EAAE,CAAC;AAEhB,mCAAmC;AACnC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAClC,OAAO,CAAC,UAAU,EAAE,CAAC;AACvB,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Element-map capture — the layout + computed-style digest that powers
3
+ * element attribution, shift classification, and the style fingerprint
4
+ * on the comparison side (@testivai/witness).
5
+ *
6
+ * `collectElementMap` is a SELF-CONTAINED function (no imports, no
7
+ * closures) so it can be serialized into the page with
8
+ * `(${collectElementMap.toString()})(document, window, max)` — and unit
9
+ * tested in Node against a duck-typed DOM. Keep it dependency-free.
10
+ */
11
+ /**
12
+ * Minimal structural types for the DOM surface this collector touches.
13
+ *
14
+ * This module lives in `@testivai/witness`, which compiles for Node and
15
+ * deliberately has no `dom` lib — a Node package that can reference
16
+ * `window` by accident is a footgun. The collector is injected into a
17
+ * page as a string and unit-tested against a duck-typed DOM, so
18
+ * structural types are both sufficient and closer to how it is used.
19
+ */
20
+ export interface ElementLike {
21
+ tagName: string;
22
+ classList: {
23
+ length: number;
24
+ [index: number]: string;
25
+ };
26
+ children: ArrayLike<ElementLike>;
27
+ parentElement: ElementLike | null;
28
+ getBoundingClientRect(): {
29
+ x: number;
30
+ y: number;
31
+ width: number;
32
+ height: number;
33
+ };
34
+ matches?(selector: string): boolean;
35
+ }
36
+ export interface DocumentLike {
37
+ body: ElementLike;
38
+ }
39
+ export interface WindowLike {
40
+ devicePixelRatio: number;
41
+ scrollX: number;
42
+ scrollY: number;
43
+ getComputedStyle(el: ElementLike): {
44
+ getPropertyValue(prop: string): string;
45
+ };
46
+ }
47
+ export interface CollectedElement {
48
+ path: string;
49
+ x: number;
50
+ y: number;
51
+ width: number;
52
+ height: number;
53
+ styleHash: string;
54
+ }
55
+ /**
56
+ * Walk the document and produce [{path, rect, styleHash}] in document
57
+ * coordinates (DPR-scaled to match full-page screenshot pixels).
58
+ *
59
+ * - `path` is deterministic: tag + first class + :nth-of-type when the
60
+ * element has same-tag element siblings.
61
+ * - Elements smaller than 4×4 px and invisible elements are skipped.
62
+ * - The walk is depth-first and capped at `maxElements`.
63
+ * - `styleHash` digests a fixed, documented list of computed properties
64
+ * (FNV-1a 32-bit, hex) — deterministic for identical rendering.
65
+ */
66
+ export declare function collectElementMap(doc: DocumentLike, win: WindowLike, maxElements: number, ignoreSelectors?: string[]): CollectedElement[];
67
+ /** Default element cap — bounds capture time and elements.json size. */
68
+ export declare const DEFAULT_MAX_ELEMENTS = 3000;
69
+ /**
70
+ * The page-side expression evaluated by the adapter. Serializes the
71
+ * collector so page code and unit-tested code are the same function.
72
+ */
73
+ export declare function buildElementMapExpression(maxElements?: number, ignoreSelectors?: string[]): string;
74
+ //# sourceMappingURL=element-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"element-map.d.ts","sourceRoot":"","sources":["../../src/capture/element-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvD,QAAQ,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IACjC,aAAa,EAAE,WAAW,GAAG,IAAI,CAAC;IAClC,qBAAqB,IAAI;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACjF,OAAO,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,EAAE,WAAW,GAAG;QAAE,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CAC/E;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,UAAU,EACf,WAAW,EAAE,MAAM,EACnB,eAAe,GAAE,MAAM,EAAO,GAC7B,gBAAgB,EAAE,CAiHpB;AAED,wEAAwE;AACxE,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,GAAE,MAA6B,EAC1C,eAAe,GAAE,MAAM,EAAO,GAC7B,MAAM,CAER"}
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ /**
3
+ * Element-map capture — the layout + computed-style digest that powers
4
+ * element attribution, shift classification, and the style fingerprint
5
+ * on the comparison side (@testivai/witness).
6
+ *
7
+ * `collectElementMap` is a SELF-CONTAINED function (no imports, no
8
+ * closures) so it can be serialized into the page with
9
+ * `(${collectElementMap.toString()})(document, window, max)` — and unit
10
+ * tested in Node against a duck-typed DOM. Keep it dependency-free.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.DEFAULT_MAX_ELEMENTS = void 0;
14
+ exports.collectElementMap = collectElementMap;
15
+ exports.buildElementMapExpression = buildElementMapExpression;
16
+ /**
17
+ * Walk the document and produce [{path, rect, styleHash}] in document
18
+ * coordinates (DPR-scaled to match full-page screenshot pixels).
19
+ *
20
+ * - `path` is deterministic: tag + first class + :nth-of-type when the
21
+ * element has same-tag element siblings.
22
+ * - Elements smaller than 4×4 px and invisible elements are skipped.
23
+ * - The walk is depth-first and capped at `maxElements`.
24
+ * - `styleHash` digests a fixed, documented list of computed properties
25
+ * (FNV-1a 32-bit, hex) — deterministic for identical rendering.
26
+ */
27
+ function collectElementMap(doc, win, maxElements, ignoreSelectors = []) {
28
+ var STYLE_PROPS = [
29
+ 'color', 'background-color', 'background-image',
30
+ 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
31
+ 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
32
+ 'border-top-style', 'border-bottom-style', 'border-radius',
33
+ 'font-family', 'font-size', 'font-weight', 'font-style', 'line-height',
34
+ 'text-align', 'text-transform', 'letter-spacing',
35
+ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
36
+ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
37
+ 'display', 'opacity', 'visibility', 'box-shadow',
38
+ ];
39
+ function fnv1a(str) {
40
+ var h = 0x811c9dc5;
41
+ for (var i = 0; i < str.length; i++) {
42
+ h ^= str.charCodeAt(i);
43
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
44
+ }
45
+ return ('0000000' + h.toString(16)).slice(-8);
46
+ }
47
+ function segment(el) {
48
+ var tag = el.tagName.toLowerCase();
49
+ var cls = '';
50
+ if (el.classList && el.classList.length > 0) {
51
+ cls = '.' + el.classList[0];
52
+ }
53
+ var parent = el.parentElement;
54
+ if (!parent)
55
+ return tag + cls;
56
+ var sameTag = 0;
57
+ var index = 0;
58
+ for (var i = 0; i < parent.children.length; i++) {
59
+ var sib = parent.children[i];
60
+ if (sib.tagName === el.tagName) {
61
+ sameTag++;
62
+ if (sib === el)
63
+ index = sameTag;
64
+ }
65
+ }
66
+ return sameTag > 1 ? tag + cls + ':nth-of-type(' + index + ')' : tag + cls;
67
+ }
68
+ function pathOf(el, stopAt) {
69
+ var parts = [];
70
+ var node = el;
71
+ while (node && node !== stopAt.parentElement) {
72
+ parts.unshift(segment(node));
73
+ node = node.parentElement;
74
+ }
75
+ return parts.join(' > ');
76
+ }
77
+ var dpr = win.devicePixelRatio || 1;
78
+ var scrollX = win.scrollX || 0;
79
+ var scrollY = win.scrollY || 0;
80
+ var out = [];
81
+ var body = doc.body;
82
+ if (!body)
83
+ return out;
84
+ var stack = [body];
85
+ while (stack.length > 0 && out.length < maxElements) {
86
+ var el = stack.pop();
87
+ // The consistency rule: elements covered by ignoreSelectors are
88
+ // excluded from pixels and the DOM snapshot — they must be excluded
89
+ // from the element map too (subtree included), or their dynamic
90
+ // styles would trip the style fingerprint they were meant to escape.
91
+ var ignored = false;
92
+ if (ignoreSelectors.length > 0 && typeof el.matches === 'function') {
93
+ for (var g = 0; g < ignoreSelectors.length; g++) {
94
+ try {
95
+ if (el.matches(ignoreSelectors[g])) {
96
+ ignored = true;
97
+ break;
98
+ }
99
+ }
100
+ catch (e) {
101
+ // invalid selector — never breaks the walk
102
+ }
103
+ }
104
+ }
105
+ if (ignored)
106
+ continue; // skip element AND subtree
107
+ var rect = el.getBoundingClientRect();
108
+ if (rect.width >= 4 && rect.height >= 4) {
109
+ var styleParts = [];
110
+ var hidden = false;
111
+ try {
112
+ var cs = win.getComputedStyle(el);
113
+ for (var p = 0; p < STYLE_PROPS.length; p++) {
114
+ var value = cs.getPropertyValue(STYLE_PROPS[p]);
115
+ if (STYLE_PROPS[p] === 'visibility' && value === 'hidden')
116
+ hidden = true;
117
+ styleParts.push(STYLE_PROPS[p] + ':' + value);
118
+ }
119
+ }
120
+ catch (e) {
121
+ // styleHash stays a digest of the empty string — still deterministic
122
+ }
123
+ // visibility:hidden elements paint no pixels, so their style changes
124
+ // can never explain a pixel diff — keep them out of the map. Their
125
+ // CHILDREN may override visibility, so the subtree still walks.
126
+ if (!hidden) {
127
+ out.push({
128
+ path: pathOf(el, body),
129
+ x: Math.round((rect.x + scrollX) * dpr),
130
+ y: Math.round((rect.y + scrollY) * dpr),
131
+ width: Math.round(rect.width * dpr),
132
+ height: Math.round(rect.height * dpr),
133
+ styleHash: fnv1a(styleParts.join(';')),
134
+ });
135
+ }
136
+ }
137
+ // Push children in reverse so the walk stays document-ordered
138
+ for (var c = el.children.length - 1; c >= 0; c--) {
139
+ stack.push(el.children[c]);
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+ /** Default element cap — bounds capture time and elements.json size. */
145
+ exports.DEFAULT_MAX_ELEMENTS = 3000;
146
+ /**
147
+ * The page-side expression evaluated by the adapter. Serializes the
148
+ * collector so page code and unit-tested code are the same function.
149
+ */
150
+ function buildElementMapExpression(maxElements = exports.DEFAULT_MAX_ELEMENTS, ignoreSelectors = []) {
151
+ return `(${collectElementMap.toString()})(document, window, ${maxElements}, ${JSON.stringify(ignoreSelectors)})`;
152
+ }
153
+ //# sourceMappingURL=element-map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"element-map.js","sourceRoot":"","sources":["../../src/capture/element-map.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Page-settled probe — "has this page stopped changing?" answered without
3
+ * screenshotting repeatedly.
4
+ *
5
+ * WHY NOT NETWORK IDLE
6
+ * --------------------
7
+ * Playwright's own docs mark `networkidle` **DISCOURAGED**: "Don't use this
8
+ * method for testing, rely on web assertions to assess readiness instead." It
9
+ * is the wrong signal for a visual snapshot anyway — a page with analytics
10
+ * beacons or long-polling never goes quiet yet is perfectly settled, while a
11
+ * page can be network-idle with a CSS animation still running.
12
+ *
13
+ * WHY NOT A SETTLE LOOP
14
+ * ---------------------
15
+ * Playwright's built-in screenshots repeatedly until two consecutive images
16
+ * match. That costs a capture per attempt. TestivAI already absorbs render
17
+ * jitter through the DOM/style layer (identical DOM + identical styles reads as
18
+ * noise, not a change), so the only gap left is content that had not *loaded*
19
+ * yet — which is a load question, not a pixel question.
20
+ *
21
+ * WHAT THIS DOES
22
+ * --------------
23
+ * A cheap synchronous probe the adapter polls from the host language, exactly
24
+ * like the existing fonts wait. It reports whether the document is complete,
25
+ * whether images have finished, and how long the DOM has been quiet — the last
26
+ * measured with a MutationObserver, which observes rather than monkey-patching
27
+ * `fetch`/`XHR` and so cannot alter the behaviour of the page under test.
28
+ *
29
+ * SELF-CONTAINED on purpose: no imports, no closures, so it can be serialised
30
+ * into the page and shipped to the Python, Java and Ruby adapters as a
31
+ * generated asset — the same approach as the element-map collector.
32
+ */
33
+ export interface SettleState {
34
+ /** document.readyState === 'complete' */
35
+ ready: boolean;
36
+ /** Images still decoding. `complete` is true for failed loads, so a 404 never hangs us. */
37
+ imagesPending: number;
38
+ /** Web fonts still loading, when the Font Loading API is available. */
39
+ fontsPending: boolean;
40
+ /** Milliseconds since the last DOM mutation. */
41
+ quietFor: number;
42
+ /** Everything above satisfied, including the quiet threshold. */
43
+ settled: boolean;
44
+ }
45
+ /** Global key holding the observer state between polls. */
46
+ export declare const SETTLE_STATE_KEY = "__testivaiSettleState";
47
+ /**
48
+ * One poll. Installs the MutationObserver on first call and reuses it after,
49
+ * so repeated polls are cheap.
50
+ */
51
+ export declare function settleProbe(doc: {
52
+ readyState: string;
53
+ documentElement: unknown;
54
+ images: ArrayLike<{
55
+ complete: boolean;
56
+ }>;
57
+ fonts?: {
58
+ status?: string;
59
+ };
60
+ }, win: Record<string, unknown> & {
61
+ MutationObserver?: new (cb: () => void) => {
62
+ observe: (t: unknown, o: unknown) => void;
63
+ };
64
+ }, quietMs: number): SettleState;
65
+ /** Default DOM-quiet threshold. Long enough to span a render, short enough not to be felt. */
66
+ export declare const DEFAULT_QUIET_MS = 150;
67
+ /** Default ceiling on waiting. A page that never settles must not hang the suite. */
68
+ export declare const DEFAULT_SETTLE_TIMEOUT_MS = 5000;
69
+ /** The expression an adapter evaluates for one poll. */
70
+ export declare function buildSettleProbeExpression(quietMs?: number): string;
71
+ /**
72
+ * Disconnect the observer and drop the global. Adapters call this in the same
73
+ * `finally` that removes the injected CSS — leaving an observer attached to
74
+ * someone else's page would be rude and would keep firing for the rest of the
75
+ * test.
76
+ */
77
+ export declare const SETTLE_STOP_EXPRESSION = "(function (w) {\n try {\n var s = w['__testivaiSettleState'];\n if (s && s.observer && typeof s.observer.disconnect === 'function') s.observer.disconnect();\n } catch (e) {}\n try { delete w['__testivaiSettleState']; } catch (e) { w['__testivaiSettleState'] = undefined; }\n})(window)";
78
+ //# sourceMappingURL=settle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settle.d.ts","sourceRoot":"","sources":["../../src/capture/settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,2FAA2F;IAC3F,aAAa,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,YAAY,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,2DAA2D;AAC3D,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAExD;;;GAGG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,MAAM,EAAE,SAAS,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7B,EACD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,IAAI,KAAK;QAAE,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAA;CAAE,EAC3H,OAAO,EAAE,MAAM,GACd,WAAW,CAoDb;AAED,8FAA8F;AAC9F,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,wDAAwD;AACxD,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,MAAyB,GAAG,MAAM,CAErF;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,0SAMxB,CAAC"}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * Page-settled probe — "has this page stopped changing?" answered without
4
+ * screenshotting repeatedly.
5
+ *
6
+ * WHY NOT NETWORK IDLE
7
+ * --------------------
8
+ * Playwright's own docs mark `networkidle` **DISCOURAGED**: "Don't use this
9
+ * method for testing, rely on web assertions to assess readiness instead." It
10
+ * is the wrong signal for a visual snapshot anyway — a page with analytics
11
+ * beacons or long-polling never goes quiet yet is perfectly settled, while a
12
+ * page can be network-idle with a CSS animation still running.
13
+ *
14
+ * WHY NOT A SETTLE LOOP
15
+ * ---------------------
16
+ * Playwright's built-in screenshots repeatedly until two consecutive images
17
+ * match. That costs a capture per attempt. TestivAI already absorbs render
18
+ * jitter through the DOM/style layer (identical DOM + identical styles reads as
19
+ * noise, not a change), so the only gap left is content that had not *loaded*
20
+ * yet — which is a load question, not a pixel question.
21
+ *
22
+ * WHAT THIS DOES
23
+ * --------------
24
+ * A cheap synchronous probe the adapter polls from the host language, exactly
25
+ * like the existing fonts wait. It reports whether the document is complete,
26
+ * whether images have finished, and how long the DOM has been quiet — the last
27
+ * measured with a MutationObserver, which observes rather than monkey-patching
28
+ * `fetch`/`XHR` and so cannot alter the behaviour of the page under test.
29
+ *
30
+ * SELF-CONTAINED on purpose: no imports, no closures, so it can be serialised
31
+ * into the page and shipped to the Python, Java and Ruby adapters as a
32
+ * generated asset — the same approach as the element-map collector.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.SETTLE_STOP_EXPRESSION = exports.DEFAULT_SETTLE_TIMEOUT_MS = exports.DEFAULT_QUIET_MS = exports.SETTLE_STATE_KEY = void 0;
36
+ exports.settleProbe = settleProbe;
37
+ exports.buildSettleProbeExpression = buildSettleProbeExpression;
38
+ /** Global key holding the observer state between polls. */
39
+ exports.SETTLE_STATE_KEY = '__testivaiSettleState';
40
+ /**
41
+ * One poll. Installs the MutationObserver on first call and reuses it after,
42
+ * so repeated polls are cheap.
43
+ */
44
+ function settleProbe(doc, win, quietMs) {
45
+ var KEY = '__testivaiSettleState';
46
+ var state = win[KEY];
47
+ if (!state) {
48
+ state = { last: Date.now() };
49
+ try {
50
+ if (typeof win.MutationObserver === 'function') {
51
+ var observer = new win.MutationObserver(function () {
52
+ win[KEY].last = Date.now();
53
+ });
54
+ observer.observe(doc.documentElement, {
55
+ subtree: true,
56
+ childList: true,
57
+ attributes: true,
58
+ characterData: true,
59
+ });
60
+ state.observer = observer;
61
+ }
62
+ }
63
+ catch (e) {
64
+ // No MutationObserver (or a hostile document): quietness degrades to
65
+ // "always quiet" rather than blocking the capture.
66
+ }
67
+ win[KEY] = state;
68
+ }
69
+ var imagesPending = 0;
70
+ try {
71
+ for (var i = 0; i < doc.images.length; i++) {
72
+ if (!doc.images[i].complete)
73
+ imagesPending++;
74
+ }
75
+ }
76
+ catch (e) {
77
+ imagesPending = 0;
78
+ }
79
+ var fontsPending = false;
80
+ try {
81
+ fontsPending = !!(doc.fonts && doc.fonts.status && doc.fonts.status !== 'loaded');
82
+ }
83
+ catch (e) {
84
+ fontsPending = false;
85
+ }
86
+ var ready = doc.readyState === 'complete';
87
+ var quietFor = Date.now() - state.last;
88
+ return {
89
+ ready: ready,
90
+ imagesPending: imagesPending,
91
+ fontsPending: fontsPending,
92
+ quietFor: quietFor,
93
+ settled: ready && imagesPending === 0 && !fontsPending && quietFor >= quietMs,
94
+ };
95
+ }
96
+ /** Default DOM-quiet threshold. Long enough to span a render, short enough not to be felt. */
97
+ exports.DEFAULT_QUIET_MS = 150;
98
+ /** Default ceiling on waiting. A page that never settles must not hang the suite. */
99
+ exports.DEFAULT_SETTLE_TIMEOUT_MS = 5000;
100
+ /** The expression an adapter evaluates for one poll. */
101
+ function buildSettleProbeExpression(quietMs = exports.DEFAULT_QUIET_MS) {
102
+ return `(${settleProbe.toString()})(document, window, ${quietMs})`;
103
+ }
104
+ /**
105
+ * Disconnect the observer and drop the global. Adapters call this in the same
106
+ * `finally` that removes the injected CSS — leaving an observer attached to
107
+ * someone else's page would be rude and would keep firing for the rest of the
108
+ * test.
109
+ */
110
+ exports.SETTLE_STOP_EXPRESSION = `(function (w) {
111
+ try {
112
+ var s = w['${exports.SETTLE_STATE_KEY}'];
113
+ if (s && s.observer && typeof s.observer.disconnect === 'function') s.observer.disconnect();
114
+ } catch (e) {}
115
+ try { delete w['${exports.SETTLE_STATE_KEY}']; } catch (e) { w['${exports.SETTLE_STATE_KEY}'] = undefined; }
116
+ })(window)`;
117
+ //# sourceMappingURL=settle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settle.js","sourceRoot":"","sources":["../../src/capture/settle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shard participation — the framework-agnostic half of parallel runs.
3
+ *
4
+ * Playwright can tell its reporter `--shard=i/N`; pytest, JUnit, RSpec and a
5
+ * bare Selenium script cannot. So the mechanism is environment, not framework
6
+ * API, and every adapter honours the same two variables:
7
+ *
8
+ * TESTIVAI_CAPTURE_ONLY=1 capture, do not compare or write a report
9
+ * TESTIVAI_SHARD=3/8 this process is shard 3 of 8
10
+ *
11
+ * A framework that happens to know its own shard (Playwright) may fill these in
12
+ * itself, but the env var is the contract — that is what keeps a Selenium or
13
+ * pytest suite a first-class citizen of the same sharded CI flow.
14
+ */
15
+ export interface ShardInfo {
16
+ current: number;
17
+ total: number;
18
+ }
19
+ export declare const SHARD_MANIFEST = "testivai-shard.json";
20
+ /** Parse `TESTIVAI_SHARD` in either `3/8` or `3of8` form. Invalid → null. */
21
+ export declare function parseShardEnv(value: string | undefined): ShardInfo | null;
22
+ /**
23
+ * Capture-only resolution shared by every JS adapter.
24
+ * An explicit argument wins; then the env var; then "are we a shard of many".
25
+ */
26
+ export declare function resolveCaptureOnly(explicit: boolean | undefined, shard: ShardInfo | null, env?: NodeJS.ProcessEnv): boolean;
27
+ export interface ShardManifest {
28
+ shard: ShardInfo;
29
+ captures: string[];
30
+ /**
31
+ * True once the run finished. Adapters with a run-end hook (Playwright,
32
+ * pytest, JUnit) set it; adapters without one (a bare Selenium script, RSpec)
33
+ * write the manifest per capture and leave it false. `merge-captures` uses it
34
+ * to tell "this shard finished" from "this shard started and vanished".
35
+ */
36
+ complete: boolean;
37
+ status?: string;
38
+ timestamp: string;
39
+ }
40
+ /**
41
+ * Write (or refresh) the manifest for this shard.
42
+ *
43
+ * Safe to call on every capture: it rewrites the file with the captures present
44
+ * so far, which is what makes it usable by adapters that have no end-of-run
45
+ * hook to attach to.
46
+ */
47
+ export declare function writeShardManifest(tempRoot: string, shard: ShardInfo, options?: {
48
+ complete?: boolean;
49
+ status?: string;
50
+ }): void;
51
+ //# sourceMappingURL=shard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shard.d.ts","sourceRoot":"","sources":["../../src/capture/shard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,cAAc,wBAAwB,CAAC;AAEpD,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CASzE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,OAAO,GAAG,SAAS,EAC7B,KAAK,EAAE,SAAS,GAAG,IAAI,EACvB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAKT;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;OAKG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,IAAI,CAqBN"}