@testivai/witness-playwright 1.1.1 → 1.1.2
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/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/cli/init.js +0 -0
- package/dist/reporter.d.ts +1 -0
- package/dist/reporter.js +58 -1
- package/dist/snapshot.js +158 -147
- package/package.json +31 -10
- package/__tests__/.gitkeep +0 -0
- package/__tests__/config-integration.spec.ts +0 -102
- package/__tests__/snapshot.spec.d.ts +0 -1
- package/__tests__/snapshot.spec.js +0 -81
- package/__tests__/snapshot.spec.ts +0 -58
- package/__tests__/unit/ci.spec.d.ts +0 -1
- package/__tests__/unit/ci.spec.js +0 -35
- package/__tests__/unit/ci.spec.ts +0 -40
- package/__tests__/unit/reporter.spec.d.ts +0 -1
- package/__tests__/unit/reporter.spec.js +0 -40
- package/__tests__/unit/reporter.spec.ts +0 -72
- package/__tests__/unit/structureAnalyzer.spec.js +0 -212
- package/__tests__/unit/types.spec.ts +0 -179
- package/dist/__tests__/unit/ci.spec.d.ts +0 -1
- package/dist/__tests__/unit/ci.spec.js +0 -226
- package/dist/__tests__/unit/compression.spec.d.ts +0 -4
- package/dist/__tests__/unit/compression.spec.js +0 -46
- package/dist/domAnalyzer.d.ts +0 -10
- package/dist/domAnalyzer.js +0 -285
- package/examples/structure-analysis-example.spec.ts +0 -118
- package/examples/structure-analysis.config.ts +0 -159
- package/jest.config.js +0 -8
- package/playwright.config.ts +0 -11
- package/src/__tests__/unit/ci.spec.ts +0 -257
- package/src/__tests__/unit/compression.spec.ts +0 -52
- package/src/ci.ts +0 -140
- package/src/cli/index.ts +0 -49
- package/src/cli/init.ts +0 -131
- package/src/config/loader.ts +0 -238
- package/src/index.ts +0 -14
- package/src/reporter-entry.ts +0 -6
- package/src/reporter-types.ts +0 -5
- package/src/reporter.ts +0 -252
- package/src/snapshot.ts +0 -632
- package/src/structureAnalyzer.ts +0 -338
- package/src/types.ts +0 -390
- package/tsconfig.jest.json +0 -7
- package/tsconfig.json +0 -20
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 TestivAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @testivai/witness-playwright
|
|
2
|
+
|
|
3
|
+
Official Playwright adapter for TestivAI visual regression testing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D @testivai/witness-playwright
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start (Local Mode — Free)
|
|
12
|
+
|
|
13
|
+
1. **Initialize your project**
|
|
14
|
+
```bash
|
|
15
|
+
npx testivai init
|
|
16
|
+
# Select "Local Mode" when prompted
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. **Add the reporter to your Playwright config**
|
|
20
|
+
```typescript
|
|
21
|
+
// playwright.config.ts
|
|
22
|
+
import { defineConfig } from '@playwright/test';
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
reporter: [
|
|
26
|
+
['line'],
|
|
27
|
+
['@testivai/witness-playwright/reporter', {
|
|
28
|
+
// No API key needed for local mode!
|
|
29
|
+
}]
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. **Capture snapshots in your tests**
|
|
35
|
+
```typescript
|
|
36
|
+
import { test, expect } from '@playwright/test';
|
|
37
|
+
import { snapshot } from '@testivai/witness-playwright';
|
|
38
|
+
|
|
39
|
+
test('homepage visual', async ({ page }, testInfo) => {
|
|
40
|
+
await page.goto('https://example.com');
|
|
41
|
+
await snapshot(page, testInfo, 'homepage');
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
4. **Run your tests**
|
|
46
|
+
```bash
|
|
47
|
+
npx playwright test
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
5. **View the report**
|
|
51
|
+
Open `visual-report/index.html` in your browser.
|
|
52
|
+
|
|
53
|
+
6. **Approve changes**
|
|
54
|
+
```bash
|
|
55
|
+
npx testivai approve --all
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
### Local Mode (Default)
|
|
61
|
+
|
|
62
|
+
No configuration needed! Just run `npx testivai init` and select "Local Mode".
|
|
63
|
+
|
|
64
|
+
### Cloud Mode
|
|
65
|
+
|
|
66
|
+
For team dashboards and collaboration:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// playwright.config.ts
|
|
70
|
+
export default defineConfig({
|
|
71
|
+
reporter: [
|
|
72
|
+
['line'],
|
|
73
|
+
['@testivai/witness-playwright/reporter', {
|
|
74
|
+
apiKey: process.env.TESTIVAI_API_KEY,
|
|
75
|
+
}]
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
### `snapshot(page, testInfo, name?, config?)`
|
|
83
|
+
|
|
84
|
+
Capture a visual snapshot of the current page.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { snapshot } from '@testivai/witness-playwright';
|
|
88
|
+
|
|
89
|
+
// Basic usage
|
|
90
|
+
await snapshot(page, testInfo, 'my-snapshot');
|
|
91
|
+
|
|
92
|
+
// With custom config
|
|
93
|
+
await snapshot(page, testInfo, 'checkout-page', {
|
|
94
|
+
threshold: 0.05, // 5% difference tolerance
|
|
95
|
+
fullPage: true, // Capture full page
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## CI/CD Integration
|
|
100
|
+
|
|
101
|
+
### GitHub Actions
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
name: Visual Tests
|
|
105
|
+
on: [push, pull_request]
|
|
106
|
+
|
|
107
|
+
jobs:
|
|
108
|
+
test:
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
steps:
|
|
111
|
+
- uses: actions/checkout@v4
|
|
112
|
+
- uses: actions/setup-node@v4
|
|
113
|
+
with:
|
|
114
|
+
node-version: '20'
|
|
115
|
+
- run: npm ci
|
|
116
|
+
- run: npx playwright install chromium
|
|
117
|
+
- run: npx playwright test
|
|
118
|
+
- uses: testivai/testivai-action@v1
|
|
119
|
+
if: always()
|
|
120
|
+
with:
|
|
121
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
122
|
+
report-dir: visual-report
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
128
|
+
|
|
129
|
+
## Support
|
|
130
|
+
|
|
131
|
+
- Documentation: https://github.com/mcbuddy/testivai-oss/tree/main/packages/playwright
|
|
132
|
+
- Issues: https://github.com/mcbuddy/testivai-oss/issues
|
|
133
|
+
- Website: https://testiv.ai
|
package/dist/cli/init.js
CHANGED
|
File without changes
|
package/dist/reporter.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class TestivAIPlaywrightReporter implements Reporter {
|
|
|
14
14
|
private ciInfo;
|
|
15
15
|
private tempDir;
|
|
16
16
|
private compressionHelper;
|
|
17
|
+
private localMode;
|
|
17
18
|
constructor(options?: TestivaiReporterOptions);
|
|
18
19
|
onBegin(config: FullConfig, suite: Suite): Promise<void>;
|
|
19
20
|
onEnd(result: FullResult): Promise<void>;
|
package/dist/reporter.js
CHANGED
|
@@ -43,6 +43,22 @@ const simple_git_1 = __importDefault(require("simple-git"));
|
|
|
43
43
|
const axios_1 = __importDefault(require("axios"));
|
|
44
44
|
const ci_1 = require("./ci");
|
|
45
45
|
const common_1 = require("@testivai/common");
|
|
46
|
+
/**
|
|
47
|
+
* Check if local mode is configured by looking for .testivai/config.json with mode: 'local'
|
|
48
|
+
*/
|
|
49
|
+
function isLocalMode() {
|
|
50
|
+
try {
|
|
51
|
+
const configPath = path.join(process.cwd(), '.testivai', 'config.json');
|
|
52
|
+
if (!fs.existsSync(configPath)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const config = fs.readJsonSync(configPath);
|
|
56
|
+
return config.mode === 'local';
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
46
62
|
class TestivAIPlaywrightReporter {
|
|
47
63
|
constructor(options = {}) {
|
|
48
64
|
this.gitInfo = null;
|
|
@@ -50,6 +66,7 @@ class TestivAIPlaywrightReporter {
|
|
|
50
66
|
this.runId = null;
|
|
51
67
|
this.ciInfo = null;
|
|
52
68
|
this.tempDir = path.join(process.cwd(), '.testivai', 'temp');
|
|
69
|
+
this.localMode = false;
|
|
53
70
|
this.options = {
|
|
54
71
|
apiUrl: options.apiUrl || process.env.TESTIVAI_API_URL || 'https://core-api.testiv.ai',
|
|
55
72
|
apiKey: options.apiKey || process.env.TESTIVAI_API_KEY,
|
|
@@ -60,7 +77,16 @@ class TestivAIPlaywrightReporter {
|
|
|
60
77
|
this.compressionHelper = new common_1.CompressionHelper(this.options.compression);
|
|
61
78
|
}
|
|
62
79
|
async onBegin(config, suite) {
|
|
63
|
-
|
|
80
|
+
// Check for local mode first
|
|
81
|
+
this.localMode = isLocalMode();
|
|
82
|
+
if (this.localMode) {
|
|
83
|
+
process.env.TESTIVAI_MODE = 'local';
|
|
84
|
+
if (this.options.debug) {
|
|
85
|
+
console.log('Testivai Reporter: [DEBUG] Local mode detected, skipping API key validation');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// API key is only required in cloud mode
|
|
89
|
+
if (!this.localMode && !this.options.apiKey) {
|
|
64
90
|
console.error('Testivai Reporter: API Key is not configured. Disabling reporter.');
|
|
65
91
|
console.error('Set TESTIVAI_API_KEY environment variable or pass apiKey in reporter options.');
|
|
66
92
|
this.options.apiUrl = undefined; // Disable reporter
|
|
@@ -106,6 +132,37 @@ class TestivAIPlaywrightReporter {
|
|
|
106
132
|
async onEnd(result) {
|
|
107
133
|
// Wrap entire reporter logic in try-catch to prevent crashes
|
|
108
134
|
try {
|
|
135
|
+
// ── Local mode: generate HTML report instead of uploading ─────────────────
|
|
136
|
+
if (this.localMode) {
|
|
137
|
+
if (this.options.debug) {
|
|
138
|
+
console.log('Testivai Reporter: [DEBUG] Local mode - generating visual report...');
|
|
139
|
+
}
|
|
140
|
+
// Dynamic import of @testivai/witness/report for local mode
|
|
141
|
+
const { generateReport } = await Promise.resolve().then(() => __importStar(require('@testivai/witness/report')));
|
|
142
|
+
// Load local config for report settings
|
|
143
|
+
const localConfigPath = path.join(process.cwd(), '.testivai', 'config.json');
|
|
144
|
+
const localConfig = fs.existsSync(localConfigPath)
|
|
145
|
+
? fs.readJsonSync(localConfigPath)
|
|
146
|
+
: { threshold: 0.1, reportDir: 'visual-report', autoOpen: false };
|
|
147
|
+
const reportData = generateReport({
|
|
148
|
+
projectRoot: process.cwd(),
|
|
149
|
+
reportDir: localConfig.reportDir || 'visual-report',
|
|
150
|
+
threshold: localConfig.threshold,
|
|
151
|
+
autoOpen: localConfig.autoOpen,
|
|
152
|
+
});
|
|
153
|
+
// Print summary
|
|
154
|
+
const { summary } = reportData;
|
|
155
|
+
console.log(`\n ═══ TestivAI Visual Report ═══`);
|
|
156
|
+
console.log(` Total: ${summary.total} | Passed: ${summary.passed} | Changed: ${summary.changed} | New: ${summary.newSnapshots}`);
|
|
157
|
+
if (summary.changed > 0 || summary.newSnapshots > 0) {
|
|
158
|
+
console.log(`\n To approve: npx testivai approve --all`);
|
|
159
|
+
}
|
|
160
|
+
if (this.options.debug) {
|
|
161
|
+
console.log(`Testivai Reporter: [DEBUG] Report generated at ${path.join(process.cwd(), localConfig.reportDir || 'visual-report')}`);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// ── Cloud mode: upload to TestivAI API ────────────────────────────────────
|
|
109
166
|
if (!this.options.apiUrl) {
|
|
110
167
|
return; // Reporter is disabled
|
|
111
168
|
}
|
package/dist/snapshot.js
CHANGED
|
@@ -72,6 +72,8 @@ function getSnapshotNameFromUrl(pageUrl) {
|
|
|
72
72
|
* @param config Optional TestivAI configuration for this snapshot (overrides project defaults).
|
|
73
73
|
*/
|
|
74
74
|
async function snapshot(page, testInfo, name, config) {
|
|
75
|
+
// Check for local mode - only capture screenshots, skip heavy data
|
|
76
|
+
const isLocalMode = process.env.TESTIVAI_MODE === 'local';
|
|
75
77
|
// Load project configuration and merge with test-specific overrides
|
|
76
78
|
const projectConfig = await (0, loader_1.loadConfig)();
|
|
77
79
|
const effectiveConfig = (0, loader_1.mergeTestConfig)(projectConfig, config);
|
|
@@ -80,7 +82,8 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
80
82
|
console.log('[TestivAI] Config:', {
|
|
81
83
|
projectConfig,
|
|
82
84
|
testConfig: config,
|
|
83
|
-
effectiveConfig
|
|
85
|
+
effectiveConfig,
|
|
86
|
+
isLocalMode
|
|
84
87
|
});
|
|
85
88
|
}
|
|
86
89
|
const outputDir = path.join(process.cwd(), '.testivai', 'temp');
|
|
@@ -307,171 +310,179 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
307
310
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
308
311
|
}
|
|
309
312
|
}
|
|
310
|
-
// 2. Dump page structure (HTML)
|
|
313
|
+
// 2. Dump page structure (HTML) - skip in local mode
|
|
311
314
|
// @renamed: domPath → structurePath (IP protection)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
315
|
+
let structurePath = '';
|
|
316
|
+
if (!isLocalMode) {
|
|
317
|
+
structurePath = path.join(outputDir, `${baseFilename}.html`);
|
|
318
|
+
const htmlContent = await page.content();
|
|
319
|
+
await fs.writeFile(structurePath, htmlContent);
|
|
320
|
+
}
|
|
321
|
+
// 2.5. Capture computed styles using browser session - skip in local mode
|
|
316
322
|
// @renamed: cssPath → stylesPath (IP protection)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const path = [];
|
|
345
|
-
let current = element;
|
|
346
|
-
|
|
347
|
-
while (current && current !== document.body) {
|
|
348
|
-
let selector = current.tagName.toLowerCase();
|
|
349
|
-
|
|
350
|
-
// Add up to 3 CSS classes for better uniqueness
|
|
351
|
-
// e.g., button.button.primary-button instead of just button.button
|
|
352
|
-
if (current.className && typeof current.className === 'string') {
|
|
353
|
-
const classes = current.className.trim().split(/\\s+/).filter(Boolean);
|
|
354
|
-
const maxClasses = Math.min(classes.length, 3);
|
|
355
|
-
for (let c = 0; c < maxClasses; c++) {
|
|
356
|
-
selector += '.' + classes[c];
|
|
357
|
-
}
|
|
323
|
+
let stylesPath = '';
|
|
324
|
+
if (!isLocalMode) {
|
|
325
|
+
stylesPath = path.join(outputDir, `${baseFilename}.css.json`);
|
|
326
|
+
try {
|
|
327
|
+
const browserSession = await page.context().newCDPSession(page);
|
|
328
|
+
// Enable DOM and CSS domains
|
|
329
|
+
await browserSession.send('DOM.enable');
|
|
330
|
+
await browserSession.send('CSS.enable');
|
|
331
|
+
// Get all elements and their computed styles
|
|
332
|
+
const computedStyles = {};
|
|
333
|
+
// Visual properties we care about
|
|
334
|
+
const visualProperties = [
|
|
335
|
+
'color', 'background-color', 'background-image',
|
|
336
|
+
'font-size', 'font-weight', 'font-family',
|
|
337
|
+
'width', 'height', 'padding', 'margin',
|
|
338
|
+
'border', 'border-radius', 'box-shadow',
|
|
339
|
+
'display', 'position', 'top', 'left', 'right', 'bottom',
|
|
340
|
+
'transform', 'opacity', 'visibility', 'z-index'
|
|
341
|
+
];
|
|
342
|
+
// Execute script to get all elements with unique identifiers
|
|
343
|
+
const elementsData = await browserSession.send('Runtime.evaluate', {
|
|
344
|
+
expression: `
|
|
345
|
+
(function() {
|
|
346
|
+
// Helper to get stable CSS selector path for an element
|
|
347
|
+
function getElementPath(element) {
|
|
348
|
+
if (element.id) {
|
|
349
|
+
return '#' + element.id;
|
|
358
350
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
351
|
+
|
|
352
|
+
const path = [];
|
|
353
|
+
let current = element;
|
|
354
|
+
|
|
355
|
+
while (current && current !== document.body) {
|
|
356
|
+
let selector = current.tagName.toLowerCase();
|
|
357
|
+
|
|
358
|
+
// Add up to 3 CSS classes for better uniqueness
|
|
359
|
+
// e.g., button.button.primary-button instead of just button.button
|
|
360
|
+
if (current.className && typeof current.className === 'string') {
|
|
361
|
+
const classes = current.className.trim().split(/\\s+/).filter(Boolean);
|
|
362
|
+
const maxClasses = Math.min(classes.length, 3);
|
|
363
|
+
for (let c = 0; c < maxClasses; c++) {
|
|
364
|
+
selector += '.' + classes[c];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Get nth-child position for uniqueness
|
|
369
|
+
if (current.parentNode) {
|
|
370
|
+
const siblings = Array.from(current.parentNode.children);
|
|
371
|
+
const sameTagSiblings = siblings.filter(s => s.tagName === current.tagName);
|
|
372
|
+
if (sameTagSiblings.length > 1) {
|
|
373
|
+
const index = sameTagSiblings.indexOf(current) + 1;
|
|
374
|
+
selector += \`:nth-of-type(\${index})\`;
|
|
375
|
+
}
|
|
367
376
|
}
|
|
377
|
+
|
|
378
|
+
path.unshift(selector);
|
|
379
|
+
current = current.parentElement;
|
|
368
380
|
}
|
|
369
|
-
|
|
370
|
-
path.
|
|
371
|
-
current = current.parentElement;
|
|
381
|
+
|
|
382
|
+
return path.join(' > ');
|
|
372
383
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
className,
|
|
387
|
-
index
|
|
384
|
+
|
|
385
|
+
const elements = document.querySelectorAll('*');
|
|
386
|
+
const result = [];
|
|
387
|
+
elements.forEach((el, index) => {
|
|
388
|
+
const selectorPath = getElementPath(el);
|
|
389
|
+
const tagName = el.tagName.toLowerCase();
|
|
390
|
+
const className = el.className || '';
|
|
391
|
+
result.push({
|
|
392
|
+
selectorPath,
|
|
393
|
+
tagName,
|
|
394
|
+
className,
|
|
395
|
+
index
|
|
396
|
+
});
|
|
388
397
|
});
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
398
|
+
return result;
|
|
399
|
+
})()
|
|
400
|
+
`,
|
|
401
|
+
returnByValue: true
|
|
402
|
+
});
|
|
403
|
+
if (elementsData.result.value) {
|
|
404
|
+
const elements = elementsData.result.value;
|
|
405
|
+
// Get computed styles for each element (sample first 100 to avoid performance issues)
|
|
406
|
+
const sampleSize = Math.min(elements.length, 100);
|
|
407
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
408
|
+
const element = elements[i];
|
|
409
|
+
try {
|
|
410
|
+
const styleResult = await browserSession.send('Runtime.evaluate', {
|
|
411
|
+
expression: `
|
|
412
|
+
(function() {
|
|
413
|
+
const el = document.querySelectorAll('*')[${element.index}];
|
|
414
|
+
if (!el) return null;
|
|
415
|
+
const styles = window.getComputedStyle(el);
|
|
416
|
+
const result = {};
|
|
417
|
+
${JSON.stringify(visualProperties)}.forEach(prop => {
|
|
418
|
+
result[prop] = styles.getPropertyValue(prop);
|
|
419
|
+
});
|
|
420
|
+
return result;
|
|
421
|
+
})()
|
|
422
|
+
`,
|
|
423
|
+
returnByValue: true
|
|
424
|
+
});
|
|
425
|
+
if (styleResult.result.value) {
|
|
426
|
+
// Use stable selector path as element ID instead of unstable index
|
|
427
|
+
// Deduplicate: if key already exists, append numeric suffix to prevent overwriting
|
|
428
|
+
let uniqueKey = element.selectorPath;
|
|
429
|
+
if (computedStyles[uniqueKey]) {
|
|
430
|
+
let suffix = 2;
|
|
431
|
+
while (computedStyles[`${element.selectorPath}[${suffix}]`]) {
|
|
432
|
+
suffix++;
|
|
433
|
+
}
|
|
434
|
+
uniqueKey = `${element.selectorPath}[${suffix}]`;
|
|
425
435
|
}
|
|
426
|
-
uniqueKey =
|
|
436
|
+
computedStyles[uniqueKey] = styleResult.result.value;
|
|
427
437
|
}
|
|
428
|
-
computedStyles[uniqueKey] = styleResult.result.value;
|
|
429
438
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
439
|
+
catch (err) {
|
|
440
|
+
// Skip elements that fail
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
434
443
|
}
|
|
435
444
|
}
|
|
445
|
+
// Disable domains and close session
|
|
446
|
+
await browserSession.send('CSS.disable');
|
|
447
|
+
await browserSession.send('DOM.disable');
|
|
448
|
+
await browserSession.detach();
|
|
449
|
+
// Save computed styles to file
|
|
450
|
+
await fs.writeJson(stylesPath, {
|
|
451
|
+
computed_styles: computedStyles,
|
|
452
|
+
timestamp: Date.now(),
|
|
453
|
+
sample_size: Object.keys(computedStyles).length
|
|
454
|
+
});
|
|
455
|
+
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
456
|
+
console.log(`[TestivAI] Captured ${Object.keys(computedStyles).length} element styles`);
|
|
457
|
+
}
|
|
436
458
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
await browserSession.detach();
|
|
441
|
-
// Save computed styles to file
|
|
442
|
-
await fs.writeJson(stylesPath, {
|
|
443
|
-
computed_styles: computedStyles,
|
|
444
|
-
timestamp: Date.now(),
|
|
445
|
-
sample_size: Object.keys(computedStyles).length
|
|
446
|
-
});
|
|
447
|
-
if (process.env.TESTIVAI_DEBUG === 'true') {
|
|
448
|
-
console.log(`[TestivAI] Captured ${Object.keys(computedStyles).length} element styles`);
|
|
459
|
+
catch (error) {
|
|
460
|
+
console.warn('[TestivAI] Failed to capture CSS via browser session:', error);
|
|
461
|
+
// Continue without CSS data
|
|
449
462
|
}
|
|
450
463
|
}
|
|
451
|
-
|
|
452
|
-
console.warn('[TestivAI] Failed to capture CSS via browser session:', error);
|
|
453
|
-
// Continue without CSS data
|
|
454
|
-
}
|
|
455
|
-
// 3. Extract bounding boxes for requested selectors
|
|
464
|
+
// 3. Extract bounding boxes for requested selectors - skip in local mode
|
|
456
465
|
const selectors = effectiveConfig.selectors ?? ['body'];
|
|
457
466
|
const layout = {};
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
if (!isLocalMode) {
|
|
468
|
+
for (const selector of selectors) {
|
|
469
|
+
const element = page.locator(selector).first();
|
|
470
|
+
const boundingBox = await element.boundingBox();
|
|
471
|
+
if (boundingBox) {
|
|
472
|
+
layout[selector] = {
|
|
473
|
+
...boundingBox,
|
|
474
|
+
top: boundingBox.y,
|
|
475
|
+
left: boundingBox.x,
|
|
476
|
+
right: boundingBox.x + boundingBox.width,
|
|
477
|
+
bottom: boundingBox.y + boundingBox.height,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
469
480
|
}
|
|
470
481
|
}
|
|
471
|
-
// 4. Capture performance metrics using browser session (if enabled)
|
|
482
|
+
// 4. Capture performance metrics using browser session (if enabled) - skip in local mode
|
|
472
483
|
let performanceMetrics = undefined;
|
|
473
484
|
const metricsEnabled = effectiveConfig.performanceMetrics?.enabled ?? true; // Default: enabled
|
|
474
|
-
if (metricsEnabled) {
|
|
485
|
+
if (metricsEnabled && !isLocalMode) {
|
|
475
486
|
try {
|
|
476
487
|
// Get browser session from Playwright page
|
|
477
488
|
const browserSession = await page.context().newCDPSession(page);
|
|
@@ -568,10 +579,10 @@ async function snapshot(page, testInfo, name, config) {
|
|
|
568
579
|
console.warn('Failed to capture performance metrics:', err);
|
|
569
580
|
}
|
|
570
581
|
}
|
|
571
|
-
// 5. Structure analysis is now handled on the backend
|
|
582
|
+
// 5. Structure analysis is now handled on the backend - skip in local mode
|
|
572
583
|
// The SDK just captures the HTML and sends it with the configuration
|
|
573
584
|
// @renamed: domAnalysis → structureAnalysis (IP protection)
|
|
574
|
-
const structureAnalysis = undefined; // Will be populated by backend
|
|
585
|
+
const structureAnalysis = isLocalMode ? undefined : undefined; // Will be populated by backend
|
|
575
586
|
// 6. Save metadata with configuration and performance data
|
|
576
587
|
const metadataPath = path.join(outputDir, `${baseFilename}.json`);
|
|
577
588
|
const metadata = {
|