@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.
- 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/__tests__/unit/structureAnalyzer.spec.js +212 -0
- package/__tests__/unit/types.spec.ts +179 -0
- package/dist/__tests__/unit/ci.spec.d.ts +1 -0
- package/dist/__tests__/unit/ci.spec.js +226 -0
- package/dist/__tests__/unit/compression.spec.d.ts +4 -0
- package/dist/__tests__/unit/compression.spec.js +46 -0
- package/dist/ci.d.ts +30 -0
- package/dist/ci.js +117 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +47 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +158 -0
- package/dist/config/loader.d.ts +29 -0
- package/dist/config/loader.js +251 -0
- package/dist/domAnalyzer.d.ts +10 -0
- package/dist/domAnalyzer.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +11 -0
- package/dist/reporter-entry.d.ts +2 -0
- package/dist/reporter-entry.js +5 -0
- package/dist/reporter-types.d.ts +2 -0
- package/dist/reporter-types.js +2 -0
- package/dist/reporter.d.ts +21 -0
- package/dist/reporter.js +249 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +601 -0
- package/dist/structureAnalyzer.d.ts +12 -0
- package/dist/structureAnalyzer.js +288 -0
- package/dist/types.d.ts +368 -0
- package/dist/types.js +10 -0
- package/examples/structure-analysis-example.spec.ts +118 -0
- package/examples/structure-analysis.config.ts +159 -0
- package/jest.config.js +8 -0
- package/package.json +51 -0
- package/playwright.config.ts +11 -0
- package/src/__tests__/unit/ci.spec.ts +257 -0
- package/src/__tests__/unit/compression.spec.ts +52 -0
- package/src/ci.ts +140 -0
- package/src/cli/index.ts +49 -0
- package/src/cli/init.ts +131 -0
- package/src/config/loader.ts +238 -0
- package/src/index.ts +14 -0
- package/src/reporter-entry.ts +6 -0
- package/src/reporter-types.ts +5 -0
- package/src/reporter.ts +251 -0
- package/src/snapshot.ts +632 -0
- package/src/structureAnalyzer.ts +338 -0
- package/src/types.ts +388 -0
- package/tsconfig.jest.json +7 -0
- package/tsconfig.json +20 -0
package/src/ci.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for detecting CI/CD environments and extracting a unique run identifier.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CI environment information for integration feedback (e.g., GitHub commit statuses, PR comments).
|
|
7
|
+
*/
|
|
8
|
+
export interface CiInfo {
|
|
9
|
+
/** CI provider name */
|
|
10
|
+
provider: string;
|
|
11
|
+
/** Pull/Merge request number (if available) */
|
|
12
|
+
prNumber?: number;
|
|
13
|
+
/** URL to the CI run (if available) */
|
|
14
|
+
runUrl?: string;
|
|
15
|
+
/** CI build/run identifier */
|
|
16
|
+
buildId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets a unique identifier for the current CI run.
|
|
21
|
+
* This ID is used to group snapshots from parallel shards into a single batch.
|
|
22
|
+
*
|
|
23
|
+
* @returns A unique string identifier for the CI run, or null if not in a known CI environment.
|
|
24
|
+
*/
|
|
25
|
+
export function getCiRunId(): string | null {
|
|
26
|
+
// GitHub Actions
|
|
27
|
+
if (process.env.GITHUB_RUN_ID) {
|
|
28
|
+
return `github-${process.env.GITHUB_RUN_ID}`;
|
|
29
|
+
}
|
|
30
|
+
// GitLab CI
|
|
31
|
+
if (process.env.GITLAB_CI) {
|
|
32
|
+
return `gitlab-${process.env.CI_PIPELINE_ID}`;
|
|
33
|
+
}
|
|
34
|
+
// Jenkins
|
|
35
|
+
if (process.env.JENKINS_URL) {
|
|
36
|
+
return `jenkins-${process.env.BUILD_ID}`;
|
|
37
|
+
}
|
|
38
|
+
// CircleCI
|
|
39
|
+
if (process.env.CIRCLECI) {
|
|
40
|
+
return `circleci-${process.env.CIRCLE_WORKFLOW_ID}`;
|
|
41
|
+
}
|
|
42
|
+
// Travis CI
|
|
43
|
+
if (process.env.TRAVIS) {
|
|
44
|
+
return `travis-${process.env.TRAVIS_BUILD_ID}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gets detailed CI environment information including PR number and run URL.
|
|
52
|
+
* Used for integration feedback (GitHub commit statuses, PR comments, etc.).
|
|
53
|
+
*
|
|
54
|
+
* @returns CiInfo object if in a known CI environment, or null otherwise.
|
|
55
|
+
*/
|
|
56
|
+
export function getCiInfo(): CiInfo | null {
|
|
57
|
+
// GitHub Actions
|
|
58
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
59
|
+
const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
|
|
60
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
61
|
+
const runId = process.env.GITHUB_RUN_ID;
|
|
62
|
+
|
|
63
|
+
// PR number: available via GITHUB_EVENT_NUMBER in pull_request events,
|
|
64
|
+
// or from GITHUB_REF (refs/pull/<number>/merge)
|
|
65
|
+
let prNumber: number | undefined;
|
|
66
|
+
if (process.env.GITHUB_EVENT_NUMBER) {
|
|
67
|
+
prNumber = parseInt(process.env.GITHUB_EVENT_NUMBER, 10) || undefined;
|
|
68
|
+
} else if (process.env.GITHUB_REF?.startsWith('refs/pull/')) {
|
|
69
|
+
const match = process.env.GITHUB_REF.match(/^refs\/pull\/(\d+)\//);
|
|
70
|
+
if (match) {
|
|
71
|
+
prNumber = parseInt(match[1], 10);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
provider: 'github_actions',
|
|
77
|
+
prNumber,
|
|
78
|
+
runUrl: repo && runId ? `${serverUrl}/${repo}/actions/runs/${runId}` : undefined,
|
|
79
|
+
buildId: runId,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// GitLab CI
|
|
84
|
+
if (process.env.GITLAB_CI) {
|
|
85
|
+
const prNumber = process.env.CI_MERGE_REQUEST_IID
|
|
86
|
+
? parseInt(process.env.CI_MERGE_REQUEST_IID, 10) || undefined
|
|
87
|
+
: undefined;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
provider: 'gitlab_ci',
|
|
91
|
+
prNumber,
|
|
92
|
+
runUrl: process.env.CI_PIPELINE_URL || undefined,
|
|
93
|
+
buildId: process.env.CI_PIPELINE_ID,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// CircleCI
|
|
98
|
+
if (process.env.CIRCLECI) {
|
|
99
|
+
const prNumber = process.env.CIRCLE_PULL_REQUEST
|
|
100
|
+
? parseInt(process.env.CIRCLE_PULL_REQUEST.split('/').pop() || '', 10) || undefined
|
|
101
|
+
: undefined;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
provider: 'circleci',
|
|
105
|
+
prNumber,
|
|
106
|
+
runUrl: process.env.CIRCLE_BUILD_URL || undefined,
|
|
107
|
+
buildId: process.env.CIRCLE_WORKFLOW_ID,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Jenkins
|
|
112
|
+
if (process.env.JENKINS_URL) {
|
|
113
|
+
const prNumber = process.env.CHANGE_ID
|
|
114
|
+
? parseInt(process.env.CHANGE_ID, 10) || undefined
|
|
115
|
+
: undefined;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
provider: 'jenkins',
|
|
119
|
+
prNumber,
|
|
120
|
+
runUrl: process.env.BUILD_URL || undefined,
|
|
121
|
+
buildId: process.env.BUILD_ID,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Travis CI
|
|
126
|
+
if (process.env.TRAVIS) {
|
|
127
|
+
const prNumber = process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== 'false'
|
|
128
|
+
? parseInt(process.env.TRAVIS_PULL_REQUEST, 10) || undefined
|
|
129
|
+
: undefined;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
provider: 'travis_ci',
|
|
133
|
+
prNumber,
|
|
134
|
+
runUrl: process.env.TRAVIS_BUILD_WEB_URL || undefined,
|
|
135
|
+
buildId: process.env.TRAVIS_BUILD_ID,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { createConfigFile } from './init';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
// Display banner
|
|
10
|
+
const showBanner = () => {
|
|
11
|
+
console.log();
|
|
12
|
+
console.log(chalk.cyan.bold(' TestivAI'));
|
|
13
|
+
console.log(chalk.gray(' Catch Visual Bugs Automatically'));
|
|
14
|
+
console.log(chalk.gray(' AI that catches real bugs, ignores the noise.'));
|
|
15
|
+
console.log();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('testivai')
|
|
20
|
+
.description('TestivAI Playwright SDK CLI')
|
|
21
|
+
.version(require('../../package.json').version)
|
|
22
|
+
.hook('preAction', () => {
|
|
23
|
+
if (!process.argv.includes('--quiet') && !process.argv.includes('-q')) {
|
|
24
|
+
showBanner();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Global options
|
|
29
|
+
program
|
|
30
|
+
.option('-q, --quiet', 'Suppress banner output (ideal for CI)');
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('init')
|
|
34
|
+
.description('Initialize TestivAI configuration file')
|
|
35
|
+
.action(async () => {
|
|
36
|
+
try {
|
|
37
|
+
await createConfigFile();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('❌ Failed to initialize:', error);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
program.parse(process.argv);
|
|
45
|
+
|
|
46
|
+
// Show help if no command provided
|
|
47
|
+
if (!process.argv.slice(2).length) {
|
|
48
|
+
program.outputHelp();
|
|
49
|
+
}
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as fs from 'fs-extra';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Basic CLI init script for TestivAI Playwright SDK
|
|
8
|
+
* Creates a testivai.config.ts file with sensible defaults
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONFIG = `/**
|
|
12
|
+
* TestivAI Configuration File
|
|
13
|
+
*
|
|
14
|
+
* This file configures how TestivAI analyzes visual differences in your tests.
|
|
15
|
+
*
|
|
16
|
+
* Layout Settings:
|
|
17
|
+
* - sensitivity: 0-4 scale (0=strict/precise, 4=very lenient)
|
|
18
|
+
* - tolerance: Base pixel tolerance for layout differences
|
|
19
|
+
*
|
|
20
|
+
* AI Settings:
|
|
21
|
+
* - sensitivity: 0-4 scale (0=conservative, 4=aggressive)
|
|
22
|
+
* - confidence: 0.0-1.0 scale (minimum confidence required for AI_BUG verdict)
|
|
23
|
+
*
|
|
24
|
+
* Custom Configuration Examples:
|
|
25
|
+
*
|
|
26
|
+
* // For strict testing (critical components):
|
|
27
|
+
* export default {
|
|
28
|
+
* layout: { sensitivity: 0, tolerance: 0.5 },
|
|
29
|
+
* ai: { sensitivity: 0, confidence: 0.9 }
|
|
30
|
+
* };
|
|
31
|
+
*
|
|
32
|
+
* // For lenient testing (dynamic content):
|
|
33
|
+
* export default {
|
|
34
|
+
* layout: { sensitivity: 4, tolerance: 3.0 },
|
|
35
|
+
* ai: { sensitivity: 3, confidence: 0.6 }
|
|
36
|
+
* };
|
|
37
|
+
*
|
|
38
|
+
* // Per-selector tolerances (advanced):
|
|
39
|
+
* export default {
|
|
40
|
+
* layout: {
|
|
41
|
+
* sensitivity: 2,
|
|
42
|
+
* tolerance: 1.0,
|
|
43
|
+
* selectorTolerances: {
|
|
44
|
+
* '.carousel': 4.0, // Carousel components shift
|
|
45
|
+
* '.dropdown': 2.0, // Dropdowns can vary
|
|
46
|
+
* '.tooltip': 5.0, // Tooltips are highly variable
|
|
47
|
+
* '.submit-button': 0.5 // Critical buttons need precision
|
|
48
|
+
* }
|
|
49
|
+
* },
|
|
50
|
+
* ai: { sensitivity: 2, confidence: 0.7 }
|
|
51
|
+
* };
|
|
52
|
+
*
|
|
53
|
+
* // Environment-specific overrides:
|
|
54
|
+
* export default {
|
|
55
|
+
* layout: { sensitivity: 2, tolerance: 1.0 },
|
|
56
|
+
* ai: { sensitivity: 2, confidence: 0.7 },
|
|
57
|
+
* environments: {
|
|
58
|
+
* ci: {
|
|
59
|
+
* layout: { sensitivity: 1, tolerance: 0.5 }, // Stricter in CI
|
|
60
|
+
* ai: { sensitivity: 1, confidence: 0.8 }
|
|
61
|
+
* },
|
|
62
|
+
* development: {
|
|
63
|
+
* layout: { sensitivity: 3, tolerance: 2.0 }, // More lenient locally
|
|
64
|
+
* ai: { sensitivity: 3, confidence: 0.6 }
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* };
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
export default {
|
|
71
|
+
// Your TestivAI API key - get this from the dashboard after creating a project
|
|
72
|
+
apiKey: process.env.TESTIVAI_API_KEY || '',
|
|
73
|
+
|
|
74
|
+
// API endpoint - automatically points to production
|
|
75
|
+
apiUrl: process.env.TESTIVAI_API_URL || 'https://core-api.testiv.ai',
|
|
76
|
+
|
|
77
|
+
layout: {
|
|
78
|
+
sensitivity: 2, // Balanced sensitivity (0-4 scale)
|
|
79
|
+
tolerance: 1.0, // 1 pixel base tolerance
|
|
80
|
+
},
|
|
81
|
+
ai: {
|
|
82
|
+
sensitivity: 2, // Balanced AI analysis (0-4 scale)
|
|
83
|
+
confidence: 0.7, // 70% confidence required for AI_BUG
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
async function createConfigFile(): Promise<void> {
|
|
89
|
+
const configPath = path.join(process.cwd(), 'testivai.config.ts');
|
|
90
|
+
|
|
91
|
+
// Check if config already exists
|
|
92
|
+
if (await fs.pathExists(configPath)) {
|
|
93
|
+
console.log('❌ TestivAI config already exists at:', configPath);
|
|
94
|
+
console.log(' Delete it first if you want to regenerate it.');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Create the config file
|
|
100
|
+
await fs.writeFile(configPath, DEFAULT_CONFIG, 'utf8');
|
|
101
|
+
console.log('✅ TestivAI configuration created successfully!');
|
|
102
|
+
console.log('📁 Config file:', configPath);
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log('📖 Next steps:');
|
|
105
|
+
console.log(' 1. Set up your API key:');
|
|
106
|
+
console.log(' TESTIVAI_API_KEY=tstvai-your-key-here');
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(' 2. Update your playwright.config.ts to use TestivAI reporter:');
|
|
109
|
+
console.log(' reporter: [[\'@testivai/witness-playwright/reporter\']]');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(' 3. Review and customize testivai.config.ts');
|
|
112
|
+
console.log(' 4. Run your tests: npx playwright test');
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log('💡 Get your API key from: https://dashboard.testiv.ai');
|
|
115
|
+
console.log('💡 The SDK automatically connects to the production API - no URL configuration needed!');
|
|
116
|
+
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('❌ Failed to create configuration file:', error);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Main execution
|
|
124
|
+
if (require.main === module) {
|
|
125
|
+
createConfigFile().catch(error => {
|
|
126
|
+
console.error('❌ CLI init failed:', error);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { createConfigFile };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import * as fs from 'fs-extra';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { TestivAIProjectConfig, TestivAIConfig, LayoutConfig, AIConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default configuration when no config file is found
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_CONFIG: TestivAIProjectConfig = {
|
|
9
|
+
layout: {
|
|
10
|
+
sensitivity: 2, // Balanced sensitivity (0-4 scale)
|
|
11
|
+
tolerance: 1.0, // 1 pixel base tolerance
|
|
12
|
+
},
|
|
13
|
+
ai: {
|
|
14
|
+
sensitivity: 2, // Balanced AI analysis (0-4 scale)
|
|
15
|
+
confidence: 0.7, // 70% confidence required for AI_BUG
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load TestivAI configuration from file system
|
|
21
|
+
* Supports both .ts and .js config files
|
|
22
|
+
*
|
|
23
|
+
* @returns Promise<TestivAIProjectConfig> The loaded configuration or defaults
|
|
24
|
+
*/
|
|
25
|
+
export async function loadConfig(): Promise<TestivAIProjectConfig> {
|
|
26
|
+
// Try TypeScript config first, then JavaScript
|
|
27
|
+
const tsConfigPath = path.join(process.cwd(), 'testivai.config.ts');
|
|
28
|
+
const jsConfigPath = path.join(process.cwd(), 'testivai.config.js');
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
let configPath: string;
|
|
32
|
+
let configModule: any;
|
|
33
|
+
|
|
34
|
+
// Check for TypeScript config
|
|
35
|
+
if (await fs.pathExists(tsConfigPath)) {
|
|
36
|
+
configPath = tsConfigPath;
|
|
37
|
+
} else if (await fs.pathExists(jsConfigPath)) {
|
|
38
|
+
configPath = jsConfigPath;
|
|
39
|
+
} else {
|
|
40
|
+
console.log('⚠️ No testivai.config.ts or testivai.config.js found, using defaults');
|
|
41
|
+
return DEFAULT_CONFIG;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Load configuration based on file type
|
|
45
|
+
if (configPath.endsWith('.js')) {
|
|
46
|
+
// For .js files, use require to get CommonJS module
|
|
47
|
+
// Clear require cache to ensure fresh load
|
|
48
|
+
delete require.cache[require.resolve(configPath)];
|
|
49
|
+
configModule = require(configPath);
|
|
50
|
+
} else {
|
|
51
|
+
// For .ts files, use dynamic import (ES module)
|
|
52
|
+
configModule = await import(configPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const config = configModule.default || configModule;
|
|
56
|
+
|
|
57
|
+
// Validate and merge with defaults
|
|
58
|
+
return validateAndMergeConfig(config);
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn('⚠️ Failed to load testivai config, using defaults:', error);
|
|
62
|
+
return DEFAULT_CONFIG;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Merge per-test configuration with project configuration
|
|
68
|
+
*
|
|
69
|
+
* @param projectConfig The project-level configuration
|
|
70
|
+
* @param testConfig Optional per-test configuration overrides
|
|
71
|
+
* @returns TestivAIConfig The effective configuration for this test
|
|
72
|
+
*/
|
|
73
|
+
export function mergeTestConfig(
|
|
74
|
+
projectConfig: TestivAIProjectConfig,
|
|
75
|
+
testConfig?: TestivAIConfig
|
|
76
|
+
): TestivAIConfig {
|
|
77
|
+
if (!testConfig) {
|
|
78
|
+
return {
|
|
79
|
+
layout: projectConfig.layout,
|
|
80
|
+
ai: projectConfig.ai,
|
|
81
|
+
// @renamed: dom → structure (IP protection)
|
|
82
|
+
structure: projectConfig.structure,
|
|
83
|
+
performanceMetrics: projectConfig.performanceMetrics
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
layout: {
|
|
89
|
+
...projectConfig.layout,
|
|
90
|
+
...testConfig.layout
|
|
91
|
+
},
|
|
92
|
+
ai: {
|
|
93
|
+
...projectConfig.ai,
|
|
94
|
+
...testConfig.ai
|
|
95
|
+
},
|
|
96
|
+
// @renamed: dom → structure (IP protection)
|
|
97
|
+
structure: {
|
|
98
|
+
...projectConfig.structure,
|
|
99
|
+
...testConfig.structure
|
|
100
|
+
},
|
|
101
|
+
performanceMetrics: {
|
|
102
|
+
...projectConfig.performanceMetrics,
|
|
103
|
+
...testConfig.performanceMetrics
|
|
104
|
+
},
|
|
105
|
+
selectors: testConfig.selectors,
|
|
106
|
+
useCDP: testConfig.useCDP
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect current environment (CI, development, production)
|
|
112
|
+
*
|
|
113
|
+
* @returns string The detected environment
|
|
114
|
+
*/
|
|
115
|
+
export function detectEnvironment(): string {
|
|
116
|
+
// Check for common CI environment variables
|
|
117
|
+
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) {
|
|
118
|
+
return 'ci';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for production indicators
|
|
122
|
+
if (process.env.NODE_ENV === 'production') {
|
|
123
|
+
return 'production';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Default to development
|
|
127
|
+
return 'development';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Apply environment-specific overrides to configuration
|
|
132
|
+
*
|
|
133
|
+
* @param config The base configuration
|
|
134
|
+
* @returns TestivAIProjectConfig Configuration with environment overrides applied
|
|
135
|
+
*/
|
|
136
|
+
export function applyEnvironmentOverrides(config: TestivAIProjectConfig): TestivAIProjectConfig {
|
|
137
|
+
const environment = detectEnvironment();
|
|
138
|
+
|
|
139
|
+
if (!config.environments) {
|
|
140
|
+
return config;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Type-safe environment override access
|
|
144
|
+
let envOverrides: { layout?: Partial<LayoutConfig>; ai?: Partial<AIConfig> } | undefined;
|
|
145
|
+
|
|
146
|
+
switch (environment) {
|
|
147
|
+
case 'ci':
|
|
148
|
+
envOverrides = config.environments.ci;
|
|
149
|
+
break;
|
|
150
|
+
case 'development':
|
|
151
|
+
envOverrides = config.environments.development;
|
|
152
|
+
break;
|
|
153
|
+
case 'production':
|
|
154
|
+
envOverrides = config.environments.production;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!envOverrides) {
|
|
159
|
+
return config;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
layout: {
|
|
164
|
+
...config.layout,
|
|
165
|
+
...envOverrides.layout
|
|
166
|
+
},
|
|
167
|
+
ai: {
|
|
168
|
+
...config.ai,
|
|
169
|
+
...envOverrides.ai
|
|
170
|
+
},
|
|
171
|
+
environments: config.environments // Keep the original environments config
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validate configuration values and merge with defaults
|
|
177
|
+
*
|
|
178
|
+
* @param config The configuration to validate
|
|
179
|
+
* @returns TestivAIProjectConfig Validated configuration
|
|
180
|
+
*/
|
|
181
|
+
function validateAndMergeConfig(config: any): TestivAIProjectConfig {
|
|
182
|
+
// Guard against null/undefined config
|
|
183
|
+
if (!config) {
|
|
184
|
+
console.warn('⚠️ Config is null or undefined, using defaults');
|
|
185
|
+
return DEFAULT_CONFIG;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Basic validation
|
|
189
|
+
const validatedConfig: TestivAIProjectConfig = {
|
|
190
|
+
// API configuration with production default
|
|
191
|
+
apiKey: config.apiKey,
|
|
192
|
+
apiUrl: config.apiUrl || 'https://core-api.testiv.ai',
|
|
193
|
+
layout: {
|
|
194
|
+
sensitivity: validateRange(config.layout?.sensitivity ?? DEFAULT_CONFIG.layout.sensitivity, 0, 4, 'layout.sensitivity', DEFAULT_CONFIG.layout.sensitivity),
|
|
195
|
+
tolerance: validateRange(config.layout?.tolerance ?? DEFAULT_CONFIG.layout.tolerance, 0, 100, 'layout.tolerance', DEFAULT_CONFIG.layout.tolerance),
|
|
196
|
+
selectorTolerances: config.layout?.selectorTolerances,
|
|
197
|
+
useRelativeTolerance: config.layout?.useRelativeTolerance,
|
|
198
|
+
relativeTolerance: config.layout?.relativeTolerance
|
|
199
|
+
},
|
|
200
|
+
ai: {
|
|
201
|
+
sensitivity: validateRange(config.ai?.sensitivity ?? DEFAULT_CONFIG.ai.sensitivity, 0, 4, 'ai.sensitivity', DEFAULT_CONFIG.ai.sensitivity),
|
|
202
|
+
confidence: validateRange(config.ai?.confidence ?? DEFAULT_CONFIG.ai.confidence, 0, 1, 'ai.confidence', DEFAULT_CONFIG.ai.confidence),
|
|
203
|
+
enableReasoning: config.ai?.enableReasoning
|
|
204
|
+
},
|
|
205
|
+
// @renamed: dom → structure (IP protection)
|
|
206
|
+
structure: config.structure, // Pass through structure analysis configuration as-is
|
|
207
|
+
performanceMetrics: config.performanceMetrics, // Pass through performance metrics configuration as-is
|
|
208
|
+
environments: config.environments
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return applyEnvironmentOverrides(validatedConfig);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validate that a number is within the expected range
|
|
216
|
+
*
|
|
217
|
+
* @param value The value to validate
|
|
218
|
+
* @param min Minimum allowed value
|
|
219
|
+
* @param max Maximum allowed value
|
|
220
|
+
* @param field Field name for error messages
|
|
221
|
+
* @param defaultValue Default value to use when validation fails
|
|
222
|
+
* @returns number The validated value
|
|
223
|
+
*/
|
|
224
|
+
function validateRange(value: any, min: number, max: number, field: string, defaultValue: number): number {
|
|
225
|
+
const numValue = Number(value);
|
|
226
|
+
|
|
227
|
+
if (isNaN(numValue)) {
|
|
228
|
+
console.warn(`⚠️ Invalid ${field}: ${value}, using default: ${defaultValue}`);
|
|
229
|
+
return defaultValue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (numValue < min || numValue > max) {
|
|
233
|
+
console.warn(`⚠️ ${field} must be between ${min} and ${max}, got ${numValue}, using default: ${defaultValue}`);
|
|
234
|
+
return defaultValue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return numValue;
|
|
238
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { snapshot } from './snapshot';
|
|
2
|
+
import { getCiRunId } from './ci';
|
|
3
|
+
|
|
4
|
+
export const testivai = {
|
|
5
|
+
witness: snapshot,
|
|
6
|
+
ci: getCiRunId,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Re-export types for convenience
|
|
10
|
+
// @renamed: DOMAnalysisConfig → StructureAnalysisConfig, DOMAnalysis → StructureAnalysis (IP protection)
|
|
11
|
+
export type { TestivAIConfig, TestivAIProjectConfig, StructureAnalysisConfig, StructureAnalysis } from './types';
|
|
12
|
+
|
|
13
|
+
// Structure analyzer is now handled on the backend
|
|
14
|
+
// The types are kept for backwards compatibility
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// This file isolates the Playwright-specific types for the reporter
|
|
2
|
+
// to allow for clean mocking in the Jest test environment.
|
|
3
|
+
import type { Reporter, FullConfig, Suite, FullResult } from '@playwright/test/reporter';
|
|
4
|
+
|
|
5
|
+
export type { Reporter, FullConfig, Suite, FullResult };
|