@testivai/witness-playwright 1.0.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 (61) hide show
  1. package/__tests__/.gitkeep +0 -0
  2. package/__tests__/config-integration.spec.ts +102 -0
  3. package/__tests__/snapshot.spec.d.ts +1 -0
  4. package/__tests__/snapshot.spec.js +81 -0
  5. package/__tests__/snapshot.spec.ts +58 -0
  6. package/__tests__/unit/ci.spec.d.ts +1 -0
  7. package/__tests__/unit/ci.spec.js +35 -0
  8. package/__tests__/unit/ci.spec.ts +40 -0
  9. package/__tests__/unit/reporter.spec.d.ts +1 -0
  10. package/__tests__/unit/reporter.spec.js +37 -0
  11. package/__tests__/unit/reporter.spec.ts +43 -0
  12. package/__tests__/unit/structureAnalyzer.spec.js +212 -0
  13. package/__tests__/unit/types.spec.ts +179 -0
  14. package/dist/__tests__/unit/ci.spec.d.ts +1 -0
  15. package/dist/__tests__/unit/ci.spec.js +226 -0
  16. package/dist/__tests__/unit/compression.spec.d.ts +4 -0
  17. package/dist/__tests__/unit/compression.spec.js +46 -0
  18. package/dist/ci.d.ts +30 -0
  19. package/dist/ci.js +117 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.js +47 -0
  22. package/dist/cli/init.d.ts +3 -0
  23. package/dist/cli/init.js +158 -0
  24. package/dist/config/loader.d.ts +29 -0
  25. package/dist/config/loader.js +251 -0
  26. package/dist/domAnalyzer.d.ts +10 -0
  27. package/dist/domAnalyzer.js +285 -0
  28. package/dist/index.d.ts +7 -0
  29. package/dist/index.js +11 -0
  30. package/dist/reporter-entry.d.ts +2 -0
  31. package/dist/reporter-entry.js +5 -0
  32. package/dist/reporter-types.d.ts +2 -0
  33. package/dist/reporter-types.js +2 -0
  34. package/dist/reporter.d.ts +21 -0
  35. package/dist/reporter.js +249 -0
  36. package/dist/snapshot.d.ts +12 -0
  37. package/dist/snapshot.js +601 -0
  38. package/dist/structureAnalyzer.d.ts +12 -0
  39. package/dist/structureAnalyzer.js +288 -0
  40. package/dist/types.d.ts +368 -0
  41. package/dist/types.js +10 -0
  42. package/examples/structure-analysis-example.spec.ts +118 -0
  43. package/examples/structure-analysis.config.ts +159 -0
  44. package/jest.config.js +8 -0
  45. package/package.json +51 -0
  46. package/playwright.config.ts +11 -0
  47. package/src/__tests__/unit/ci.spec.ts +257 -0
  48. package/src/__tests__/unit/compression.spec.ts +52 -0
  49. package/src/ci.ts +140 -0
  50. package/src/cli/index.ts +49 -0
  51. package/src/cli/init.ts +131 -0
  52. package/src/config/loader.ts +238 -0
  53. package/src/index.ts +14 -0
  54. package/src/reporter-entry.ts +6 -0
  55. package/src/reporter-types.ts +5 -0
  56. package/src/reporter.ts +251 -0
  57. package/src/snapshot.ts +632 -0
  58. package/src/structureAnalyzer.ts +338 -0
  59. package/src/types.ts +388 -0
  60. package/tsconfig.jest.json +7 -0
  61. package/tsconfig.json +20 -0
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.createConfigFile = createConfigFile;
38
+ const fs = __importStar(require("fs-extra"));
39
+ const path = __importStar(require("path"));
40
+ /**
41
+ * Basic CLI init script for TestivAI Playwright SDK
42
+ * Creates a testivai.config.ts file with sensible defaults
43
+ */
44
+ const DEFAULT_CONFIG = `/**
45
+ * TestivAI Configuration File
46
+ *
47
+ * This file configures how TestivAI analyzes visual differences in your tests.
48
+ *
49
+ * Layout Settings:
50
+ * - sensitivity: 0-4 scale (0=strict/precise, 4=very lenient)
51
+ * - tolerance: Base pixel tolerance for layout differences
52
+ *
53
+ * AI Settings:
54
+ * - sensitivity: 0-4 scale (0=conservative, 4=aggressive)
55
+ * - confidence: 0.0-1.0 scale (minimum confidence required for AI_BUG verdict)
56
+ *
57
+ * Custom Configuration Examples:
58
+ *
59
+ * // For strict testing (critical components):
60
+ * export default {
61
+ * layout: { sensitivity: 0, tolerance: 0.5 },
62
+ * ai: { sensitivity: 0, confidence: 0.9 }
63
+ * };
64
+ *
65
+ * // For lenient testing (dynamic content):
66
+ * export default {
67
+ * layout: { sensitivity: 4, tolerance: 3.0 },
68
+ * ai: { sensitivity: 3, confidence: 0.6 }
69
+ * };
70
+ *
71
+ * // Per-selector tolerances (advanced):
72
+ * export default {
73
+ * layout: {
74
+ * sensitivity: 2,
75
+ * tolerance: 1.0,
76
+ * selectorTolerances: {
77
+ * '.carousel': 4.0, // Carousel components shift
78
+ * '.dropdown': 2.0, // Dropdowns can vary
79
+ * '.tooltip': 5.0, // Tooltips are highly variable
80
+ * '.submit-button': 0.5 // Critical buttons need precision
81
+ * }
82
+ * },
83
+ * ai: { sensitivity: 2, confidence: 0.7 }
84
+ * };
85
+ *
86
+ * // Environment-specific overrides:
87
+ * export default {
88
+ * layout: { sensitivity: 2, tolerance: 1.0 },
89
+ * ai: { sensitivity: 2, confidence: 0.7 },
90
+ * environments: {
91
+ * ci: {
92
+ * layout: { sensitivity: 1, tolerance: 0.5 }, // Stricter in CI
93
+ * ai: { sensitivity: 1, confidence: 0.8 }
94
+ * },
95
+ * development: {
96
+ * layout: { sensitivity: 3, tolerance: 2.0 }, // More lenient locally
97
+ * ai: { sensitivity: 3, confidence: 0.6 }
98
+ * }
99
+ * }
100
+ * };
101
+ */
102
+
103
+ export default {
104
+ // Your TestivAI API key - get this from the dashboard after creating a project
105
+ apiKey: process.env.TESTIVAI_API_KEY || '',
106
+
107
+ // API endpoint - automatically points to production
108
+ apiUrl: process.env.TESTIVAI_API_URL || 'https://core-api.testiv.ai',
109
+
110
+ layout: {
111
+ sensitivity: 2, // Balanced sensitivity (0-4 scale)
112
+ tolerance: 1.0, // 1 pixel base tolerance
113
+ },
114
+ ai: {
115
+ sensitivity: 2, // Balanced AI analysis (0-4 scale)
116
+ confidence: 0.7, // 70% confidence required for AI_BUG
117
+ }
118
+ };
119
+ `;
120
+ async function createConfigFile() {
121
+ const configPath = path.join(process.cwd(), 'testivai.config.ts');
122
+ // Check if config already exists
123
+ if (await fs.pathExists(configPath)) {
124
+ console.log('❌ TestivAI config already exists at:', configPath);
125
+ console.log(' Delete it first if you want to regenerate it.');
126
+ process.exit(1);
127
+ }
128
+ try {
129
+ // Create the config file
130
+ await fs.writeFile(configPath, DEFAULT_CONFIG, 'utf8');
131
+ console.log('✅ TestivAI configuration created successfully!');
132
+ console.log('📁 Config file:', configPath);
133
+ console.log('');
134
+ console.log('📖 Next steps:');
135
+ console.log(' 1. Set up your API key:');
136
+ console.log(' TESTIVAI_API_KEY=tstvai-your-key-here');
137
+ console.log('');
138
+ console.log(' 2. Update your playwright.config.ts to use TestivAI reporter:');
139
+ console.log(' reporter: [[\'@testivai/witness-playwright/reporter\']]');
140
+ console.log('');
141
+ console.log(' 3. Review and customize testivai.config.ts');
142
+ console.log(' 4. Run your tests: npx playwright test');
143
+ console.log('');
144
+ console.log('💡 Get your API key from: https://dashboard.testiv.ai');
145
+ console.log('💡 The SDK automatically connects to the production API - no URL configuration needed!');
146
+ }
147
+ catch (error) {
148
+ console.error('❌ Failed to create configuration file:', error);
149
+ process.exit(1);
150
+ }
151
+ }
152
+ // Main execution
153
+ if (require.main === module) {
154
+ createConfigFile().catch(error => {
155
+ console.error('❌ CLI init failed:', error);
156
+ process.exit(1);
157
+ });
158
+ }
@@ -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,251 @@
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
+ // @renamed: dom → structure (IP protection)
112
+ structure: projectConfig.structure,
113
+ performanceMetrics: projectConfig.performanceMetrics
114
+ };
115
+ }
116
+ return {
117
+ layout: {
118
+ ...projectConfig.layout,
119
+ ...testConfig.layout
120
+ },
121
+ ai: {
122
+ ...projectConfig.ai,
123
+ ...testConfig.ai
124
+ },
125
+ // @renamed: dom → structure (IP protection)
126
+ structure: {
127
+ ...projectConfig.structure,
128
+ ...testConfig.structure
129
+ },
130
+ performanceMetrics: {
131
+ ...projectConfig.performanceMetrics,
132
+ ...testConfig.performanceMetrics
133
+ },
134
+ selectors: testConfig.selectors,
135
+ useCDP: testConfig.useCDP
136
+ };
137
+ }
138
+ /**
139
+ * Detect current environment (CI, development, production)
140
+ *
141
+ * @returns string The detected environment
142
+ */
143
+ function detectEnvironment() {
144
+ // Check for common CI environment variables
145
+ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
146
+ return 'ci';
147
+ }
148
+ // Check for production indicators
149
+ if (process.env.NODE_ENV === 'production') {
150
+ return 'production';
151
+ }
152
+ // Default to development
153
+ return 'development';
154
+ }
155
+ /**
156
+ * Apply environment-specific overrides to configuration
157
+ *
158
+ * @param config The base configuration
159
+ * @returns TestivAIProjectConfig Configuration with environment overrides applied
160
+ */
161
+ function applyEnvironmentOverrides(config) {
162
+ const environment = detectEnvironment();
163
+ if (!config.environments) {
164
+ return config;
165
+ }
166
+ // Type-safe environment override access
167
+ let envOverrides;
168
+ switch (environment) {
169
+ case 'ci':
170
+ envOverrides = config.environments.ci;
171
+ break;
172
+ case 'development':
173
+ envOverrides = config.environments.development;
174
+ break;
175
+ case 'production':
176
+ envOverrides = config.environments.production;
177
+ break;
178
+ }
179
+ if (!envOverrides) {
180
+ return config;
181
+ }
182
+ return {
183
+ layout: {
184
+ ...config.layout,
185
+ ...envOverrides.layout
186
+ },
187
+ ai: {
188
+ ...config.ai,
189
+ ...envOverrides.ai
190
+ },
191
+ environments: config.environments // Keep the original environments config
192
+ };
193
+ }
194
+ /**
195
+ * Validate configuration values and merge with defaults
196
+ *
197
+ * @param config The configuration to validate
198
+ * @returns TestivAIProjectConfig Validated configuration
199
+ */
200
+ function validateAndMergeConfig(config) {
201
+ // Guard against null/undefined config
202
+ if (!config) {
203
+ console.warn('⚠️ Config is null or undefined, using defaults');
204
+ return DEFAULT_CONFIG;
205
+ }
206
+ // Basic validation
207
+ const validatedConfig = {
208
+ // API configuration with production default
209
+ apiKey: config.apiKey,
210
+ apiUrl: config.apiUrl || 'https://core-api.testiv.ai',
211
+ layout: {
212
+ sensitivity: validateRange(config.layout?.sensitivity ?? DEFAULT_CONFIG.layout.sensitivity, 0, 4, 'layout.sensitivity', DEFAULT_CONFIG.layout.sensitivity),
213
+ tolerance: validateRange(config.layout?.tolerance ?? DEFAULT_CONFIG.layout.tolerance, 0, 100, 'layout.tolerance', DEFAULT_CONFIG.layout.tolerance),
214
+ selectorTolerances: config.layout?.selectorTolerances,
215
+ useRelativeTolerance: config.layout?.useRelativeTolerance,
216
+ relativeTolerance: config.layout?.relativeTolerance
217
+ },
218
+ ai: {
219
+ sensitivity: validateRange(config.ai?.sensitivity ?? DEFAULT_CONFIG.ai.sensitivity, 0, 4, 'ai.sensitivity', DEFAULT_CONFIG.ai.sensitivity),
220
+ confidence: validateRange(config.ai?.confidence ?? DEFAULT_CONFIG.ai.confidence, 0, 1, 'ai.confidence', DEFAULT_CONFIG.ai.confidence),
221
+ enableReasoning: config.ai?.enableReasoning
222
+ },
223
+ // @renamed: dom → structure (IP protection)
224
+ structure: config.structure, // Pass through structure analysis configuration as-is
225
+ performanceMetrics: config.performanceMetrics, // Pass through performance metrics configuration as-is
226
+ environments: config.environments
227
+ };
228
+ return applyEnvironmentOverrides(validatedConfig);
229
+ }
230
+ /**
231
+ * Validate that a number is within the expected range
232
+ *
233
+ * @param value The value to validate
234
+ * @param min Minimum allowed value
235
+ * @param max Maximum allowed value
236
+ * @param field Field name for error messages
237
+ * @param defaultValue Default value to use when validation fails
238
+ * @returns number The validated value
239
+ */
240
+ function validateRange(value, min, max, field, defaultValue) {
241
+ const numValue = Number(value);
242
+ if (isNaN(numValue)) {
243
+ console.warn(`⚠️ Invalid ${field}: ${value}, using default: ${defaultValue}`);
244
+ return defaultValue;
245
+ }
246
+ if (numValue < min || numValue > max) {
247
+ console.warn(`⚠️ ${field} must be between ${min} and ${max}, got ${numValue}, using default: ${defaultValue}`);
248
+ return defaultValue;
249
+ }
250
+ return numValue;
251
+ }
@@ -0,0 +1,10 @@
1
+ import { Page } from '@playwright/test';
2
+ import { DOMAnalysisConfig, DOMAnalysis, DOMChange } from './types';
3
+ /**
4
+ * Analyzes DOM structure and generates fingerprint
5
+ */
6
+ export declare function analyzeDOM(page: Page, config?: DOMAnalysisConfig): Promise<DOMAnalysis>;
7
+ /**
8
+ * Compare two DOM analyses and identify changes
9
+ */
10
+ export declare function compareDOMAnalysis(baseline: DOMAnalysis, current: DOMAnalysis): DOMChange[];