@testivai/witness-playwright 2.0.9 → 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.
@@ -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
+ }
package/dist/snapshot.js CHANGED
@@ -46,6 +46,8 @@ 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");
49
51
  const full_page_1 = require("./capture/full-page");
50
52
  /**
51
53
  * Generates a safe filename from a URL.
@@ -111,7 +113,15 @@ async function snapshot(page, testInfo, name, config) {
111
113
  }
112
114
  const outputDir = path.join(process.cwd(), '.testivai', 'temp');
113
115
  await fs.ensureDir(outputDir);
114
- const snapshotName = effectiveSnapshotName(name || getSnapshotNameFromUrl(page.url()), testInfo);
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;
115
125
  const timestamp = Date.now();
116
126
  const safeName = snapshotName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
117
127
  const baseFilename = `${timestamp}_${safeName}`;
@@ -225,9 +235,11 @@ async function snapshot(page, testInfo, name, config) {
225
235
  }
226
236
  // If only one capture needed, just take a regular screenshot
227
237
  if (numCaptures <= 1) {
238
+ captureMethod = witness_1.CAPTURE_METHODS.PLAYWRIGHT_FULL_PAGE;
228
239
  await page.screenshot({ path: screenshotPath, fullPage: true });
229
240
  }
230
241
  else {
242
+ captureMethod = witness_1.CAPTURE_METHODS.STITCHED_FULL_PAGE;
231
243
  // Scroll-and-stitch approach
232
244
  const screenshots = [];
233
245
  for (let i = 0; i < numCaptures; i++) {
@@ -346,6 +358,7 @@ async function snapshot(page, testInfo, name, config) {
346
358
  catch (error) {
347
359
  console.error('[TestivAI] Browser screenshot failed:', error.message);
348
360
  // Fallback to regular screenshot
361
+ captureMethod = witness_1.CAPTURE_METHODS.PLAYWRIGHT_FULL_PAGE;
349
362
  await page.screenshot({ path: screenshotPath, fullPage: true });
350
363
  }
351
364
  }
@@ -456,6 +469,23 @@ async function snapshot(page, testInfo, name, config) {
456
469
  catch {
457
470
  // metadata is an enhancement; the screenshot path never depends on it
458
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
+ }
459
489
  // Element map: layout + computed-style digest for every visible
460
490
  // element, powering attribution ("WHICH element changed"), exact
461
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.9",
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.4.0"
49
+ "@testivai/witness": "2.5.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@playwright/test": "^1.63.0",