@testivai/witness-playwright 0.1.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 (43) hide show
  1. package/README.md +65 -0
  2. package/__tests__/.gitkeep +0 -0
  3. package/__tests__/config-integration.spec.ts +102 -0
  4. package/__tests__/snapshot.spec.d.ts +1 -0
  5. package/__tests__/snapshot.spec.js +81 -0
  6. package/__tests__/snapshot.spec.ts +58 -0
  7. package/__tests__/unit/ci.spec.d.ts +1 -0
  8. package/__tests__/unit/ci.spec.js +35 -0
  9. package/__tests__/unit/ci.spec.ts +40 -0
  10. package/__tests__/unit/reporter.spec.d.ts +1 -0
  11. package/__tests__/unit/reporter.spec.js +37 -0
  12. package/__tests__/unit/reporter.spec.ts +43 -0
  13. package/dist/ci.d.ts +10 -0
  14. package/dist/ci.js +35 -0
  15. package/dist/cli/init.d.ts +3 -0
  16. package/dist/cli/init.js +146 -0
  17. package/dist/config/loader.d.ts +29 -0
  18. package/dist/config/loader.js +232 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.js +24 -0
  21. package/dist/reporter-types.d.ts +2 -0
  22. package/dist/reporter-types.js +2 -0
  23. package/dist/reporter.d.ts +16 -0
  24. package/dist/reporter.js +155 -0
  25. package/dist/snapshot.d.ts +12 -0
  26. package/dist/snapshot.js +122 -0
  27. package/dist/types.d.ts +181 -0
  28. package/dist/types.js +10 -0
  29. package/jest.config.js +11 -0
  30. package/package.json +47 -0
  31. package/playwright.config.ts +11 -0
  32. package/progress.md +620 -0
  33. package/src/ci.ts +34 -0
  34. package/src/cli/init.ts +119 -0
  35. package/src/config/loader.ts +219 -0
  36. package/src/index.ts +9 -0
  37. package/src/reporter-types.ts +5 -0
  38. package/src/reporter.ts +148 -0
  39. package/src/snapshot.ts +103 -0
  40. package/src/types.ts +193 -0
  41. package/test-results/.last-run.json +4 -0
  42. package/tsconfig.jest.json +7 -0
  43. package/tsconfig.json +19 -0
