@testivai/witness-playwright 2.0.3 → 2.0.5
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/package.json +2 -2
- package/dist/ci.d.ts +0 -30
- package/dist/ci.js +0 -117
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -47
- package/dist/cli/init.d.ts +0 -3
- package/dist/cli/init.js +0 -158
- package/dist/mode.d.ts +0 -21
- package/dist/mode.js +0 -84
- package/dist/structureAnalyzer.d.ts +0 -12
- package/dist/structureAnalyzer.js +0 -288
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-playwright",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Playwright adapter for TestivAI Witness — local-first visual regression with pixel + DOM comparison",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"fs-extra": "^11.4.0",
|
|
48
48
|
"sharp": "^0.35.4",
|
|
49
|
-
"@testivai/witness": "2.
|
|
49
|
+
"@testivai/witness": "2.2.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@playwright/test": "^1.63.0",
|
package/dist/ci.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utilities for detecting CI/CD environments and extracting a unique run identifier.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* CI environment information for integration feedback (e.g., GitHub commit statuses, PR comments).
|
|
6
|
-
*/
|
|
7
|
-
export interface CiInfo {
|
|
8
|
-
/** CI provider name */
|
|
9
|
-
provider: string;
|
|
10
|
-
/** Pull/Merge request number (if available) */
|
|
11
|
-
prNumber?: number;
|
|
12
|
-
/** URL to the CI run (if available) */
|
|
13
|
-
runUrl?: string;
|
|
14
|
-
/** CI build/run identifier */
|
|
15
|
-
buildId?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Gets a unique identifier for the current CI run.
|
|
19
|
-
* This ID is used to group snapshots from parallel shards into a single batch.
|
|
20
|
-
*
|
|
21
|
-
* @returns A unique string identifier for the CI run, or null if not in a known CI environment.
|
|
22
|
-
*/
|
|
23
|
-
export declare function getCiRunId(): string | null;
|
|
24
|
-
/**
|
|
25
|
-
* Gets detailed CI environment information including PR number and run URL.
|
|
26
|
-
* Used for integration feedback (GitHub commit statuses, PR comments, etc.).
|
|
27
|
-
*
|
|
28
|
-
* @returns CiInfo object if in a known CI environment, or null otherwise.
|
|
29
|
-
*/
|
|
30
|
-
export declare function getCiInfo(): CiInfo | null;
|
package/dist/ci.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Utilities for detecting CI/CD environments and extracting a unique run identifier.
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getCiRunId = getCiRunId;
|
|
7
|
-
exports.getCiInfo = getCiInfo;
|
|
8
|
-
/**
|
|
9
|
-
* Gets a unique identifier for the current CI run.
|
|
10
|
-
* This ID is used to group snapshots from parallel shards into a single batch.
|
|
11
|
-
*
|
|
12
|
-
* @returns A unique string identifier for the CI run, or null if not in a known CI environment.
|
|
13
|
-
*/
|
|
14
|
-
function getCiRunId() {
|
|
15
|
-
// GitHub Actions
|
|
16
|
-
if (process.env.GITHUB_RUN_ID) {
|
|
17
|
-
return `github-${process.env.GITHUB_RUN_ID}`;
|
|
18
|
-
}
|
|
19
|
-
// GitLab CI
|
|
20
|
-
if (process.env.GITLAB_CI) {
|
|
21
|
-
return `gitlab-${process.env.CI_PIPELINE_ID}`;
|
|
22
|
-
}
|
|
23
|
-
// Jenkins
|
|
24
|
-
if (process.env.JENKINS_URL) {
|
|
25
|
-
return `jenkins-${process.env.BUILD_ID}`;
|
|
26
|
-
}
|
|
27
|
-
// CircleCI
|
|
28
|
-
if (process.env.CIRCLECI) {
|
|
29
|
-
return `circleci-${process.env.CIRCLE_WORKFLOW_ID}`;
|
|
30
|
-
}
|
|
31
|
-
// Travis CI
|
|
32
|
-
if (process.env.TRAVIS) {
|
|
33
|
-
return `travis-${process.env.TRAVIS_BUILD_ID}`;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Gets detailed CI environment information including PR number and run URL.
|
|
39
|
-
* Used for integration feedback (GitHub commit statuses, PR comments, etc.).
|
|
40
|
-
*
|
|
41
|
-
* @returns CiInfo object if in a known CI environment, or null otherwise.
|
|
42
|
-
*/
|
|
43
|
-
function getCiInfo() {
|
|
44
|
-
// GitHub Actions
|
|
45
|
-
if (process.env.GITHUB_ACTIONS) {
|
|
46
|
-
const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
|
|
47
|
-
const repo = process.env.GITHUB_REPOSITORY;
|
|
48
|
-
const runId = process.env.GITHUB_RUN_ID;
|
|
49
|
-
// PR number: available via GITHUB_EVENT_NUMBER in pull_request events,
|
|
50
|
-
// or from GITHUB_REF (refs/pull/<number>/merge)
|
|
51
|
-
let prNumber;
|
|
52
|
-
if (process.env.GITHUB_EVENT_NUMBER) {
|
|
53
|
-
prNumber = parseInt(process.env.GITHUB_EVENT_NUMBER, 10) || undefined;
|
|
54
|
-
}
|
|
55
|
-
else if (process.env.GITHUB_REF?.startsWith('refs/pull/')) {
|
|
56
|
-
const match = process.env.GITHUB_REF.match(/^refs\/pull\/(\d+)\//);
|
|
57
|
-
if (match) {
|
|
58
|
-
prNumber = parseInt(match[1], 10);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
provider: 'github_actions',
|
|
63
|
-
prNumber,
|
|
64
|
-
runUrl: repo && runId ? `${serverUrl}/${repo}/actions/runs/${runId}` : undefined,
|
|
65
|
-
buildId: runId,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
// GitLab CI
|
|
69
|
-
if (process.env.GITLAB_CI) {
|
|
70
|
-
const prNumber = process.env.CI_MERGE_REQUEST_IID
|
|
71
|
-
? parseInt(process.env.CI_MERGE_REQUEST_IID, 10) || undefined
|
|
72
|
-
: undefined;
|
|
73
|
-
return {
|
|
74
|
-
provider: 'gitlab_ci',
|
|
75
|
-
prNumber,
|
|
76
|
-
runUrl: process.env.CI_PIPELINE_URL || undefined,
|
|
77
|
-
buildId: process.env.CI_PIPELINE_ID,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
// CircleCI
|
|
81
|
-
if (process.env.CIRCLECI) {
|
|
82
|
-
const prNumber = process.env.CIRCLE_PULL_REQUEST
|
|
83
|
-
? parseInt(process.env.CIRCLE_PULL_REQUEST.split('/').pop() || '', 10) || undefined
|
|
84
|
-
: undefined;
|
|
85
|
-
return {
|
|
86
|
-
provider: 'circleci',
|
|
87
|
-
prNumber,
|
|
88
|
-
runUrl: process.env.CIRCLE_BUILD_URL || undefined,
|
|
89
|
-
buildId: process.env.CIRCLE_WORKFLOW_ID,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
// Jenkins
|
|
93
|
-
if (process.env.JENKINS_URL) {
|
|
94
|
-
const prNumber = process.env.CHANGE_ID
|
|
95
|
-
? parseInt(process.env.CHANGE_ID, 10) || undefined
|
|
96
|
-
: undefined;
|
|
97
|
-
return {
|
|
98
|
-
provider: 'jenkins',
|
|
99
|
-
prNumber,
|
|
100
|
-
runUrl: process.env.BUILD_URL || undefined,
|
|
101
|
-
buildId: process.env.BUILD_ID,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
// Travis CI
|
|
105
|
-
if (process.env.TRAVIS) {
|
|
106
|
-
const prNumber = process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== 'false'
|
|
107
|
-
? parseInt(process.env.TRAVIS_PULL_REQUEST, 10) || undefined
|
|
108
|
-
: undefined;
|
|
109
|
-
return {
|
|
110
|
-
provider: 'travis_ci',
|
|
111
|
-
prNumber,
|
|
112
|
-
runUrl: process.env.TRAVIS_BUILD_WEB_URL || undefined,
|
|
113
|
-
buildId: process.env.TRAVIS_BUILD_ID,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
package/dist/cli/index.d.ts
DELETED
package/dist/cli/index.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const commander_1 = require("commander");
|
|
8
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
-
const init_1 = require("./init");
|
|
10
|
-
const program = new commander_1.Command();
|
|
11
|
-
// Display banner
|
|
12
|
-
const showBanner = () => {
|
|
13
|
-
console.log();
|
|
14
|
-
console.log(chalk_1.default.cyan.bold(' TestivAI'));
|
|
15
|
-
console.log(chalk_1.default.gray(' Catch Visual Bugs Automatically'));
|
|
16
|
-
console.log(chalk_1.default.gray(' AI that catches real bugs, ignores the noise.'));
|
|
17
|
-
console.log();
|
|
18
|
-
};
|
|
19
|
-
program
|
|
20
|
-
.name('testivai')
|
|
21
|
-
.description('TestivAI Playwright SDK CLI')
|
|
22
|
-
.version(require('../../package.json').version)
|
|
23
|
-
.hook('preAction', () => {
|
|
24
|
-
if (!process.argv.includes('--quiet') && !process.argv.includes('-q')) {
|
|
25
|
-
showBanner();
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
// Global options
|
|
29
|
-
program
|
|
30
|
-
.option('-q, --quiet', 'Suppress banner output (ideal for CI)');
|
|
31
|
-
program
|
|
32
|
-
.command('init')
|
|
33
|
-
.description('Initialize TestivAI configuration file')
|
|
34
|
-
.action(async () => {
|
|
35
|
-
try {
|
|
36
|
-
await (0, init_1.createConfigFile)();
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
console.error('❌ Failed to initialize:', error);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
program.parse(process.argv);
|
|
44
|
-
// Show help if no command provided
|
|
45
|
-
if (!process.argv.slice(2).length) {
|
|
46
|
-
program.outputHelp();
|
|
47
|
-
}
|
package/dist/cli/init.d.ts
DELETED
package/dist/cli/init.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/mode.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve whether the adapter should run in local mode.
|
|
3
|
-
*
|
|
4
|
-
* Local-first is the zero-config default: with no API key, the adapter
|
|
5
|
-
* captures to `.testivai/temp/<name>/` and writes an HTML report — no cloud,
|
|
6
|
-
* no account, nothing scary. Cloud mode activates only when a key is present.
|
|
7
|
-
*
|
|
8
|
-
* Precedence (first match wins):
|
|
9
|
-
* 1. TESTIVAI_MODE env — explicit override ('local' | 'cloud')
|
|
10
|
-
* 2. .testivai/config.json `mode` — project default
|
|
11
|
-
* 3. TESTIVAI_API_KEY presence — key ⇒ cloud, no key ⇒ local
|
|
12
|
-
*
|
|
13
|
-
* This is deliberately derivable from the shell environment and the config
|
|
14
|
-
* file alone, so it resolves identically in the Playwright main process (the
|
|
15
|
-
* reporter) and in worker processes (the `snapshot()` capture) — env vars set
|
|
16
|
-
* at runtime by the reporter never propagate to already-spawned workers.
|
|
17
|
-
*/
|
|
18
|
-
export declare function resolveLocalMode(opts?: {
|
|
19
|
-
apiKey?: string;
|
|
20
|
-
projectRoot?: string;
|
|
21
|
-
}): boolean;
|
package/dist/mode.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
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.resolveLocalMode = resolveLocalMode;
|
|
37
|
-
const fs = __importStar(require("fs-extra"));
|
|
38
|
-
const path = __importStar(require("path"));
|
|
39
|
-
/**
|
|
40
|
-
* Read `mode` from .testivai/config.json. Returns null when the file is
|
|
41
|
-
* absent, malformed, or has no recognized mode field.
|
|
42
|
-
*/
|
|
43
|
-
function configMode(projectRoot) {
|
|
44
|
-
try {
|
|
45
|
-
const cfg = fs.readJsonSync(path.join(projectRoot, '.testivai', 'config.json'));
|
|
46
|
-
if (cfg?.mode === 'local')
|
|
47
|
-
return 'local';
|
|
48
|
-
if (cfg?.mode === 'cloud')
|
|
49
|
-
return 'cloud';
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Resolve whether the adapter should run in local mode.
|
|
58
|
-
*
|
|
59
|
-
* Local-first is the zero-config default: with no API key, the adapter
|
|
60
|
-
* captures to `.testivai/temp/<name>/` and writes an HTML report — no cloud,
|
|
61
|
-
* no account, nothing scary. Cloud mode activates only when a key is present.
|
|
62
|
-
*
|
|
63
|
-
* Precedence (first match wins):
|
|
64
|
-
* 1. TESTIVAI_MODE env — explicit override ('local' | 'cloud')
|
|
65
|
-
* 2. .testivai/config.json `mode` — project default
|
|
66
|
-
* 3. TESTIVAI_API_KEY presence — key ⇒ cloud, no key ⇒ local
|
|
67
|
-
*
|
|
68
|
-
* This is deliberately derivable from the shell environment and the config
|
|
69
|
-
* file alone, so it resolves identically in the Playwright main process (the
|
|
70
|
-
* reporter) and in worker processes (the `snapshot()` capture) — env vars set
|
|
71
|
-
* at runtime by the reporter never propagate to already-spawned workers.
|
|
72
|
-
*/
|
|
73
|
-
function resolveLocalMode(opts = {}) {
|
|
74
|
-
const envMode = process.env.TESTIVAI_MODE;
|
|
75
|
-
if (envMode === 'local')
|
|
76
|
-
return true;
|
|
77
|
-
if (envMode === 'cloud')
|
|
78
|
-
return false;
|
|
79
|
-
const cfg = configMode(opts.projectRoot ?? process.cwd());
|
|
80
|
-
if (cfg)
|
|
81
|
-
return cfg === 'local';
|
|
82
|
-
const apiKey = opts.apiKey ?? process.env.TESTIVAI_API_KEY;
|
|
83
|
-
return !apiKey;
|
|
84
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Page } from '@playwright/test';
|
|
2
|
-
import { StructureAnalysisConfig, StructureAnalysis, StructureChange } from './types';
|
|
3
|
-
/**
|
|
4
|
-
* Analyzes page structure and generates fingerprint
|
|
5
|
-
* @renamed Was `analyzeDOM` — renamed to conceal internal layer terminology (IP protection)
|
|
6
|
-
*/
|
|
7
|
-
export declare function analyzeStructure(page: Page, config?: StructureAnalysisConfig): Promise<StructureAnalysis>;
|
|
8
|
-
/**
|
|
9
|
-
* Compare two structure analyses and identify changes
|
|
10
|
-
* @renamed Was `compareDOMAnalysis` — renamed to conceal internal layer terminology (IP protection)
|
|
11
|
-
*/
|
|
12
|
-
export declare function compareStructureAnalysis(baseline: StructureAnalysis, current: StructureAnalysis): StructureChange[];
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.analyzeStructure = analyzeStructure;
|
|
4
|
-
exports.compareStructureAnalysis = compareStructureAnalysis;
|
|
5
|
-
/**
|
|
6
|
-
* Default configuration for structure analysis
|
|
7
|
-
* @renamed Was DEFAULT_CONFIG in domAnalyzer.ts — renamed to conceal internal layer terminology (IP protection)
|
|
8
|
-
*/
|
|
9
|
-
const DEFAULT_CONFIG = {
|
|
10
|
-
enableFingerprint: true,
|
|
11
|
-
enableStructure: true,
|
|
12
|
-
enableSemantic: true,
|
|
13
|
-
ignoreAttributes: [
|
|
14
|
-
'data-testid',
|
|
15
|
-
'data-reactid',
|
|
16
|
-
'data-reactroot',
|
|
17
|
-
'ng-version',
|
|
18
|
-
'ng-version',
|
|
19
|
-
'ng-reflect-router-outlet',
|
|
20
|
-
'data-ng-version',
|
|
21
|
-
'style', // Inline styles change often
|
|
22
|
-
'class', // CSS classes can be dynamic
|
|
23
|
-
],
|
|
24
|
-
ignoreElements: [
|
|
25
|
-
'script',
|
|
26
|
-
'style',
|
|
27
|
-
'noscript',
|
|
28
|
-
'meta',
|
|
29
|
-
'link',
|
|
30
|
-
'title',
|
|
31
|
-
],
|
|
32
|
-
ignoreContentPatterns: [
|
|
33
|
-
/\d{4}-\d{2}-\d{2}/, // Dates
|
|
34
|
-
/\d{1,2}:\d{2}(:\d{2})?/, // Times
|
|
35
|
-
/\b\d{4}\b/, // Years
|
|
36
|
-
/\b\d+\b/, // Pure numbers
|
|
37
|
-
/uuid-/i, // UUIDs
|
|
38
|
-
/_\d+/, // Number suffixes
|
|
39
|
-
/\$\d+\.?\d*/, // Currency
|
|
40
|
-
],
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* Analyzes page structure and generates fingerprint
|
|
44
|
-
* @renamed Was `analyzeDOM` — renamed to conceal internal layer terminology (IP protection)
|
|
45
|
-
*/
|
|
46
|
-
async function analyzeStructure(page, config = {}) {
|
|
47
|
-
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
48
|
-
return await page.evaluate((cfg) => {
|
|
49
|
-
// Helper functions
|
|
50
|
-
const normalizeSelector = (el) => {
|
|
51
|
-
let selector = el.nodeName.toLowerCase();
|
|
52
|
-
// Add ID if present and not a dynamic ID
|
|
53
|
-
if (el.id && !cfg.ignoreAttributes.includes('id')) {
|
|
54
|
-
// Skip if ID looks dynamic
|
|
55
|
-
if (!/^(ember|react|ng|vue|_)/.test(el.id)) {
|
|
56
|
-
selector += `#${el.id}`;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Add meaningful classes only
|
|
60
|
-
if (el.className && !cfg.ignoreAttributes.includes('class')) {
|
|
61
|
-
const classes = el.className.toString()
|
|
62
|
-
.split(' ')
|
|
63
|
-
.filter((c) => c &&
|
|
64
|
-
!/^(ember|react|ng|vue|css|active|hover|focus)/.test(c) &&
|
|
65
|
-
!c.includes('_') &&
|
|
66
|
-
!/\d/.test(c))
|
|
67
|
-
.slice(0, 3); // Limit to 3 most meaningful classes
|
|
68
|
-
if (classes.length > 0) {
|
|
69
|
-
selector += `.${classes.join('.')}`;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return selector;
|
|
73
|
-
};
|
|
74
|
-
const getElementPath = (el, maxDepth = 10) => {
|
|
75
|
-
const path = [];
|
|
76
|
-
let current = el;
|
|
77
|
-
let depth = 0;
|
|
78
|
-
while (current && current.nodeType === Node.ELEMENT_NODE && depth < maxDepth) {
|
|
79
|
-
path.unshift(normalizeSelector(current));
|
|
80
|
-
current = current.parentElement;
|
|
81
|
-
depth++;
|
|
82
|
-
}
|
|
83
|
-
return path.join(' > ');
|
|
84
|
-
};
|
|
85
|
-
const shouldIgnoreElement = (el) => {
|
|
86
|
-
return cfg.ignoreElements.includes(el.nodeName.toLowerCase());
|
|
87
|
-
};
|
|
88
|
-
const shouldIgnoreAttribute = (attr) => {
|
|
89
|
-
return cfg.ignoreAttributes.includes(attr.toLowerCase());
|
|
90
|
-
};
|
|
91
|
-
const shouldIgnoreContent = (content) => {
|
|
92
|
-
return cfg.ignoreContentPatterns.some((pattern) => pattern.test(content));
|
|
93
|
-
};
|
|
94
|
-
const normalizeText = (text) => {
|
|
95
|
-
if (!text)
|
|
96
|
-
return '';
|
|
97
|
-
// Replace ignored patterns with placeholders
|
|
98
|
-
let normalized = text.trim();
|
|
99
|
-
cfg.ignoreContentPatterns.forEach((pattern) => {
|
|
100
|
-
normalized = normalized.replace(pattern, '[DYNAMIC]');
|
|
101
|
-
});
|
|
102
|
-
// Normalize whitespace
|
|
103
|
-
normalized = normalized.replace(/\s+/g, ' ');
|
|
104
|
-
return normalized;
|
|
105
|
-
};
|
|
106
|
-
// Get all meaningful elements
|
|
107
|
-
const allElements = Array.from(document.querySelectorAll('*'))
|
|
108
|
-
.filter(el => !shouldIgnoreElement(el));
|
|
109
|
-
const analysis = {};
|
|
110
|
-
// 1. Generate fingerprint
|
|
111
|
-
if (cfg.enableFingerprint) {
|
|
112
|
-
const paths = allElements.map((el) => {
|
|
113
|
-
const path = getElementPath(el);
|
|
114
|
-
const text = normalizeText(el.textContent || '');
|
|
115
|
-
const attrs = [];
|
|
116
|
-
// Add important attributes
|
|
117
|
-
for (const attr of el.attributes) {
|
|
118
|
-
if (!shouldIgnoreAttribute(attr.name) && attr.value) {
|
|
119
|
-
attrs.push(`${attr.name}="${attr.value}"`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return `${path}${text ? `|${text}` : ''}${attrs.length ? `|${attrs.join(',')}` : ''}`;
|
|
123
|
-
});
|
|
124
|
-
// Create hash from sorted paths
|
|
125
|
-
const sortedPaths = paths.sort().join('|||');
|
|
126
|
-
analysis.fingerprint = btoa(sortedPaths).substring(0, 32);
|
|
127
|
-
}
|
|
128
|
-
// 2. Structural analysis
|
|
129
|
-
if (cfg.enableStructure) {
|
|
130
|
-
const elementTypes = {};
|
|
131
|
-
let maxDepth = 0;
|
|
132
|
-
allElements.forEach((el) => {
|
|
133
|
-
const tag = el.nodeName.toLowerCase();
|
|
134
|
-
elementTypes[tag] = (elementTypes[tag] || 0) + 1;
|
|
135
|
-
// Calculate depth
|
|
136
|
-
let depth = 0;
|
|
137
|
-
let current = el;
|
|
138
|
-
while (current.parentElement) {
|
|
139
|
-
depth++;
|
|
140
|
-
current = current.parentElement;
|
|
141
|
-
}
|
|
142
|
-
maxDepth = Math.max(maxDepth, depth);
|
|
143
|
-
});
|
|
144
|
-
analysis.structure = {
|
|
145
|
-
totalElements: allElements.length,
|
|
146
|
-
elementTypes,
|
|
147
|
-
maxDepth,
|
|
148
|
-
interactiveElements: {
|
|
149
|
-
buttons: document.querySelectorAll('button').length,
|
|
150
|
-
inputs: document.querySelectorAll('input').length,
|
|
151
|
-
links: document.querySelectorAll('a[href]').length,
|
|
152
|
-
forms: document.querySelectorAll('form').length,
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
// 3. Semantic analysis
|
|
157
|
-
if (cfg.enableSemantic) {
|
|
158
|
-
const headings = {};
|
|
159
|
-
for (let i = 1; i <= 6; i++) {
|
|
160
|
-
headings[`h${i}`] = document.querySelectorAll(`h${i}`).length;
|
|
161
|
-
}
|
|
162
|
-
analysis.semantic = {
|
|
163
|
-
headings,
|
|
164
|
-
landmarks: {
|
|
165
|
-
hasHeader: !!document.querySelector('header'),
|
|
166
|
-
hasNav: !!document.querySelector('nav'),
|
|
167
|
-
hasMain: !!document.querySelector('main'),
|
|
168
|
-
hasFooter: !!document.querySelector('footer'),
|
|
169
|
-
hasAside: !!document.querySelector('aside'),
|
|
170
|
-
},
|
|
171
|
-
lists: {
|
|
172
|
-
ordered: document.querySelectorAll('ol').length,
|
|
173
|
-
unordered: document.querySelectorAll('ul').length,
|
|
174
|
-
},
|
|
175
|
-
tables: document.querySelectorAll('table').length,
|
|
176
|
-
images: document.querySelectorAll('img').length,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
// 4. Component analysis (framework-agnostic)
|
|
180
|
-
const components = {};
|
|
181
|
-
allElements.forEach((el) => {
|
|
182
|
-
// Try various component detection strategies
|
|
183
|
-
const componentName = el.getAttribute('data-testid')?.replace(/-/g, '') ||
|
|
184
|
-
el.getAttribute('data-component') ||
|
|
185
|
-
el.getAttribute('data-testid') ||
|
|
186
|
-
el.className?.toString().match(/(\w+Container|\w+Component|\w+Page|\w+Card|\w+Modal)/i)?.[1];
|
|
187
|
-
if (componentName && componentName.length > 2) {
|
|
188
|
-
if (!components[componentName]) {
|
|
189
|
-
components[componentName] = [];
|
|
190
|
-
}
|
|
191
|
-
const attrs = {};
|
|
192
|
-
for (const attr of el.attributes) {
|
|
193
|
-
if (!shouldIgnoreAttribute(attr.name) && attr.value) {
|
|
194
|
-
attrs[attr.name] = attr.value;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
components[componentName].push({
|
|
198
|
-
selector: getElementPath(el),
|
|
199
|
-
text: normalizeText(el.textContent || '').substring(0, 100),
|
|
200
|
-
attributes: attrs,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
if (Object.keys(components).length > 0) {
|
|
205
|
-
analysis.components = components;
|
|
206
|
-
}
|
|
207
|
-
return analysis;
|
|
208
|
-
}, finalConfig);
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Compare two structure analyses and identify changes
|
|
212
|
-
* @renamed Was `compareDOMAnalysis` — renamed to conceal internal layer terminology (IP protection)
|
|
213
|
-
*/
|
|
214
|
-
function compareStructureAnalysis(baseline, current) {
|
|
215
|
-
const changes = [];
|
|
216
|
-
// Compare fingerprints
|
|
217
|
-
if (baseline.fingerprint && current.fingerprint) {
|
|
218
|
-
if (baseline.fingerprint !== current.fingerprint) {
|
|
219
|
-
changes.push({
|
|
220
|
-
type: 'fingerprint',
|
|
221
|
-
severity: 'high',
|
|
222
|
-
description: 'Page structure has changed',
|
|
223
|
-
details: {
|
|
224
|
-
baseline: baseline.fingerprint,
|
|
225
|
-
current: current.fingerprint,
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
// Compare structure
|
|
231
|
-
if (baseline.structure && current.structure) {
|
|
232
|
-
if (baseline.structure.totalElements !== current.structure.totalElements) {
|
|
233
|
-
changes.push({
|
|
234
|
-
type: 'structure',
|
|
235
|
-
severity: 'medium',
|
|
236
|
-
description: `Element count changed from ${baseline.structure.totalElements} to ${current.structure.totalElements}`,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
// Check for new/removed element types
|
|
240
|
-
const baselineTypes = Object.keys(baseline.structure.elementTypes);
|
|
241
|
-
const currentTypes = Object.keys(current.structure.elementTypes);
|
|
242
|
-
const removed = baselineTypes.filter(t => !currentTypes.includes(t));
|
|
243
|
-
const added = currentTypes.filter(t => !baselineTypes.includes(t));
|
|
244
|
-
if (removed.length > 0) {
|
|
245
|
-
changes.push({
|
|
246
|
-
type: 'structure',
|
|
247
|
-
severity: 'medium',
|
|
248
|
-
description: `Removed element types: ${removed.join(', ')}`,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
if (added.length > 0) {
|
|
252
|
-
changes.push({
|
|
253
|
-
type: 'structure',
|
|
254
|
-
severity: 'medium',
|
|
255
|
-
description: `Added element types: ${added.join(', ')}`,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
// Compare semantic structure
|
|
260
|
-
if (baseline.semantic && current.semantic) {
|
|
261
|
-
// Check heading changes
|
|
262
|
-
const baselineHeadings = baseline.semantic.headings;
|
|
263
|
-
const currentHeadings = current.semantic.headings;
|
|
264
|
-
for (const level in baselineHeadings) {
|
|
265
|
-
if (baselineHeadings[level] !== currentHeadings[level]) {
|
|
266
|
-
changes.push({
|
|
267
|
-
type: 'semantic',
|
|
268
|
-
severity: 'low',
|
|
269
|
-
description: `${level} count changed from ${baselineHeadings[level]} to ${currentHeadings[level]}`,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// Check landmark changes
|
|
274
|
-
const baselineLandmarks = baseline.semantic.landmarks;
|
|
275
|
-
const currentLandmarks = current.semantic.landmarks;
|
|
276
|
-
for (const landmark in baselineLandmarks) {
|
|
277
|
-
if (baselineLandmarks[landmark] !==
|
|
278
|
-
currentLandmarks[landmark]) {
|
|
279
|
-
changes.push({
|
|
280
|
-
type: 'semantic',
|
|
281
|
-
severity: 'medium',
|
|
282
|
-
description: `Landmark ${landmark} ${baselineLandmarks[landmark] ? 'removed' : 'added'}`,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return changes;
|
|
288
|
-
}
|