@testivai/witness-playwright 1.8.0 → 1.9.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/dist/config/stabilize.d.ts +18 -0
- package/dist/config/stabilize.js +49 -0
- package/dist/reporter.js +10 -26
- package/dist/snapshot.js +5 -0
- package/package.json +2 -2
|
@@ -33,3 +33,21 @@ export declare function resolveStabilize(projectRoot: string, projectConfig: Tes
|
|
|
33
33
|
* a missing FontFaceSet API just means no wait.
|
|
34
34
|
*/
|
|
35
35
|
export declare function waitForFonts(page: Page): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Wait until the page has stopped changing: document complete, images
|
|
38
|
+
* finished, fonts loaded, and no DOM mutations for `quietMs`.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately NOT `networkidle` — Playwright's own docs mark that DISCOURAGED
|
|
41
|
+
* for testing, and it is the wrong signal for a visual snapshot: a page with
|
|
42
|
+
* analytics beacons never goes quiet, while a network-idle page can still be
|
|
43
|
+
* animating. What matters is whether the rendered page settled.
|
|
44
|
+
*
|
|
45
|
+
* Always bounded; a page that never settles yields a capture, not a hang.
|
|
46
|
+
*/
|
|
47
|
+
export declare function waitForSettled(page: {
|
|
48
|
+
evaluate: (expr: string) => Promise<unknown>;
|
|
49
|
+
}, quietMs?: number, timeoutMs?: number): Promise<void>;
|
|
50
|
+
/** Detach the settle observer so it does not linger on the page under test. */
|
|
51
|
+
export declare function stopSettleObserver(page: {
|
|
52
|
+
evaluate: (expr: string) => Promise<unknown>;
|
|
53
|
+
}): Promise<void>;
|
package/dist/config/stabilize.js
CHANGED
|
@@ -52,6 +52,9 @@ exports.STABILIZE_CSS = void 0;
|
|
|
52
52
|
exports.readWitnessConfigStabilize = readWitnessConfigStabilize;
|
|
53
53
|
exports.resolveStabilize = resolveStabilize;
|
|
54
54
|
exports.waitForFonts = waitForFonts;
|
|
55
|
+
exports.waitForSettled = waitForSettled;
|
|
56
|
+
exports.stopSettleObserver = stopSettleObserver;
|
|
57
|
+
const witness_1 = require("@testivai/witness");
|
|
55
58
|
const fs = __importStar(require("fs"));
|
|
56
59
|
const path = __importStar(require("path"));
|
|
57
60
|
/**
|
|
@@ -110,3 +113,49 @@ async function waitForFonts(page) {
|
|
|
110
113
|
// Page navigated or evaluate failed — capture proceeds unstabilized fonts
|
|
111
114
|
}
|
|
112
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Wait until the page has stopped changing: document complete, images
|
|
118
|
+
* finished, fonts loaded, and no DOM mutations for `quietMs`.
|
|
119
|
+
*
|
|
120
|
+
* Deliberately NOT `networkidle` — Playwright's own docs mark that DISCOURAGED
|
|
121
|
+
* for testing, and it is the wrong signal for a visual snapshot: a page with
|
|
122
|
+
* analytics beacons never goes quiet, while a network-idle page can still be
|
|
123
|
+
* animating. What matters is whether the rendered page settled.
|
|
124
|
+
*
|
|
125
|
+
* Always bounded; a page that never settles yields a capture, not a hang.
|
|
126
|
+
*/
|
|
127
|
+
async function waitForSettled(page, quietMs = witness_1.DEFAULT_QUIET_MS, timeoutMs = witness_1.DEFAULT_SETTLE_TIMEOUT_MS) {
|
|
128
|
+
const expr = (0, witness_1.buildSettleProbeExpression)(quietMs);
|
|
129
|
+
const deadline = Date.now() + timeoutMs;
|
|
130
|
+
for (;;) {
|
|
131
|
+
let state;
|
|
132
|
+
try {
|
|
133
|
+
state = (await page.evaluate(expr));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return; // probe unavailable — never block the capture
|
|
137
|
+
}
|
|
138
|
+
// An unusable answer means we cannot probe; polling to the timeout would
|
|
139
|
+
// add the full wait to every capture.
|
|
140
|
+
if (!state || typeof state.settled !== 'boolean')
|
|
141
|
+
return;
|
|
142
|
+
if (state.settled)
|
|
143
|
+
return;
|
|
144
|
+
if (Date.now() >= deadline) {
|
|
145
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
146
|
+
console.log(`[TestivAI] page did not settle within ${timeoutMs}ms — capturing anyway`);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Detach the settle observer so it does not linger on the page under test. */
|
|
154
|
+
async function stopSettleObserver(page) {
|
|
155
|
+
try {
|
|
156
|
+
await page.evaluate(witness_1.SETTLE_STOP_EXPRESSION);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// best-effort cleanup
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/reporter.js
CHANGED
|
@@ -44,6 +44,7 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
44
44
|
const ci_1 = require("./ci");
|
|
45
45
|
const common_1 = require("@testivai/common");
|
|
46
46
|
const mode_1 = require("./mode");
|
|
47
|
+
const witness_1 = require("@testivai/witness");
|
|
47
48
|
class TestivAIPlaywrightReporter {
|
|
48
49
|
constructor(options = {}) {
|
|
49
50
|
this.gitInfo = null;
|
|
@@ -70,16 +71,13 @@ class TestivAIPlaywrightReporter {
|
|
|
70
71
|
* 2. TESTIVAI_CAPTURE_ONLY — for CI that can't edit playwright.config
|
|
71
72
|
* 3. auto: Playwright says this is shard i/N with N > 1
|
|
72
73
|
*/
|
|
73
|
-
resolveCaptureOnly(
|
|
74
|
-
|
|
75
|
-
return this.options.captureOnly;
|
|
76
|
-
const env = process.env.TESTIVAI_CAPTURE_ONLY;
|
|
77
|
-
if (env !== undefined && env !== '')
|
|
78
|
-
return env !== '0' && env.toLowerCase() !== 'false';
|
|
79
|
-
return !!(this.shard && this.shard.total > 1);
|
|
74
|
+
resolveCaptureOnly(_config) {
|
|
75
|
+
return (0, witness_1.resolveCaptureOnly)(this.options.captureOnly, this.shard);
|
|
80
76
|
}
|
|
81
77
|
async onBegin(config, suite) {
|
|
82
|
-
|
|
78
|
+
// Playwright's own --shard wins; TESTIVAI_SHARD covers CI that splits work
|
|
79
|
+
// some other way, and is the same contract the non-JS adapters honour.
|
|
80
|
+
this.shard = config.shard ?? (0, witness_1.parseShardEnv)(process.env.TESTIVAI_SHARD);
|
|
83
81
|
this.captureOnly = this.resolveCaptureOnly(config);
|
|
84
82
|
// Resolve mode. Local-first is the default: with no API key we capture
|
|
85
83
|
// and report locally rather than disabling the reporter. Cloud mode
|
|
@@ -145,24 +143,10 @@ class TestivAIPlaywrightReporter {
|
|
|
145
143
|
// Without this a crashed shard silently reduces coverage whenever
|
|
146
144
|
// failOnMissing is off.
|
|
147
145
|
if (this.shard) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.readdirSync(tempRoot, { withFileTypes: true })
|
|
153
|
-
.filter((e) => e.isDirectory())
|
|
154
|
-
.map((e) => e.name);
|
|
155
|
-
fs.writeJsonSync(path.join(tempRoot, 'testivai-shard.json'), {
|
|
156
|
-
shard: { current: this.shard.current, total: this.shard.total },
|
|
157
|
-
captures,
|
|
158
|
-
status: result.status,
|
|
159
|
-
timestamp: new Date().toISOString(),
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
// A manifest we cannot write must not fail the test run; the merge
|
|
164
|
-
// step degrades to "no completeness check" and says so.
|
|
165
|
-
}
|
|
146
|
+
(0, witness_1.writeShardManifest)(path.join(process.cwd(), '.testivai', 'temp'), this.shard, {
|
|
147
|
+
complete: true,
|
|
148
|
+
status: result.status,
|
|
149
|
+
});
|
|
166
150
|
}
|
|
167
151
|
const where = this.shard ? `shard ${this.shard.current}/${this.shard.total}` : 'capture-only mode';
|
|
168
152
|
console.log(`\n TestivAI: ${where} — captured to .testivai/temp/, comparison skipped.`);
|
package/dist/snapshot.js
CHANGED
|
@@ -152,6 +152,10 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
152
152
|
// Style injection can fail on locked-down pages; capture proceeds
|
|
153
153
|
}
|
|
154
154
|
await (0, stabilize_1.waitForFonts)(page);
|
|
155
|
+
// Then wait for the page itself to stop changing — images finished, DOM
|
|
156
|
+
// quiet. This is the load question the DOM/style layer cannot answer:
|
|
157
|
+
// content that never arrived reads as a real change, not as noise.
|
|
158
|
+
await (0, stabilize_1.waitForSettled)(page);
|
|
155
159
|
}
|
|
156
160
|
// Check if scroll-and-stitch is explicitly requested (backup method)
|
|
157
161
|
if (effectiveConfig.useBrowserCapture === false) {
|
|
@@ -378,6 +382,7 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
378
382
|
if (stabilizeStyleEl) {
|
|
379
383
|
await stabilizeStyleEl.evaluate((el) => el.remove()).catch(() => { });
|
|
380
384
|
stabilizeStyleEl = null;
|
|
385
|
+
await (0, stabilize_1.stopSettleObserver)(page);
|
|
381
386
|
}
|
|
382
387
|
// 1.5. Local mode: also place the screenshot in the layout expected by
|
|
383
388
|
// @testivai/witness/report (subdirectory keyed by snapshot name).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-playwright",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Playwright adapter for TestivAI Witness — local-first visual regression with pixel + DOM comparison",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"sharp": "^0.34.5",
|
|
53
53
|
"simple-git": "^3.21.0",
|
|
54
54
|
"@testivai/common": "0.2.3",
|
|
55
|
-
"@testivai/witness": "1.
|
|
55
|
+
"@testivai/witness": "1.13.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@playwright/test": "^1.60.0",
|