@testivai/witness 2.4.0 → 2.6.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 (106) hide show
  1. package/README.md +9 -2
  2. package/dist/baselines/store.d.ts +31 -0
  3. package/dist/baselines/store.d.ts.map +1 -1
  4. package/dist/baselines/store.js +38 -1
  5. package/dist/baselines/store.js.map +1 -1
  6. package/dist/bin/testivai.js +2 -0
  7. package/dist/bin/testivai.js.map +1 -1
  8. package/dist/capture/element-map.d.ts +23 -2
  9. package/dist/capture/element-map.d.ts.map +1 -1
  10. package/dist/capture/element-map.js +102 -4
  11. package/dist/capture/element-map.js.map +1 -1
  12. package/dist/capture/environment.d.ts +243 -0
  13. package/dist/capture/environment.d.ts.map +1 -0
  14. package/dist/capture/environment.js +403 -0
  15. package/dist/capture/environment.js.map +1 -0
  16. package/dist/capture/health-limits.d.ts +14 -0
  17. package/dist/capture/health-limits.d.ts.map +1 -0
  18. package/dist/capture/health-limits.js +17 -0
  19. package/dist/capture/health-limits.js.map +1 -0
  20. package/dist/capture/health.d.ts +124 -0
  21. package/dist/capture/health.d.ts.map +1 -0
  22. package/dist/capture/health.js +327 -0
  23. package/dist/capture/health.js.map +1 -0
  24. package/dist/capture/native-app.d.ts +77 -0
  25. package/dist/capture/native-app.d.ts.map +1 -0
  26. package/dist/capture/native-app.js +172 -0
  27. package/dist/capture/native-app.js.map +1 -0
  28. package/dist/capture/shard.d.ts +17 -0
  29. package/dist/capture/shard.d.ts.map +1 -1
  30. package/dist/capture/shard.js +8 -1
  31. package/dist/capture/shard.js.map +1 -1
  32. package/dist/commands/approve.d.ts.map +1 -1
  33. package/dist/commands/approve.js +20 -0
  34. package/dist/commands/approve.js.map +1 -1
  35. package/dist/commands/env.d.ts +37 -0
  36. package/dist/commands/env.d.ts.map +1 -0
  37. package/dist/commands/env.js +113 -0
  38. package/dist/commands/env.js.map +1 -0
  39. package/dist/commands/merge-captures.d.ts.map +1 -1
  40. package/dist/commands/merge-captures.js +10 -0
  41. package/dist/commands/merge-captures.js.map +1 -1
  42. package/dist/config/local-config.d.ts +27 -0
  43. package/dist/config/local-config.d.ts.map +1 -1
  44. package/dist/config/local-config.js +1 -0
  45. package/dist/config/local-config.js.map +1 -1
  46. package/dist/config/validate-config.d.ts.map +1 -1
  47. package/dist/config/validate-config.js +22 -0
  48. package/dist/config/validate-config.js.map +1 -1
  49. package/dist/diff/attribution.d.ts +16 -2
  50. package/dist/diff/attribution.d.ts.map +1 -1
  51. package/dist/diff/attribution.js +5 -4
  52. package/dist/diff/attribution.js.map +1 -1
  53. package/dist/diff/dom-diff.d.ts.map +1 -1
  54. package/dist/diff/dom-diff.js +7 -1
  55. package/dist/diff/dom-diff.js.map +1 -1
  56. package/dist/index.d.ts +10 -4
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +42 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/report/anchor.d.ts +18 -0
  61. package/dist/report/anchor.d.ts.map +1 -0
  62. package/dist/report/anchor.js +51 -0
  63. package/dist/report/anchor.js.map +1 -0
  64. package/dist/report/compare.d.ts +18 -1
  65. package/dist/report/compare.d.ts.map +1 -1
  66. package/dist/report/compare.js +107 -16
  67. package/dist/report/compare.js.map +1 -1
  68. package/dist/report/environment.d.ts +101 -0
  69. package/dist/report/environment.d.ts.map +1 -0
  70. package/dist/report/environment.js +354 -0
  71. package/dist/report/environment.js.map +1 -0
  72. package/dist/report/generator.d.ts +20 -2
  73. package/dist/report/generator.d.ts.map +1 -1
  74. package/dist/report/generator.js +46 -6
  75. package/dist/report/generator.js.map +1 -1
  76. package/dist/report/health.d.ts +115 -0
  77. package/dist/report/health.d.ts.map +1 -0
  78. package/dist/report/health.js +279 -0
  79. package/dist/report/health.js.map +1 -0
  80. package/dist/report/index.d.ts +9 -2
  81. package/dist/report/index.d.ts.map +1 -1
  82. package/dist/report/index.js +25 -1
  83. package/dist/report/index.js.map +1 -1
  84. package/dist/report/results.d.ts +73 -1
  85. package/dist/report/results.d.ts.map +1 -1
  86. package/dist/report/shift-check.d.ts +40 -0
  87. package/dist/report/shift-check.d.ts.map +1 -0
  88. package/dist/report/shift-check.js +109 -0
  89. package/dist/report/shift-check.js.map +1 -0
  90. package/dist/report/template.d.ts +28 -1
  91. package/dist/report/template.d.ts.map +1 -1
  92. package/dist/report/template.js +179 -16
  93. package/dist/report/template.js.map +1 -1
  94. package/dist/report/thumbnail.d.ts +76 -0
  95. package/dist/report/thumbnail.d.ts.map +1 -0
  96. package/dist/report/thumbnail.js +166 -0
  97. package/dist/report/thumbnail.js.map +1 -0
  98. package/dist/report/validate.d.ts +53 -0
  99. package/dist/report/validate.d.ts.map +1 -0
  100. package/dist/report/validate.js +383 -0
  101. package/dist/report/validate.js.map +1 -0
  102. package/dist/standalone/run.d.ts +39 -1
  103. package/dist/standalone/run.d.ts.map +1 -1
  104. package/dist/standalone/run.js +142 -3
  105. package/dist/standalone/run.js.map +1 -1
  106. package/package.json +1 -1
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Capture provenance — where a capture came from.
3
+ *
4
+ * When the same URL renders differently, the question a reviewer needs
5
+ * answered first is whether the PAGE changed or the MACHINE did: a new
6
+ * browser build, another OS or distro (another set of fonts), another device
7
+ * scale, another way of taking the screenshot. Every adapter records the
8
+ * same `environment` block in the capture's metadata.json; approving a
9
+ * capture freezes it into the baseline's metadata.json, and the compare
10
+ * reads the two side by side (report/environment.ts).
11
+ *
12
+ * The block is plain JSON, so non-JS adapters write the same shape
13
+ * (docs/extension-api.md). Every part an adapter cannot observe is left out,
14
+ * never guessed: an absent field reads as "not recorded", not as a change.
15
+ */
16
+ /** What the page was rendered with, measured in the page (RENDER_PROBE_EXPRESSION). */
17
+ export interface RenderEnvironment {
18
+ /** CSS viewport (window.innerWidth × innerHeight). */
19
+ viewport?: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /** window.devicePixelRatio. */
24
+ deviceScaleFactor?: number;
25
+ colorScheme?: 'light' | 'dark';
26
+ reducedMotion?: 'reduce' | 'no-preference';
27
+ /** navigator.language. */
28
+ locale?: string;
29
+ /** Intl time zone. */
30
+ timezone?: string;
31
+ }
32
+ export interface CaptureEnvironment {
33
+ os: {
34
+ /** process.platform: linux / darwin / win32. */
35
+ platform: string;
36
+ /** os.release(): the kernel on Linux, the OS version on macOS and Windows. */
37
+ release: string;
38
+ arch: string;
39
+ /** Linux only: `<ID> <VERSION_ID>` from /etc/os-release, e.g. "ubuntu 24.04". */
40
+ distro?: string;
41
+ };
42
+ browser: {
43
+ /** chromium / firefox / webkit (the engine family). */
44
+ name: string;
45
+ /** Full build version; null when the adapter could not read it. */
46
+ version: string | null;
47
+ /** A branded build of the engine: chrome, msedge, chrome-beta… */
48
+ channel?: string;
49
+ headless?: boolean;
50
+ };
51
+ /** The test framework driving the browser, when there is one (language only when the adapter can tell). */
52
+ framework?: {
53
+ name: string;
54
+ version: string | null;
55
+ language?: string;
56
+ runner?: string;
57
+ };
58
+ adapter: {
59
+ name: string;
60
+ version: string;
61
+ /** `<extent>:<mechanism>`, one of CAPTURE_METHODS. */
62
+ captureMethod: string;
63
+ };
64
+ /** The WebDriver binary, when the adapter talks to one. */
65
+ driver?: {
66
+ name: string;
67
+ version: string;
68
+ };
69
+ render?: RenderEnvironment;
70
+ ci?: {
71
+ provider: string;
72
+ runnerImage?: string;
73
+ };
74
+ /** Running inside a container (/.dockerenv or /run/.containerenv). */
75
+ container?: boolean;
76
+ /** TESTIVAI_ENV_LABEL: a name the team gives this environment, e.g. its image tag. */
77
+ label?: string;
78
+ /** Playwright project (or another runner's variant) the capture ran under. */
79
+ project?: string;
80
+ /**
81
+ * Native app captures only (`browser.name: "native-app"`): the device that
82
+ * drew the screen, from the Appium capabilities (capture/native-app.ts).
83
+ * `os` stays the machine the tests ran on.
84
+ */
85
+ device?: DeviceEnvironment;
86
+ }
87
+ /** The phone, tablet, emulator or simulator a native app capture came from. */
88
+ export interface DeviceEnvironment {
89
+ /** android / ios (Appium's platformName, lowercased). */
90
+ platform: string;
91
+ /** The device OS version (platformVersion): "14", "17.5". */
92
+ osVersion?: string;
93
+ /** The hardware model the device reports (Android deviceModel): "Pixel 7", "sdk_gphone64_x86_64". */
94
+ model?: string;
95
+ /** The device name the session asked for (deviceName): "iPhone 15". */
96
+ name?: string;
97
+ /** The Appium driver (automationName): UiAutomator2, XCUITest. */
98
+ automation?: string;
99
+ /** Screen size in physical pixels (Android deviceScreenSize): "1080x2400". */
100
+ screen?: string;
101
+ /** Screen density in dpi (Android deviceScreenDensity). */
102
+ density?: number;
103
+ }
104
+ /** The machine-level part of the block: the same for every capture of a run. */
105
+ export type HostEnvironment = Pick<CaptureEnvironment, 'os' | 'ci' | 'container' | 'label'>;
106
+ /**
107
+ * How the screenshot was taken, as `<extent>:<mechanism>`. The extent is what
108
+ * the image covers (the full page or the viewport); the mechanism is what
109
+ * took it. A change of method changes the image, and often its size.
110
+ */
111
+ export declare const CAPTURE_METHODS: {
112
+ /** CDP Page.captureScreenshot with captureBeyondViewport. */
113
+ readonly CDP_FULL_PAGE: "full-page:cdp";
114
+ /** Playwright page.screenshot({ fullPage: true }). */
115
+ readonly PLAYWRIGHT_FULL_PAGE: "full-page:playwright";
116
+ /** Viewport screenshots scrolled and stitched together. */
117
+ readonly STITCHED_FULL_PAGE: "full-page:stitched";
118
+ /** Firefox's own full-page screenshot (Selenium save_full_page_screenshot). */
119
+ readonly FIREFOX_FULL_PAGE: "full-page:firefox";
120
+ /** WebDriver takeScreenshot: the viewport only. */
121
+ readonly WEBDRIVER_VIEWPORT: "viewport:webdriver";
122
+ /** Appium takeScreenshot in a native app: the whole device screen, status bar included. */
123
+ readonly APPIUM_SCREEN: "screen:appium";
124
+ };
125
+ /** The `environmentPolicy` values in .testivai/config.json. */
126
+ export type EnvironmentPolicy = 'warn' | 'separate' | 'strict';
127
+ export declare const ENVIRONMENT_POLICIES: readonly EnvironmentPolicy[];
128
+ /**
129
+ * The distro from /etc/os-release, as `<ID> <VERSION_ID>` ("ubuntu 24.04",
130
+ * "debian 12"; "arch" for a rolling release). That pair decides the fonts and
131
+ * the FreeType build a Linux machine draws text with; the kernel does not.
132
+ */
133
+ export declare function distroFromOsRelease(text: string): string | undefined;
134
+ /** The CI provider, and the runner image where the provider names one. */
135
+ export declare function detectCi(env: NodeJS.ProcessEnv): CaptureEnvironment['ci'];
136
+ /** Where collectHostEnvironment reads from; every source is injectable for tests. */
137
+ export interface HostSources {
138
+ env?: NodeJS.ProcessEnv;
139
+ platform?: string;
140
+ release?: string;
141
+ arch?: string;
142
+ /** Returns a file's text, or null when it cannot be read. */
143
+ readFile?: (filePath: string) => string | null;
144
+ exists?: (filePath: string) => boolean;
145
+ }
146
+ /** The machine-level part of the block. Never throws. */
147
+ export declare function collectHostEnvironment(sources?: HostSources): HostEnvironment;
148
+ /**
149
+ * The engine and its version from a user agent: the fallback for an adapter
150
+ * whose driver does not say (a Playwright persistent context has no
151
+ * browser() to ask). Edge is reported as chromium with channel msedge, the
152
+ * way Playwright names it.
153
+ */
154
+ export declare function browserFromUserAgent(ua: string): CaptureEnvironment['browser'] | null;
155
+ /**
156
+ * The browser (and the WebDriver binary) from W3C capabilities, as
157
+ * WebdriverIO's `browser.capabilities` or selenium-webdriver's
158
+ * `getCapabilities()` returns them (a plain object, or an object with
159
+ * `get()`). Headless is read from the browser's own launch arguments, and
160
+ * only said when they are there to say it. Null without a browser name.
161
+ */
162
+ export declare function browserFromCapabilities(caps: unknown): {
163
+ browser: CaptureEnvironment['browser'];
164
+ driver?: CaptureEnvironment['driver'];
165
+ } | null;
166
+ /**
167
+ * The version of a package as the project at `root` resolves it (the user's
168
+ * own webdriverio or selenium-webdriver, not one bundled here), or null.
169
+ */
170
+ export declare function installedPackageVersion(name: string, root?: string): string | null;
171
+ /**
172
+ * One self-contained in-page expression (no imports, no closures) that
173
+ * measures what the page is actually rendered with. Measured, not taken from
174
+ * configuration: a config can say 1280×720 at scale 1 while the browser
175
+ * renders something else. Each field is read on its own, so one missing API
176
+ * never loses the rest.
177
+ */
178
+ export declare const RENDER_PROBE_EXPRESSION = "(function () {\n var out = {};\n try { out.viewport = { width: window.innerWidth, height: window.innerHeight }; } catch (e) {}\n try { out.deviceScaleFactor = window.devicePixelRatio; } catch (e) {}\n try { out.colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } catch (e) {}\n try { out.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' : 'no-preference'; } catch (e) {}\n try { out.locale = navigator.language; } catch (e) {}\n try { out.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) {}\n try { out.userAgent = navigator.userAgent; } catch (e) {}\n return out;\n})()";
179
+ /** Validate the probe's result. Mistyped fields are dropped, never trusted. */
180
+ export declare function parseRenderProbe(raw: unknown): {
181
+ render: RenderEnvironment;
182
+ userAgent?: string;
183
+ } | null;
184
+ export interface CaptureEnvironmentParts {
185
+ host: HostEnvironment;
186
+ browser: CaptureEnvironment['browser'];
187
+ adapter: CaptureEnvironment['adapter'];
188
+ framework?: CaptureEnvironment['framework'];
189
+ driver?: CaptureEnvironment['driver'];
190
+ render?: RenderEnvironment;
191
+ project?: string;
192
+ device?: DeviceEnvironment;
193
+ }
194
+ /** Assemble the block from what one adapter observed. */
195
+ export declare function buildCaptureEnvironment(parts: CaptureEnvironmentParts): CaptureEnvironment;
196
+ export interface WebDriverEnvironmentInput {
197
+ /** The session's capabilities (WebdriverIO browser.capabilities, Selenium getCapabilities()). */
198
+ capabilities: unknown;
199
+ /** The raw RENDER_PROBE_EXPRESSION result. */
200
+ probe: unknown;
201
+ adapter: CaptureEnvironment['adapter'];
202
+ framework: {
203
+ name: string;
204
+ version: string | null;
205
+ };
206
+ host?: HostEnvironment;
207
+ }
208
+ /**
209
+ * The block for a WebDriver adapter: the browser and driver from the
210
+ * capabilities (the user agent when they say nothing), the render settings
211
+ * from the probe. Null when neither names the browser: never guessed.
212
+ */
213
+ export declare function webDriverEnvironment(input: WebDriverEnvironmentInput): CaptureEnvironment | null;
214
+ /**
215
+ * The block as recorded in a metadata.json, or null when there is none:
216
+ * captures from before provenance, or from an adapter that does not record
217
+ * it. Only the three parts every recorder writes are required.
218
+ */
219
+ export declare function readEnvironment(raw: unknown): CaptureEnvironment | null;
220
+ /**
221
+ * environmentPolicy "separate" keys each snapshot by environment:
222
+ * `<name>__<browser>__<platform>`, the same in-the-name mechanism as the
223
+ * Playwright multi-project variant, so the on-disk layout, results.json, the
224
+ * report, approvals and the approve action all work unchanged. A browser the
225
+ * name already ends with (a project named after it) is not repeated. Never
226
+ * keyed by version: a browser upgrade should be seen, not hidden behind a
227
+ * fresh baseline. Other policies return the name unchanged.
228
+ */
229
+ export declare function applyEnvironmentPolicyToName(name: string, env: {
230
+ browser: {
231
+ name: string;
232
+ };
233
+ os: {
234
+ platform: string;
235
+ };
236
+ }, policy: EnvironmentPolicy | undefined): string;
237
+ /** The variant a separate-policy name carries, or null for a name without one. */
238
+ export declare function parseEnvironmentVariant(name: string): {
239
+ base: string;
240
+ browser: string;
241
+ platform: string;
242
+ } | null;
243
+ //# sourceMappingURL=environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/capture/environment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ /**
3
+ * Capture provenance — where a capture came from.
4
+ *
5
+ * When the same URL renders differently, the question a reviewer needs
6
+ * answered first is whether the PAGE changed or the MACHINE did: a new
7
+ * browser build, another OS or distro (another set of fonts), another device
8
+ * scale, another way of taking the screenshot. Every adapter records the
9
+ * same `environment` block in the capture's metadata.json; approving a
10
+ * capture freezes it into the baseline's metadata.json, and the compare
11
+ * reads the two side by side (report/environment.ts).
12
+ *
13
+ * The block is plain JSON, so non-JS adapters write the same shape
14
+ * (docs/extension-api.md). Every part an adapter cannot observe is left out,
15
+ * never guessed: an absent field reads as "not recorded", not as a change.
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.RENDER_PROBE_EXPRESSION = exports.ENVIRONMENT_POLICIES = exports.CAPTURE_METHODS = void 0;
52
+ exports.distroFromOsRelease = distroFromOsRelease;
53
+ exports.detectCi = detectCi;
54
+ exports.collectHostEnvironment = collectHostEnvironment;
55
+ exports.browserFromUserAgent = browserFromUserAgent;
56
+ exports.browserFromCapabilities = browserFromCapabilities;
57
+ exports.installedPackageVersion = installedPackageVersion;
58
+ exports.parseRenderProbe = parseRenderProbe;
59
+ exports.buildCaptureEnvironment = buildCaptureEnvironment;
60
+ exports.webDriverEnvironment = webDriverEnvironment;
61
+ exports.readEnvironment = readEnvironment;
62
+ exports.applyEnvironmentPolicyToName = applyEnvironmentPolicyToName;
63
+ exports.parseEnvironmentVariant = parseEnvironmentVariant;
64
+ const fs = __importStar(require("fs"));
65
+ const os = __importStar(require("os"));
66
+ /**
67
+ * How the screenshot was taken, as `<extent>:<mechanism>`. The extent is what
68
+ * the image covers (the full page or the viewport); the mechanism is what
69
+ * took it. A change of method changes the image, and often its size.
70
+ */
71
+ exports.CAPTURE_METHODS = {
72
+ /** CDP Page.captureScreenshot with captureBeyondViewport. */
73
+ CDP_FULL_PAGE: 'full-page:cdp',
74
+ /** Playwright page.screenshot({ fullPage: true }). */
75
+ PLAYWRIGHT_FULL_PAGE: 'full-page:playwright',
76
+ /** Viewport screenshots scrolled and stitched together. */
77
+ STITCHED_FULL_PAGE: 'full-page:stitched',
78
+ /** Firefox's own full-page screenshot (Selenium save_full_page_screenshot). */
79
+ FIREFOX_FULL_PAGE: 'full-page:firefox',
80
+ /** WebDriver takeScreenshot: the viewport only. */
81
+ WEBDRIVER_VIEWPORT: 'viewport:webdriver',
82
+ /** Appium takeScreenshot in a native app: the whole device screen, status bar included. */
83
+ APPIUM_SCREEN: 'screen:appium',
84
+ };
85
+ exports.ENVIRONMENT_POLICIES = ['warn', 'separate', 'strict'];
86
+ // ── Host ────────────────────────────────────────────────────────────────────
87
+ /**
88
+ * The distro from /etc/os-release, as `<ID> <VERSION_ID>` ("ubuntu 24.04",
89
+ * "debian 12"; "arch" for a rolling release). That pair decides the fonts and
90
+ * the FreeType build a Linux machine draws text with; the kernel does not.
91
+ */
92
+ function distroFromOsRelease(text) {
93
+ const fields = {};
94
+ for (const line of text.split(/\r?\n/)) {
95
+ const m = /^([A-Z_]+)=(.*)$/.exec(line.trim());
96
+ if (m)
97
+ fields[m[1]] = m[2].replace(/^["']|["']$/g, '');
98
+ }
99
+ if (!fields.ID)
100
+ return undefined;
101
+ return fields.VERSION_ID ? `${fields.ID} ${fields.VERSION_ID}` : fields.ID;
102
+ }
103
+ /** The CI provider, and the runner image where the provider names one. */
104
+ function detectCi(env) {
105
+ if (env.GITHUB_ACTIONS === 'true') {
106
+ const runnerImage = env.ImageOS && env.ImageVersion ? `${env.ImageOS}/${env.ImageVersion}` : undefined;
107
+ return runnerImage ? { provider: 'github-actions', runnerImage } : { provider: 'github-actions' };
108
+ }
109
+ if (env.GITLAB_CI)
110
+ return { provider: 'gitlab' };
111
+ if (env.CIRCLECI)
112
+ return { provider: 'circleci' };
113
+ if (env.BUILDKITE)
114
+ return { provider: 'buildkite' };
115
+ if (env.JENKINS_URL)
116
+ return { provider: 'jenkins' };
117
+ if (env.TF_BUILD)
118
+ return { provider: 'azure-pipelines' };
119
+ if (env.CI && env.CI !== 'false' && env.CI !== '0')
120
+ return { provider: 'ci' };
121
+ return undefined;
122
+ }
123
+ const safely = (fn, fallback) => {
124
+ try {
125
+ return fn();
126
+ }
127
+ catch {
128
+ return fallback;
129
+ }
130
+ };
131
+ /** The machine-level part of the block. Never throws. */
132
+ function collectHostEnvironment(sources = {}) {
133
+ const env = sources.env ?? process.env;
134
+ const platform = sources.platform ?? process.platform;
135
+ const readFile = sources.readFile ?? ((p) => safely(() => fs.readFileSync(p, 'utf-8'), null));
136
+ const exists = sources.exists ?? ((p) => fs.existsSync(p));
137
+ const host = {
138
+ os: {
139
+ platform,
140
+ release: sources.release ?? safely(() => os.release(), ''),
141
+ arch: sources.arch ?? process.arch,
142
+ },
143
+ };
144
+ if (platform === 'linux') {
145
+ const text = safely(() => readFile('/etc/os-release'), null);
146
+ const distro = text ? distroFromOsRelease(text) : undefined;
147
+ if (distro)
148
+ host.os.distro = distro;
149
+ }
150
+ const ci = detectCi(env);
151
+ if (ci)
152
+ host.ci = ci;
153
+ if (safely(() => exists('/.dockerenv') || exists('/run/.containerenv'), false))
154
+ host.container = true;
155
+ const label = (env.TESTIVAI_ENV_LABEL ?? '').trim();
156
+ if (label)
157
+ host.label = label;
158
+ return host;
159
+ }
160
+ // ── Browser ─────────────────────────────────────────────────────────────────
161
+ /**
162
+ * The engine and its version from a user agent: the fallback for an adapter
163
+ * whose driver does not say (a Playwright persistent context has no
164
+ * browser() to ask). Edge is reported as chromium with channel msedge, the
165
+ * way Playwright names it.
166
+ */
167
+ function browserFromUserAgent(ua) {
168
+ if (!ua)
169
+ return null;
170
+ const edge = /\bEdg\/([\d.]+)/.exec(ua);
171
+ const chrome = /\b(HeadlessChrome|Chrome|Chromium)\/([\d.]+)/.exec(ua);
172
+ if (chrome) {
173
+ const browser = { name: 'chromium', version: chrome[2] };
174
+ if (chrome[1] === 'HeadlessChrome')
175
+ browser.headless = true;
176
+ if (edge)
177
+ browser.channel = 'msedge';
178
+ return browser;
179
+ }
180
+ const firefox = /\bFirefox\/([\d.]+)/.exec(ua);
181
+ if (firefox)
182
+ return { name: 'firefox', version: firefox[1] };
183
+ // Two linear matches, not /Version\/([\d.]+).*Safari\//: that pattern
184
+ // backtracks quadratically on a long run of dots.
185
+ const safari = /\bVersion\/([\d.]+)/.exec(ua);
186
+ if (safari && /\bSafari\//.test(ua.slice(safari.index + safari[0].length)))
187
+ return { name: 'webkit', version: safari[1] };
188
+ return null;
189
+ }
190
+ /** WebDriver browserName values, normalized to the engine family and its branded channel. */
191
+ const WEBDRIVER_BROWSERS = {
192
+ chrome: { name: 'chromium', channel: 'chrome', driver: 'chromedriver' },
193
+ 'chrome-headless-shell': { name: 'chromium', channel: 'chrome-headless-shell', driver: 'chromedriver' },
194
+ chromium: { name: 'chromium', driver: 'chromedriver' },
195
+ microsoftedge: { name: 'chromium', channel: 'msedge', driver: 'msedgedriver' },
196
+ msedge: { name: 'chromium', channel: 'msedge', driver: 'msedgedriver' },
197
+ firefox: { name: 'firefox', driver: 'geckodriver' },
198
+ safari: { name: 'webkit', channel: 'safari' },
199
+ };
200
+ /** The first token of a driver version string ("141.0.7390.37 (hash-refs/…)" → "141.0.7390.37"). */
201
+ const firstToken = (v) => (typeof v === 'string' && v.trim() ? v.trim().split(/\s+/)[0] : undefined);
202
+ /**
203
+ * The browser (and the WebDriver binary) from W3C capabilities, as
204
+ * WebdriverIO's `browser.capabilities` or selenium-webdriver's
205
+ * `getCapabilities()` returns them (a plain object, or an object with
206
+ * `get()`). Headless is read from the browser's own launch arguments, and
207
+ * only said when they are there to say it. Null without a browser name.
208
+ */
209
+ function browserFromCapabilities(caps) {
210
+ if (!caps || typeof caps !== 'object')
211
+ return null;
212
+ const c = caps;
213
+ const get = (key) => (typeof c.get === 'function' ? c.get(key) : c[key]);
214
+ const rawName = get('browserName');
215
+ if (typeof rawName !== 'string' || rawName.length === 0)
216
+ return null;
217
+ const known = WEBDRIVER_BROWSERS[rawName.toLowerCase()];
218
+ const version = get('browserVersion') ?? get('version');
219
+ const browser = {
220
+ name: known?.name ?? rawName.toLowerCase(),
221
+ version: typeof version === 'string' && version.length > 0 ? version : null,
222
+ };
223
+ if (known?.channel)
224
+ browser.channel = known.channel;
225
+ const options = (get('goog:chromeOptions') ?? get('ms:edgeOptions') ?? get('moz:firefoxOptions'));
226
+ if (options && Array.isArray(options.args)) {
227
+ browser.headless = options.args.some((a) => typeof a === 'string' && /^-{1,2}headless(=|$)/.test(a));
228
+ }
229
+ let driverVersion;
230
+ if (known?.driver === 'chromedriver')
231
+ driverVersion = firstToken(get('chrome')?.chromedriverVersion);
232
+ if (known?.driver === 'msedgedriver')
233
+ driverVersion = firstToken(get('msedge')?.msedgedriverVersion);
234
+ if (known?.driver === 'geckodriver')
235
+ driverVersion = firstToken(get('moz:geckodriverVersion'));
236
+ return driverVersion && known?.driver ? { browser, driver: { name: known.driver, version: driverVersion } } : { browser };
237
+ }
238
+ /**
239
+ * The version of a package as the project at `root` resolves it (the user's
240
+ * own webdriverio or selenium-webdriver, not one bundled here), or null.
241
+ */
242
+ function installedPackageVersion(name, root = process.cwd()) {
243
+ try {
244
+ const pkgPath = require.resolve(`${name}/package.json`, { paths: [root] });
245
+ const version = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
246
+ return typeof version === 'string' ? version : null;
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ // ── Render probe ────────────────────────────────────────────────────────────
253
+ /**
254
+ * One self-contained in-page expression (no imports, no closures) that
255
+ * measures what the page is actually rendered with. Measured, not taken from
256
+ * configuration: a config can say 1280×720 at scale 1 while the browser
257
+ * renders something else. Each field is read on its own, so one missing API
258
+ * never loses the rest.
259
+ */
260
+ exports.RENDER_PROBE_EXPRESSION = `(function () {
261
+ var out = {};
262
+ try { out.viewport = { width: window.innerWidth, height: window.innerHeight }; } catch (e) {}
263
+ try { out.deviceScaleFactor = window.devicePixelRatio; } catch (e) {}
264
+ try { out.colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } catch (e) {}
265
+ try { out.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' : 'no-preference'; } catch (e) {}
266
+ try { out.locale = navigator.language; } catch (e) {}
267
+ try { out.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) {}
268
+ try { out.userAgent = navigator.userAgent; } catch (e) {}
269
+ return out;
270
+ })()`;
271
+ const isNumber = (v) => typeof v === 'number' && Number.isFinite(v);
272
+ const isString = (v) => typeof v === 'string' && v.length > 0;
273
+ /** Validate the probe's result. Mistyped fields are dropped, never trusted. */
274
+ function parseRenderProbe(raw) {
275
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
276
+ return null;
277
+ const r = raw;
278
+ const render = {};
279
+ const vp = r.viewport;
280
+ if (vp && isNumber(vp.width) && isNumber(vp.height))
281
+ render.viewport = { width: vp.width, height: vp.height };
282
+ if (isNumber(r.deviceScaleFactor))
283
+ render.deviceScaleFactor = r.deviceScaleFactor;
284
+ if (r.colorScheme === 'light' || r.colorScheme === 'dark')
285
+ render.colorScheme = r.colorScheme;
286
+ if (r.reducedMotion === 'reduce' || r.reducedMotion === 'no-preference')
287
+ render.reducedMotion = r.reducedMotion;
288
+ if (isString(r.locale))
289
+ render.locale = r.locale;
290
+ if (isString(r.timezone))
291
+ render.timezone = r.timezone;
292
+ return isString(r.userAgent) ? { render, userAgent: r.userAgent } : { render };
293
+ }
294
+ // ── Assembly ────────────────────────────────────────────────────────────────
295
+ /** Drop undefined members, so the block round-trips through JSON unchanged. */
296
+ function compact(value) {
297
+ const out = {};
298
+ for (const [k, v] of Object.entries(value))
299
+ if (v !== undefined)
300
+ out[k] = v;
301
+ return out;
302
+ }
303
+ /** Assemble the block from what one adapter observed. */
304
+ function buildCaptureEnvironment(parts) {
305
+ const { host } = parts;
306
+ // Built in the documented order (docs/extension-api.md), so metadata.json reads the same everywhere.
307
+ const env = {
308
+ os: compact({ ...host.os }),
309
+ browser: compact({ ...parts.browser }),
310
+ };
311
+ if (parts.framework)
312
+ env.framework = compact({ ...parts.framework });
313
+ env.adapter = { ...parts.adapter };
314
+ if (parts.driver)
315
+ env.driver = { ...parts.driver };
316
+ if (parts.render) {
317
+ const render = compact({ ...parts.render });
318
+ if (Object.keys(render).length > 0)
319
+ env.render = render;
320
+ }
321
+ if (host.ci)
322
+ env.ci = compact({ ...host.ci });
323
+ if (host.container)
324
+ env.container = true;
325
+ if (host.label)
326
+ env.label = host.label;
327
+ if (parts.project)
328
+ env.project = parts.project;
329
+ if (parts.device)
330
+ env.device = compact({ ...parts.device });
331
+ return env;
332
+ }
333
+ /**
334
+ * The block for a WebDriver adapter: the browser and driver from the
335
+ * capabilities (the user agent when they say nothing), the render settings
336
+ * from the probe. Null when neither names the browser: never guessed.
337
+ */
338
+ function webDriverEnvironment(input) {
339
+ const probed = parseRenderProbe(input.probe);
340
+ const fromCaps = browserFromCapabilities(input.capabilities);
341
+ const browser = fromCaps?.browser ?? (probed?.userAgent ? browserFromUserAgent(probed.userAgent) : null);
342
+ if (!browser)
343
+ return null;
344
+ return buildCaptureEnvironment({
345
+ host: input.host ?? collectHostEnvironment(),
346
+ browser,
347
+ framework: { ...input.framework },
348
+ adapter: input.adapter,
349
+ driver: fromCaps?.driver,
350
+ render: probed?.render,
351
+ });
352
+ }
353
+ /**
354
+ * The block as recorded in a metadata.json, or null when there is none:
355
+ * captures from before provenance, or from an adapter that does not record
356
+ * it. Only the three parts every recorder writes are required.
357
+ */
358
+ function readEnvironment(raw) {
359
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
360
+ return null;
361
+ const e = raw;
362
+ const obj = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
363
+ if (!obj(e.os) || typeof e.os.platform !== 'string')
364
+ return null;
365
+ if (!obj(e.browser) || typeof e.browser.name !== 'string')
366
+ return null;
367
+ if (!obj(e.adapter) || typeof e.adapter.captureMethod !== 'string')
368
+ return null;
369
+ return e;
370
+ }
371
+ // ── separate policy ─────────────────────────────────────────────────────────
372
+ const safeSegment = (s) => s.replace(/[^a-z0-9_-]+/gi, '_').toLowerCase();
373
+ /** Node's process.platform values: a suffix ending in one of them is an environment variant. */
374
+ const PLATFORMS = ['aix', 'android', 'darwin', 'freebsd', 'haiku', 'linux', 'openbsd', 'sunos', 'win32', 'cygwin', 'netbsd'];
375
+ /**
376
+ * environmentPolicy "separate" keys each snapshot by environment:
377
+ * `<name>__<browser>__<platform>`, the same in-the-name mechanism as the
378
+ * Playwright multi-project variant, so the on-disk layout, results.json, the
379
+ * report, approvals and the approve action all work unchanged. A browser the
380
+ * name already ends with (a project named after it) is not repeated. Never
381
+ * keyed by version: a browser upgrade should be seen, not hidden behind a
382
+ * fresh baseline. Other policies return the name unchanged.
383
+ */
384
+ function applyEnvironmentPolicyToName(name, env, policy) {
385
+ if (policy !== 'separate')
386
+ return name;
387
+ const browser = safeSegment(env.browser.name);
388
+ const platform = safeSegment(env.os.platform);
389
+ const segments = name.split('__');
390
+ const withBrowser = segments.length > 1 && segments[segments.length - 1] === browser ? name : `${name}__${browser}`;
391
+ return `${withBrowser}__${platform}`;
392
+ }
393
+ /** The variant a separate-policy name carries, or null for a name without one. */
394
+ function parseEnvironmentVariant(name) {
395
+ const segments = name.split('__');
396
+ if (segments.length < 3)
397
+ return null;
398
+ const platform = segments[segments.length - 1];
399
+ if (!PLATFORMS.includes(platform))
400
+ return null;
401
+ return { base: segments.slice(0, -2).join('__'), browser: segments[segments.length - 2], platform };
402
+ }
403
+ //# sourceMappingURL=environment.js.map