@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.
- package/README.md +17 -30
- package/dist/bin/testivai.js +2 -0
- package/dist/bin/testivai.js.map +1 -1
- package/dist/capture/element-map.d.ts +74 -0
- package/dist/capture/element-map.d.ts.map +1 -0
- package/dist/capture/element-map.js +153 -0
- package/dist/capture/element-map.js.map +1 -0
- package/dist/capture/settle.d.ts +78 -0
- package/dist/capture/settle.d.ts.map +1 -0
- package/dist/capture/settle.js +117 -0
- package/dist/capture/settle.js.map +1 -0
- package/dist/capture/shard.d.ts +51 -0
- package/dist/capture/shard.d.ts.map +1 -0
- package/dist/capture/shard.js +113 -0
- package/dist/capture/shard.js.map +1 -0
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/exit-codes.d.ts +4 -4
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +22 -24
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/merge-captures.d.ts +65 -0
- package/dist/commands/merge-captures.d.ts.map +1 -0
- package/dist/commands/merge-captures.js +233 -0
- package/dist/commands/merge-captures.js.map +1 -0
- package/dist/commands/report.d.ts +5 -1
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +5 -1
- package/dist/commands/report.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +0 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- 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
|
|
15
|
+
## Quick Start
|
|
16
16
|
|
|
17
|
-
TestivAI
|
|
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
|
-
##
|
|
47
|
+
## Sharing results with your team
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
Everything stays on your machine. To get a diff in front of a teammate:
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
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. **
|
|
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
|
|
208
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
- **
|
|
378
|
+
- **Compare time**: ~50-200ms per snapshot (pixel + DOM diff)
|
|
392
379
|
|
|
393
380
|
## API Reference
|
|
394
381
|
|
package/dist/bin/testivai.js
CHANGED
|
@@ -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();
|
package/dist/bin/testivai.js.map
CHANGED
|
@@ -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"}
|