@testivai/witness-playwright 2.0.8 → 2.0.10
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/capture/environment.d.ts +36 -0
- package/dist/capture/environment.js +56 -0
- package/dist/capture/full-page.d.ts +22 -0
- package/dist/capture/full-page.js +42 -0
- package/dist/snapshot.js +40 -27
- package/package.json +2 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture provenance for the Playwright adapter: the `environment` block
|
|
3
|
+
* witness() records in the capture's metadata.json (see capture/environment.ts
|
|
4
|
+
* in @testivai/witness for the shape and why it matters).
|
|
5
|
+
*
|
|
6
|
+
* Sources, in order of trust: the project's own `use` settings and Playwright's
|
|
7
|
+
* browser.version(); the in-page render probe (what the page is actually
|
|
8
|
+
* rendered with); the user agent as the fallback for a browser that cannot be
|
|
9
|
+
* asked (a persistent context has no browser()). Nothing is guessed: what none
|
|
10
|
+
* of them says is left out, or null for the version.
|
|
11
|
+
*/
|
|
12
|
+
import { type CaptureEnvironment, type HostEnvironment } from '@testivai/witness';
|
|
13
|
+
/** The parts of Playwright's TestInfo this reads (structural, so tests need no runner). */
|
|
14
|
+
export interface TestInfoLike {
|
|
15
|
+
config?: {
|
|
16
|
+
version?: string;
|
|
17
|
+
};
|
|
18
|
+
project?: {
|
|
19
|
+
name?: string;
|
|
20
|
+
use?: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
file?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The engine witness() runs in: the project's browserName, else the user agent, else Playwright's default. */
|
|
25
|
+
export declare function playwrightBrowserName(testInfo: TestInfoLike, userAgent: string | undefined): string;
|
|
26
|
+
export interface PlaywrightEnvironmentInput {
|
|
27
|
+
testInfo: TestInfoLike;
|
|
28
|
+
/** page.context().browser()?.version(), or null when there is no browser to ask. */
|
|
29
|
+
browserVersion: string | null;
|
|
30
|
+
/** The raw RENDER_PROBE_EXPRESSION result (null when the probe failed). */
|
|
31
|
+
probe: unknown;
|
|
32
|
+
captureMethod: string;
|
|
33
|
+
/** Injected in tests; collected from this machine otherwise. */
|
|
34
|
+
host?: HostEnvironment;
|
|
35
|
+
}
|
|
36
|
+
export declare function playwrightEnvironment(input: PlaywrightEnvironmentInput): CaptureEnvironment;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.playwrightBrowserName = playwrightBrowserName;
|
|
4
|
+
exports.playwrightEnvironment = playwrightEnvironment;
|
|
5
|
+
/**
|
|
6
|
+
* Capture provenance for the Playwright adapter: the `environment` block
|
|
7
|
+
* witness() records in the capture's metadata.json (see capture/environment.ts
|
|
8
|
+
* in @testivai/witness for the shape and why it matters).
|
|
9
|
+
*
|
|
10
|
+
* Sources, in order of trust: the project's own `use` settings and Playwright's
|
|
11
|
+
* browser.version(); the in-page render probe (what the page is actually
|
|
12
|
+
* rendered with); the user agent as the fallback for a browser that cannot be
|
|
13
|
+
* asked (a persistent context has no browser()). Nothing is guessed: what none
|
|
14
|
+
* of them says is left out, or null for the version.
|
|
15
|
+
*/
|
|
16
|
+
const witness_1 = require("@testivai/witness");
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
18
|
+
const ADAPTER_VERSION = require('../../package.json').version;
|
|
19
|
+
/** The engine witness() runs in: the project's browserName, else the user agent, else Playwright's default. */
|
|
20
|
+
function playwrightBrowserName(testInfo, userAgent) {
|
|
21
|
+
const configured = testInfo.project?.use?.browserName;
|
|
22
|
+
if (typeof configured === 'string' && configured.length > 0)
|
|
23
|
+
return configured;
|
|
24
|
+
return (0, witness_1.browserFromUserAgent)(userAgent ?? '')?.name ?? 'chromium';
|
|
25
|
+
}
|
|
26
|
+
function playwrightEnvironment(input) {
|
|
27
|
+
const { testInfo } = input;
|
|
28
|
+
const use = testInfo.project?.use ?? {};
|
|
29
|
+
const probed = (0, witness_1.parseRenderProbe)(input.probe);
|
|
30
|
+
const fromUa = probed?.userAgent ? (0, witness_1.browserFromUserAgent)(probed.userAgent) : null;
|
|
31
|
+
const browser = {
|
|
32
|
+
name: playwrightBrowserName(testInfo, probed?.userAgent),
|
|
33
|
+
version: input.browserVersion ?? fromUa?.version ?? null,
|
|
34
|
+
};
|
|
35
|
+
if (typeof use.channel === 'string' && use.channel.length > 0)
|
|
36
|
+
browser.channel = use.channel;
|
|
37
|
+
if (typeof use.headless === 'boolean')
|
|
38
|
+
browser.headless = use.headless;
|
|
39
|
+
else if (fromUa?.headless)
|
|
40
|
+
browser.headless = true;
|
|
41
|
+
const file = testInfo.file ?? '';
|
|
42
|
+
const projectName = testInfo.project?.name;
|
|
43
|
+
return (0, witness_1.buildCaptureEnvironment)({
|
|
44
|
+
host: input.host ?? (0, witness_1.collectHostEnvironment)(),
|
|
45
|
+
browser,
|
|
46
|
+
framework: {
|
|
47
|
+
name: 'playwright',
|
|
48
|
+
version: testInfo.config?.version ?? null,
|
|
49
|
+
language: /\.[cm]?tsx?$/.test(file) ? 'typescript' : 'javascript',
|
|
50
|
+
runner: '@playwright/test',
|
|
51
|
+
},
|
|
52
|
+
adapter: { name: '@testivai/witness-playwright', version: ADAPTER_VERSION, captureMethod: input.captureMethod },
|
|
53
|
+
render: probed?.render,
|
|
54
|
+
project: typeof projectName === 'string' && projectName.length > 0 ? projectName : undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CDPSession, Page } from '@playwright/test';
|
|
2
|
+
import { type CaptureLog } from '@testivai/witness';
|
|
3
|
+
/**
|
|
4
|
+
* Full-page screenshot through CDP: wait for a fresh frame, measure the
|
|
5
|
+
* content, then capture beyond the viewport until two captures in a row match
|
|
6
|
+
* (at most MAX_STABLE_CAPTURES from @testivai/witness).
|
|
7
|
+
*
|
|
8
|
+
* Chrome can hand back a frame painted with styles resolved against the wrong
|
|
9
|
+
* viewport while it sets up a beyond-viewport capture: on testivai-site's CI,
|
|
10
|
+
* em-sized text on the home page was drawn at its `max-width: 480px` size in
|
|
11
|
+
* some captures, so an unchanged page read as 4.13% changed while its measured
|
|
12
|
+
* layout matched the baseline. See capture/stable-capture.ts in
|
|
13
|
+
* @testivai/witness, which the standalone CLI shares.
|
|
14
|
+
*
|
|
15
|
+
* A stable page costs one extra screenshot.
|
|
16
|
+
*
|
|
17
|
+
* @param page The page being captured; used for the in-page frame wait.
|
|
18
|
+
* @param client A CDP session attached to `page`.
|
|
19
|
+
* @param log Optional debug sink.
|
|
20
|
+
* @returns The PNG bytes of the kept capture.
|
|
21
|
+
*/
|
|
22
|
+
export declare function captureFullPageScreenshot(page: Page, client: CDPSession, log?: CaptureLog): Promise<Buffer>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.captureFullPageScreenshot = captureFullPageScreenshot;
|
|
4
|
+
const witness_1 = require("@testivai/witness");
|
|
5
|
+
/**
|
|
6
|
+
* Full-page screenshot through CDP: wait for a fresh frame, measure the
|
|
7
|
+
* content, then capture beyond the viewport until two captures in a row match
|
|
8
|
+
* (at most MAX_STABLE_CAPTURES from @testivai/witness).
|
|
9
|
+
*
|
|
10
|
+
* Chrome can hand back a frame painted with styles resolved against the wrong
|
|
11
|
+
* viewport while it sets up a beyond-viewport capture: on testivai-site's CI,
|
|
12
|
+
* em-sized text on the home page was drawn at its `max-width: 480px` size in
|
|
13
|
+
* some captures, so an unchanged page read as 4.13% changed while its measured
|
|
14
|
+
* layout matched the baseline. See capture/stable-capture.ts in
|
|
15
|
+
* @testivai/witness, which the standalone CLI shares.
|
|
16
|
+
*
|
|
17
|
+
* A stable page costs one extra screenshot.
|
|
18
|
+
*
|
|
19
|
+
* @param page The page being captured; used for the in-page frame wait.
|
|
20
|
+
* @param client A CDP session attached to `page`.
|
|
21
|
+
* @param log Optional debug sink.
|
|
22
|
+
* @returns The PNG bytes of the kept capture.
|
|
23
|
+
*/
|
|
24
|
+
async function captureFullPageScreenshot(page, client, log) {
|
|
25
|
+
const waitOnce = async () => (await page.evaluate(witness_1.FRESH_FRAME_EXPRESSION)) === true;
|
|
26
|
+
const settle = () => (0, witness_1.settleFreshFrame)(waitOnce, log);
|
|
27
|
+
await settle();
|
|
28
|
+
const layoutMetrics = await client.send('Page.getLayoutMetrics');
|
|
29
|
+
const width = Math.ceil(layoutMetrics.contentSize.width);
|
|
30
|
+
const height = Math.ceil(layoutMetrics.contentSize.height);
|
|
31
|
+
log?.(`page ${width}x${height}, viewport ${layoutMetrics.layoutViewport?.clientWidth}x${layoutMetrics.layoutViewport?.clientHeight}`);
|
|
32
|
+
const data = await (0, witness_1.captureUntilStable)({
|
|
33
|
+
capture: async () => (await client.send('Page.captureScreenshot', {
|
|
34
|
+
format: 'png',
|
|
35
|
+
captureBeyondViewport: true,
|
|
36
|
+
clip: { x: 0, y: 0, width, height, scale: 1 },
|
|
37
|
+
})).data,
|
|
38
|
+
settle,
|
|
39
|
+
log,
|
|
40
|
+
});
|
|
41
|
+
return Buffer.from(data, 'base64');
|
|
42
|
+
}
|
package/dist/snapshot.js
CHANGED
|
@@ -46,6 +46,9 @@ const loader_1 = require("./config/loader");
|
|
|
46
46
|
const ignore_selectors_1 = require("./config/ignore-selectors");
|
|
47
47
|
const element_map_1 = require("./capture/element-map");
|
|
48
48
|
const stabilize_1 = require("./config/stabilize");
|
|
49
|
+
const witness_1 = require("@testivai/witness");
|
|
50
|
+
const environment_1 = require("./capture/environment");
|
|
51
|
+
const full_page_1 = require("./capture/full-page");
|
|
49
52
|
/**
|
|
50
53
|
* Generates a safe filename from a URL.
|
|
51
54
|
* @param pageUrl The URL of the page.
|
|
@@ -110,7 +113,15 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
110
113
|
}
|
|
111
114
|
const outputDir = path.join(process.cwd(), '.testivai', 'temp');
|
|
112
115
|
await fs.ensureDir(outputDir);
|
|
113
|
-
|
|
116
|
+
// What the page is rendered with, measured before anything is injected:
|
|
117
|
+
// recorded as capture provenance, and its user agent names the engine for
|
|
118
|
+
// environmentPolicy "separate" when the project does not.
|
|
119
|
+
const renderProbe = await page.evaluate(witness_1.RENDER_PROBE_EXPRESSION).catch(() => null);
|
|
120
|
+
const environmentPolicy = (0, witness_1.loadLocalConfig)(process.cwd()).environmentPolicy;
|
|
121
|
+
const snapshotName = (0, witness_1.applyEnvironmentPolicyToName)(effectiveSnapshotName(name || getSnapshotNameFromUrl(page.url()), testInfo), { browser: { name: (0, environment_1.playwrightBrowserName)(testInfo ?? {}, (0, witness_1.parseRenderProbe)(renderProbe)?.userAgent) }, os: { platform: process.platform } }, environmentPolicy);
|
|
122
|
+
// Which path took the screenshot (set below): part of the provenance, since
|
|
123
|
+
// another method changes the image and often its size.
|
|
124
|
+
let captureMethod = witness_1.CAPTURE_METHODS.CDP_FULL_PAGE;
|
|
114
125
|
const timestamp = Date.now();
|
|
115
126
|
const safeName = snapshotName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
116
127
|
const baseFilename = `${timestamp}_${safeName}`;
|
|
@@ -224,9 +235,11 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
224
235
|
}
|
|
225
236
|
// If only one capture needed, just take a regular screenshot
|
|
226
237
|
if (numCaptures <= 1) {
|
|
238
|
+
captureMethod = witness_1.CAPTURE_METHODS.PLAYWRIGHT_FULL_PAGE;
|
|
227
239
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
228
240
|
}
|
|
229
241
|
else {
|
|
242
|
+
captureMethod = witness_1.CAPTURE_METHODS.STITCHED_FULL_PAGE;
|
|
230
243
|
// Scroll-and-stitch approach
|
|
231
244
|
const screenshots = [];
|
|
232
245
|
for (let i = 0; i < numCaptures; i++) {
|
|
@@ -322,33 +335,15 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
322
335
|
});
|
|
323
336
|
// Wait a bit for styles to apply
|
|
324
337
|
await page.waitForTimeout(100);
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
pageWidth,
|
|
333
|
-
pageHeight,
|
|
334
|
-
viewportWidth: layoutMetrics.layoutViewport.clientWidth,
|
|
335
|
-
viewportHeight: layoutMetrics.layoutViewport.clientHeight
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
// Capture screenshot with captureBeyondViewport: true
|
|
339
|
-
const screenshot = await client.send('Page.captureScreenshot', {
|
|
340
|
-
format: 'png',
|
|
341
|
-
captureBeyondViewport: true,
|
|
342
|
-
clip: {
|
|
343
|
-
x: 0,
|
|
344
|
-
y: 0,
|
|
345
|
-
width: pageWidth,
|
|
346
|
-
height: pageHeight,
|
|
347
|
-
scale: 1
|
|
348
|
-
}
|
|
349
|
-
});
|
|
338
|
+
// Wait for a fresh frame, measure, and capture beyond the viewport until
|
|
339
|
+
// two captures in a row match: Chrome can return a frame painted for the
|
|
340
|
+
// wrong viewport while it sets up the capture (capture/full-page.ts).
|
|
341
|
+
const captureLog = process.env.TESTIVAI_DEBUG === 'true'
|
|
342
|
+
? (message) => console.log(`[TestivAI] Full-page capture: ${message}`)
|
|
343
|
+
: undefined;
|
|
344
|
+
const png = await (0, full_page_1.captureFullPageScreenshot)(page, client, captureLog);
|
|
350
345
|
// Save the screenshot
|
|
351
|
-
await fs.writeFile(screenshotPath,
|
|
346
|
+
await fs.writeFile(screenshotPath, png);
|
|
352
347
|
// Remove the temporary style tag
|
|
353
348
|
await page.evaluate(`
|
|
354
349
|
const styleTags = document.querySelectorAll('style');
|
|
@@ -363,6 +358,7 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
363
358
|
catch (error) {
|
|
364
359
|
console.error('[TestivAI] Browser screenshot failed:', error.message);
|
|
365
360
|
// Fallback to regular screenshot
|
|
361
|
+
captureMethod = witness_1.CAPTURE_METHODS.PLAYWRIGHT_FULL_PAGE;
|
|
366
362
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
367
363
|
}
|
|
368
364
|
}
|
|
@@ -473,6 +469,23 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
473
469
|
catch {
|
|
474
470
|
// metadata is an enhancement; the screenshot path never depends on it
|
|
475
471
|
}
|
|
472
|
+
// Capture provenance: the environment this capture was taken in, merged
|
|
473
|
+
// into metadata.json (approving freezes it into the baseline). Best-effort
|
|
474
|
+
// like the rest: the screenshot path never depends on it.
|
|
475
|
+
try {
|
|
476
|
+
let browserVersion = null;
|
|
477
|
+
try {
|
|
478
|
+
browserVersion = page.context().browser()?.version() ?? null;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// a persistent context has no browser(): the probe's user agent is the fallback
|
|
482
|
+
}
|
|
483
|
+
const environment = (0, environment_1.playwrightEnvironment)({ testInfo: testInfo ?? {}, browserVersion, probe: renderProbe, captureMethod });
|
|
484
|
+
new witness_1.BaselineStore(process.cwd()).writeTempMetadata(snapshotName, { name: snapshotName, environment });
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// no environment only means the compare cannot tell drift from noise for this capture
|
|
488
|
+
}
|
|
476
489
|
// Element map: layout + computed-style digest for every visible
|
|
477
490
|
// element, powering attribution ("WHICH element changed"), exact
|
|
478
491
|
// shift classification, and the style fingerprint on the compare
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-playwright",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"fs-extra": "^11.4.0",
|
|
48
48
|
"sharp": "^0.35.4",
|
|
49
|
-
"@testivai/witness": "2.
|
|
49
|
+
"@testivai/witness": "2.5.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@playwright/test": "^1.63.0",
|