@@ -0,0 +1,29 @@
1
+ import { TestivAIProjectConfig, TestivAIConfig } from '../types';
2
+ /**
3
+ * Load TestivAI configuration from file system
4
+ * Supports both .ts and .js config files
5
+ *
6
+ * @returns Promise<TestivAIProjectConfig> The loaded configuration or defaults
7
+ */
8
+ export declare function loadConfig(): Promise<TestivAIProjectConfig>;
9
+ /**
10
+ * Merge per-test configuration with project configuration
11
+ *
12
+ * @param projectConfig The project-level configuration
13
+ * @param testConfig Optional per-test configuration overrides
14
+ * @returns TestivAIConfig The effective configuration for this test
15
+ */
16
+ export declare function mergeTestConfig(projectConfig: TestivAIProjectConfig, testConfig?: TestivAIConfig): TestivAIConfig;
17
+ /**
18
+ * Detect current environment (CI, development, production)
19
+ *
20
+ * @returns string The detected environment
21
+ */
22
+ export declare function detectEnvironment(): string;
23
+ /**
24
+ * Apply environment-specific overrides to configuration
25
+ *
26
+ * @param config The base configuration
27
+ * @returns TestivAIProjectConfig Configuration with environment overrides applied
28
+ */
29
+ export declare function applyEnvironmentOverrides(config: TestivAIProjectConfig): TestivAIProjectConfig;
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadConfig = loadConfig;
37
+ exports.mergeTestConfig = mergeTestConfig;
38
+ exports.detectEnvironment = detectEnvironment;
39
+ exports.applyEnvironmentOverrides = applyEnvironmentOverrides;
40
+ const fs = __importStar(require("fs-extra"));
41
+ const path = __importStar(require("path"));
42
+ /**
43
+ * Default configuration when no config file is found
44
+ */
45
+ const DEFAULT_CONFIG = {
46
+ layout: {
47
+ sensitivity: 2, // Balanced sensitivity (0-4 scale)
48
+ tolerance: 1.0, // 1 pixel base tolerance
49
+ },
50
+ ai: {
51
+ sensitivity: 2, // Balanced AI analysis (0-4 scale)
52
+ confidence: 0.7, // 70% confidence required for AI_BUG
53
+ }
54
+ };
55
+ /**
56
+ * Load TestivAI configuration from file system
57
+ * Supports both .ts and .js config files
58
+ *
59
+ * @returns Promise<TestivAIProjectConfig> The loaded configuration or defaults
60
+ */
61
+ async function loadConfig() {
62
+ // Try TypeScript config first, then JavaScript
63
+ const tsConfigPath = path.join(process.cwd(), 'testivai.config.ts');
64
+ const jsConfigPath = path.join(process.cwd(), 'testivai.config.js');
65
+ try {
66
+ let configPath;
67
+ let configModule;
68
+ // Check for TypeScript config
69
+ if (await fs.pathExists(tsConfigPath)) {
70
+ configPath = tsConfigPath;
71
+ }
72
+ else if (await fs.pathExists(jsConfigPath)) {
73
+ configPath = jsConfigPath;
74
+ }
75
+ else {
76
+ console.log('⚠️ No testivai.config.ts or testivai.config.js found, using defaults');
77
+ return DEFAULT_CONFIG;
78
+ }
79
+ // Load configuration based on file type
80
+ if (configPath.endsWith('.js')) {
81
+ // For .js files, use require to get CommonJS module
82
+ // Clear require cache to ensure fresh load
83
+ delete require.cache[require.resolve(configPath)];
84
+ configModule = require(configPath);
85
+ }
86
+ else {
87
+ // For .ts files, use dynamic import (ES module)
88
+ configModule = await Promise.resolve(`${configPath}`).then(s => __importStar(require(s)));
89
+ }
90
+ const config = configModule.default || configModule;
91
+ // Validate and merge with defaults
92
+ return validateAndMergeConfig(config);
93
+ }
94
+ catch (error) {
95
+ console.warn('⚠️ Failed to load testivai config, using defaults:', error);
96
+ return DEFAULT_CONFIG;
97
+ }
98
+ }
99
+ /**
100
+ * Merge per-test configuration with project configuration
101
+ *
102
+ * @param projectConfig The project-level configuration
103
+ * @param testConfig Optional per-test configuration overrides
104
+ * @returns TestivAIConfig The effective configuration for this test
105
+ */
106
+ function mergeTestConfig(projectConfig, testConfig) {
107
+ if (!testConfig) {
108
+ return {
109
+ layout: projectConfig.layout,
110
+ ai: projectConfig.ai
111
+ };
112
+ }
113
+ return {
114
+ layout: {
115
+ ...projectConfig.layout,
116
+ ...testConfig.layout
117
+ },
118
+ ai: {
119
+ ...projectConfig.ai,
120
+ ...testConfig.ai
121
+ },
122
+ selectors: testConfig.selectors
123
+ };
124
+ }
125
+ /**
126
+ * Detect current environment (CI, development, production)
127
+ *
128
+ * @returns string The detected environment
129
+ */
130
+ function detectEnvironment() {
131
+ // Check for common CI environment variables
132
+ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
133
+ return 'ci';
134
+ }
135
+ // Check for production indicators
136
+ if (process.env.NODE_ENV === 'production') {
137
+ return 'production';
138
+ }
139
+ // Default to development
140
+ return 'development';
141
+ }
142
+ /**
143
+ * Apply environment-specific overrides to configuration
144
+ *
145
+ * @param config The base configuration
146
+ * @returns TestivAIProjectConfig Configuration with environment overrides applied
147
+ */
148
+ function applyEnvironmentOverrides(config) {
149
+ const environment = detectEnvironment();
150
+ if (!config.environments) {
151
+ return config;
152
+ }
153
+ // Type-safe environment override access
154
+ let envOverrides;
155
+ switch (environment) {
156
+ case 'ci':
157
+ envOverrides = config.environments.ci;
158
+ break;
159
+ case 'development':
160
+ envOverrides = config.environments.development;
161
+ break;
162
+ case 'production':
163
+ envOverrides = config.environments.production;
164
+ break;
165
+ }
166
+ if (!envOverrides) {
167
+ return config;
168
+ }
169
+ return {
170
+ layout: {
171
+ ...config.layout,
172
+ ...envOverrides.layout
173
+ },
174
+ ai: {
175
+ ...config.ai,
176
+ ...envOverrides.ai
177
+ },
178
+ environments: config.environments // Keep the original environments config
179
+ };
180
+ }
181
+ /**
182
+ * Validate configuration values and merge with defaults
183
+ *
184
+ * @param config The configuration to validate
185
+ * @returns TestivAIProjectConfig Validated configuration
186
+ */
187
+ function validateAndMergeConfig(config) {
188
+ // Guard against null/undefined config
189
+ if (!config) {
190
+ console.warn('⚠️ Config is null or undefined, using defaults');
191
+ return DEFAULT_CONFIG;
192
+ }
193
+ // Basic validation
194
+ const validatedConfig = {
195
+ layout: {
196
+ sensitivity: validateRange(config.layout?.sensitivity ?? DEFAULT_CONFIG.layout.sensitivity, 0, 4, 'layout.sensitivity', DEFAULT_CONFIG.layout.sensitivity),
197
+ tolerance: validateRange(config.layout?.tolerance ?? DEFAULT_CONFIG.layout.tolerance, 0, 100, 'layout.tolerance', DEFAULT_CONFIG.layout.tolerance),
198
+ selectorTolerances: config.layout?.selectorTolerances,
199
+ useRelativeTolerance: config.layout?.useRelativeTolerance,
200
+ relativeTolerance: config.layout?.relativeTolerance
201
+ },
202
+ ai: {
203
+ sensitivity: validateRange(config.ai?.sensitivity ?? DEFAULT_CONFIG.ai.sensitivity, 0, 4, 'ai.sensitivity', DEFAULT_CONFIG.ai.sensitivity),
204
+ confidence: validateRange(config.ai?.confidence ?? DEFAULT_CONFIG.ai.confidence, 0, 1, 'ai.confidence', DEFAULT_CONFIG.ai.confidence),
205
+ enableReasoning: config.ai?.enableReasoning
206
+ },
207
+ environments: config.environments
208
+ };
209
+ return applyEnvironmentOverrides(validatedConfig);
210
+ }
211
+ /**
212
+ * Validate that a number is within the expected range
213
+ *
214
+ * @param value The value to validate
215
+ * @param min Minimum allowed value
216
+ * @param max Maximum allowed value
217
+ * @param field Field name for error messages
218
+ * @param defaultValue Default value to use when validation fails
219
+ * @returns number The validated value
220
+ */
221
+ function validateRange(value, min, max, field, defaultValue) {
222
+ const numValue = Number(value);
223
+ if (isNaN(numValue)) {
224
+ console.warn(`⚠️ Invalid ${field}: ${value}, using default: ${defaultValue}`);
225
+ return defaultValue;
226
+ }
227
+ if (numValue < min || numValue > max) {
228
+ console.warn(`⚠️ ${field} must be between ${min} and ${max}, got ${numValue}, using default: ${defaultValue}`);
229
+ return defaultValue;
230
+ }
231
+ return numValue;
232
+ }
@@ -0,0 +1,7 @@
1
+ import { snapshot } from './snapshot';
2
+ export declare const testivai: {
3
+ witness: typeof snapshot;
4
+ };
5
+ export * from './types';
6
+ export * from './reporter';
7
+ export * from './ci';
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.testivai = void 0;
18
+ const snapshot_1 = require("./snapshot");
19
+ exports.testivai = {
20
+ witness: snapshot_1.snapshot,
21
+ };
22
+ __exportStar(require("./types"), exports);
23
+ __exportStar(require("./reporter"), exports);
24
+ __exportStar(require("./ci"), exports);
@@ -0,0 +1,2 @@
1
+ import type { Reporter, FullConfig, Suite, FullResult } from '@playwright/test/reporter';
2
+ export type { Reporter, FullConfig, Suite, FullResult };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,16 @@
1
+ import { Reporter, FullConfig, Suite, FullResult } from './reporter-types';
2
+ interface TestivaiReporterOptions {
3
+ apiUrl?: string;
4
+ apiKey?: string;
5
+ }
6
+ export declare class TestivAIPlaywrightReporter implements Reporter {
7
+ private options;
8
+ private gitInfo;
9
+ private browserInfo;
10
+ private runId;
11
+ private tempDir;
12
+ constructor(options?: TestivaiReporterOptions);
13
+ onBegin(config: FullConfig, suite: Suite): Promise<void>;
14
+ onEnd(result: FullResult): Promise<void>;
15
+ }
16
+ export {};
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.TestivAIPlaywrightReporter = void 0;
40
+ const fs = __importStar(require("fs-extra"));
41
+ const path = __importStar(require("path"));
42
+ const simple_git_1 = __importDefault(require("simple-git"));
43
+ const axios_1 = __importDefault(require("axios"));
44
+ const ci_1 = require("./ci");
45
+ class TestivAIPlaywrightReporter {
46
+ constructor(options = {}) {
47
+ this.gitInfo = null;
48
+ this.browserInfo = null;
49
+ this.runId = null;
50
+ this.tempDir = path.join(process.cwd(), '.testivai', 'temp');
51
+ this.options = {
52
+ apiUrl: options.apiUrl || process.env.TESTIVAI_API_URL,
53
+ apiKey: options.apiKey || process.env.TESTIVAI_API_KEY,
54
+ };
55
+ }
56
+ async onBegin(config, suite) {
57
+ if (!this.options.apiUrl || !this.options.apiKey) {
58
+ console.error('Testivai Reporter: API URL or API Key is not configured. Disabling reporter.');
59
+ this.options.apiUrl = undefined; // Disable reporter
60
+ return;
61
+ }
62
+ // 1. Clean temp directory
63
+ await fs.emptyDir(this.tempDir);
64
+ // 2. Capture Git metadata
65
+ try {
66
+ const git = (0, simple_git_1.default)();
67
+ const [branch, commit] = await Promise.all([
68
+ git.revparse(['--abbrev-ref', 'HEAD']),
69
+ git.revparse(['HEAD']),
70
+ ]);
71
+ this.gitInfo = { branch, commit };
72
+ }
73
+ catch (error) {
74
+ console.error('Testivai Reporter: Could not get Git information.', error);
75
+ this.gitInfo = { branch: 'unknown', commit: 'unknown' };
76
+ }
77
+ // 3. Capture Browser info from the first project
78
+ const project = suite.suites[0]?.project();
79
+ if (project) {
80
+ this.browserInfo = {
81
+ name: project.use.browserName || 'unknown',
82
+ version: 'unknown', // Playwright does not easily expose browser version
83
+ viewportWidth: project.use.viewport?.width || 0,
84
+ viewportHeight: project.use.viewport?.height || 0,
85
+ userAgent: project.use.userAgent || 'unknown',
86
+ os: 'unknown',
87
+ };
88
+ }
89
+ // 4. Get CI Run ID
90
+ this.runId = (0, ci_1.getCiRunId)();
91
+ if (this.runId) {
92
+ console.log(`Testivai Reporter: Detected CI environment. Run ID: ${this.runId}`);
93
+ }
94
+ }
95
+ async onEnd(result) {
96
+ if (!this.options.apiUrl) {
97
+ return; // Reporter is disabled
98
+ }
99
+ console.log('Testivai Reporter: Test run finished. Preparing to upload evidence...');
100
+ try {
101
+ const snapshotFiles = await fs.readdir(this.tempDir);
102
+ const jsonFiles = snapshotFiles.filter(f => f.endsWith('.json'));
103
+ if (jsonFiles.length === 0) {
104
+ console.log('Testivai Reporter: No snapshots found to upload.');
105
+ return;
106
+ }
107
+ const snapshots = [];
108
+ const filesToUpload = [];
109
+ for (const jsonFile of jsonFiles) {
110
+ const metadataPath = path.join(this.tempDir, jsonFile);
111
+ const metadata = await fs.readJson(metadataPath);
112
+ const domPath = metadata.files.dom;
113
+ const screenshotPath = metadata.files.screenshot;
114
+ const snapshotPayload = {
115
+ ...metadata,
116
+ dom: { html: await fs.readFile(domPath, 'utf-8') },
117
+ layout: metadata.layout,
118
+ testivaiConfig: metadata.testivaiConfig
119
+ };
120
+ snapshots.push(snapshotPayload);
121
+ filesToUpload.push({ filePath: screenshotPath, contentType: 'image/png' });
122
+ }
123
+ const batchPayload = {
124
+ git: this.gitInfo,
125
+ browser: this.browserInfo,
126
+ snapshots,
127
+ timestamp: Date.now(),
128
+ runId: this.runId,
129
+ };
130
+ // Start batch and get upload URLs
131
+ const startBatchResponse = await axios_1.default.post(`${this.options.apiUrl}/api/v1/ingest/start-batch`, batchPayload, {
132
+ headers: { 'Authorization': `Bearer ${this.options.apiKey}` },
133
+ });
134
+ const { batchId, uploadInstructions } = startBatchResponse.data;
135
+ // Upload files
136
+ const uploadPromises = filesToUpload.map((file, index) => {
137
+ const instruction = uploadInstructions[index];
138
+ return fs.readFile(file.filePath).then(buffer => axios_1.default.put(instruction.url, buffer, { headers: { 'Content-Type': file.contentType } }));
139
+ });
140
+ await Promise.all(uploadPromises);
141
+ // Finalize batch
142
+ await axios_1.default.post(`${this.options.apiUrl}/api/v1/ingest/finish-batch/${batchId}`, {}, {
143
+ headers: { 'Authorization': `Bearer ${this.options.apiKey}` },
144
+ });
145
+ console.log(`Testivai Reporter: Successfully uploaded ${snapshots.length} snapshots with Batch ID: ${batchId}`);
146
+ // Clean up temp files
147
+ await fs.emptyDir(this.tempDir);
148
+ console.log('Testivai Reporter: Cleaned up temporary evidence files.');
149
+ }
150
+ catch (error) {
151
+ console.error('Testivai Reporter: An error occurred during the onEnd hook:', error);
152
+ }
153
+ }
154
+ }
155
+ exports.TestivAIPlaywrightReporter = TestivAIPlaywrightReporter;
@@ -0,0 +1,12 @@
1
+ import { Page, TestInfo } from '@playwright/test';
2
+ import { TestivAIConfig } from './types';
3
+ /**
4
+ * Captures a snapshot of the page, including a screenshot, DOM, and layout data.
5
+ * The evidence is stored in a temporary directory for the reporter to process later.
6
+ *
7
+ * @param page The Playwright Page object.
8
+ * @param testInfo The Playwright TestInfo object, passed from the test.
9
+ * @param name An optional unique name for the snapshot. If not provided, a name is generated from the URL.
10
+ * @param config Optional TestivAI configuration for this snapshot (overrides project defaults).
11
+ */
12
+ export declare function snapshot(page: Page, testInfo: TestInfo, name?: string, config?: TestivAIConfig): Promise<void>;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.snapshot = snapshot;
37
+ const fs = __importStar(require("fs-extra"));
38
+ const path = __importStar(require("path"));
39
+ const url_1 = require("url");
40
+ const loader_1 = require("./config/loader");
41
+ /**
42
+ * Generates a safe filename from a URL.
43
+ * @param pageUrl The URL of the page.
44
+ * @returns A sanitized string suitable for a filename.
45
+ */
46
+ function getSnapshotNameFromUrl(pageUrl) {
47
+ // Handle data URIs, which are common in test environments
48
+ if (pageUrl.startsWith('data:')) {
49
+ return 'snapshot';
50
+ }
51
+ try {
52
+ const url = new url_1.URL(pageUrl);
53
+ const pathName = url.pathname.substring(1).replace(/\//g, '_'); // remove leading slash and replace others
54
+ return pathName || 'home';
55
+ }
56
+ catch (error) {
57
+ // Fallback for invalid URLs
58
+ return 'snapshot';
59
+ }
60
+ }
61
+ /**
62
+ * Captures a snapshot of the page, including a screenshot, DOM, and layout data.
63
+ * The evidence is stored in a temporary directory for the reporter to process later.
64
+ *
65
+ * @param page The Playwright Page object.
66
+ * @param testInfo The Playwright TestInfo object, passed from the test.
67
+ * @param name An optional unique name for the snapshot. If not provided, a name is generated from the URL.
68
+ * @param config Optional TestivAI configuration for this snapshot (overrides project defaults).
69
+ */
70
+ async function snapshot(page, testInfo, name, config) {
71
+ // Load project configuration and merge with test-specific overrides
72
+ const projectConfig = await (0, loader_1.loadConfig)();
73
+ const effectiveConfig = (0, loader_1.mergeTestConfig)(projectConfig, config);
74
+ const outputDir = path.join(process.cwd(), '.testivai', 'temp');
75
+ await fs.ensureDir(outputDir);
76
+ const snapshotName = name || getSnapshotNameFromUrl(page.url());
77
+ const timestamp = Date.now();
78
+ const safeName = snapshotName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
79
+ const baseFilename = `${timestamp}_${safeName}`;
80
+ // 1. Capture full-page screenshot
81
+ const screenshotPath = path.join(outputDir, `${baseFilename}.png`);
82
+ await page.screenshot({ path: screenshotPath, fullPage: true });
83
+ // 2. Dump full-page DOM
84
+ const domPath = path.join(outputDir, `${baseFilename}.html`);
85
+ const htmlContent = await page.content();
86
+ await fs.writeFile(domPath, htmlContent);
87
+ // 3. Extract bounding boxes for requested selectors
88
+ const selectors = effectiveConfig.selectors ?? ['body'];
89
+ const layout = {};
90
+ for (const selector of selectors) {
91
+ const element = page.locator(selector).first();
92
+ const boundingBox = await element.boundingBox();
93
+ if (boundingBox) {
94
+ layout[selector] = {
95
+ ...boundingBox,
96
+ top: boundingBox.y,
97
+ left: boundingBox.x,
98
+ right: boundingBox.x + boundingBox.width,
99
+ bottom: boundingBox.y + boundingBox.height,
100
+ };
101
+ }
102
+ }
103
+ // 4. Save metadata with configuration
104
+ const metadataPath = path.join(outputDir, `${baseFilename}.json`);
105
+ const metadata = {
106
+ snapshotName,
107
+ testName: testInfo.title,
108
+ timestamp,
109
+ url: page.url(),
110
+ viewport: page.viewportSize() || undefined,
111
+ };
112
+ await fs.writeJson(metadataPath, {
113
+ ...metadata,
114
+ files: {
115
+ screenshot: screenshotPath,
116
+ dom: domPath,
117
+ },
118
+ layout,
119
+ // Store the effective configuration for the reporter
120
+ testivaiConfig: effectiveConfig
121
+ });
122
+ }