@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.
- package/README.md +65 -0
- package/__tests__/.gitkeep +0 -0
- package/__tests__/config-integration.spec.ts +102 -0
- package/__tests__/snapshot.spec.d.ts +1 -0
- package/__tests__/snapshot.spec.js +81 -0
- package/__tests__/snapshot.spec.ts +58 -0
- package/__tests__/unit/ci.spec.d.ts +1 -0
- package/__tests__/unit/ci.spec.js +35 -0
- package/__tests__/unit/ci.spec.ts +40 -0
- package/__tests__/unit/reporter.spec.d.ts +1 -0
- package/__tests__/unit/reporter.spec.js +37 -0
- package/__tests__/unit/reporter.spec.ts +43 -0
- package/dist/ci.d.ts +10 -0
- package/dist/ci.js +35 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +146 -0
- package/dist/config/loader.d.ts +29 -0
- package/dist/config/loader.js +232 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +24 -0
- package/dist/reporter-types.d.ts +2 -0
- package/dist/reporter-types.js +2 -0
- package/dist/reporter.d.ts +16 -0
- package/dist/reporter.js +155 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +122 -0
- package/dist/types.d.ts +181 -0
- package/dist/types.js +10 -0
- package/jest.config.js +11 -0
- package/package.json +47 -0
- package/playwright.config.ts +11 -0
- package/progress.md +620 -0
- package/src/ci.ts +34 -0
- package/src/cli/init.ts +119 -0
- package/src/config/loader.ts +219 -0
- package/src/index.ts +9 -0
- package/src/reporter-types.ts +5 -0
- package/src/reporter.ts +148 -0
- package/src/snapshot.ts +103 -0
- package/src/types.ts +193 -0
- package/test-results/.last-run.json +4 -0
- package/tsconfig.jest.json +7 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED
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,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 {};
|
package/dist/reporter.js
ADDED
|
@@ -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>;
|
package/dist/snapshot.js
ADDED
|
@@ -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
|
+
}
|