@testivai/witness-playwright 1.1.0 → 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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/cli/init.js +0 -0
  4. package/dist/reporter.d.ts +1 -0
  5. package/dist/reporter.js +60 -2
  6. package/dist/snapshot.js +158 -147
  7. package/dist/types.d.ts +2 -0
  8. package/package.json +31 -10
  9. package/__tests__/.gitkeep +0 -0
  10. package/__tests__/config-integration.spec.ts +0 -102
  11. package/__tests__/snapshot.spec.d.ts +0 -1
  12. package/__tests__/snapshot.spec.js +0 -81
  13. package/__tests__/snapshot.spec.ts +0 -58
  14. package/__tests__/unit/ci.spec.d.ts +0 -1
  15. package/__tests__/unit/ci.spec.js +0 -35
  16. package/__tests__/unit/ci.spec.ts +0 -40
  17. package/__tests__/unit/reporter.spec.d.ts +0 -1
  18. package/__tests__/unit/reporter.spec.js +0 -40
  19. package/__tests__/unit/reporter.spec.ts +0 -72
  20. package/__tests__/unit/structureAnalyzer.spec.js +0 -212
  21. package/__tests__/unit/types.spec.ts +0 -179
  22. package/dist/__tests__/unit/ci.spec.d.ts +0 -1
  23. package/dist/__tests__/unit/ci.spec.js +0 -226
  24. package/dist/__tests__/unit/compression.spec.d.ts +0 -4
  25. package/dist/__tests__/unit/compression.spec.js +0 -46
  26. package/dist/domAnalyzer.d.ts +0 -10
  27. package/dist/domAnalyzer.js +0 -285
  28. package/examples/structure-analysis-example.spec.ts +0 -118
  29. package/examples/structure-analysis.config.ts +0 -159
  30. package/jest.config.js +0 -8
  31. package/playwright.config.ts +0 -11
  32. package/src/__tests__/unit/ci.spec.ts +0 -257
  33. package/src/__tests__/unit/compression.spec.ts +0 -52
  34. package/src/ci.ts +0 -140
  35. package/src/cli/index.ts +0 -49
  36. package/src/cli/init.ts +0 -131
  37. package/src/config/loader.ts +0 -238
  38. package/src/index.ts +0 -14
  39. package/src/reporter-entry.ts +0 -6
  40. package/src/reporter-types.ts +0 -5
  41. package/src/reporter.ts +0 -251
  42. package/src/snapshot.ts +0 -632
  43. package/src/structureAnalyzer.ts +0 -338
  44. package/src/types.ts +0 -388
  45. package/tsconfig.jest.json +0 -7
  46. 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
@@ -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
- if (!this.options.apiKey) {
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
  }
@@ -185,7 +242,8 @@ class TestivAIPlaywrightReporter {
185
242
  bottom: layoutData.y + layoutData.height
186
243
  },
187
244
  testivaiConfig: metadata.testivaiConfig,
188
- screenshotData: screenshotBase64
245
+ screenshotData: screenshotBase64,
246
+ performanceMetrics: metadata.performanceMetrics
189
247
  };
190
248
  snapshots.push(snapshotPayload);
191
249
  }
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
- const structurePath = path.join(outputDir, `${baseFilename}.html`);
313
- const htmlContent = await page.content();
314
- await fs.writeFile(structurePath, htmlContent);
315
- // 2.5. Capture computed styles using browser session
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
- const stylesPath = path.join(outputDir, `${baseFilename}.css.json`);
318
- try {
319
- const browserSession = await page.context().newCDPSession(page);
320
- // Enable DOM and CSS domains
321
- await browserSession.send('DOM.enable');
322
- await browserSession.send('CSS.enable');
323
- // Get all elements and their computed styles
324
- const computedStyles = {};
325
- // Visual properties we care about
326
- const visualProperties = [
327
- 'color', 'background-color', 'background-image',
328
- 'font-size', 'font-weight', 'font-family',
329
- 'width', 'height', 'padding', 'margin',
330
- 'border', 'border-radius', 'box-shadow',
331
- 'display', 'position', 'top', 'left', 'right', 'bottom',
332
- 'transform', 'opacity', 'visibility', 'z-index'
333
- ];
334
- // Execute script to get all elements with unique identifiers
335
- const elementsData = await browserSession.send('Runtime.evaluate', {
336
- expression: `
337
- (function() {
338
- // Helper to get stable CSS selector path for an element
339
- function getElementPath(element) {
340
- if (element.id) {
341
- return '#' + element.id;
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
- // Get nth-child position for uniqueness
361
- if (current.parentNode) {
362
- const siblings = Array.from(current.parentNode.children);
363
- const sameTagSiblings = siblings.filter(s => s.tagName === current.tagName);
364
- if (sameTagSiblings.length > 1) {
365
- const index = sameTagSiblings.indexOf(current) + 1;
366
- selector += \`:nth-of-type(\${index})\`;
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.unshift(selector);
371
- current = current.parentElement;
381
+
382
+ return path.join(' > ');
372
383
  }
373
-
374
- return path.join(' > ');
375
- }
376
-
377
- const elements = document.querySelectorAll('*');
378
- const result = [];
379
- elements.forEach((el, index) => {
380
- const selectorPath = getElementPath(el);
381
- const tagName = el.tagName.toLowerCase();
382
- const className = el.className || '';
383
- result.push({
384
- selectorPath,
385
- tagName,
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
- return result;
391
- })()
392
- `,
393
- returnByValue: true
394
- });
395
- if (elementsData.result.value) {
396
- const elements = elementsData.result.value;
397
- // Get computed styles for each element (sample first 100 to avoid performance issues)
398
- const sampleSize = Math.min(elements.length, 100);
399
- for (let i = 0; i < sampleSize; i++) {
400
- const element = elements[i];
401
- try {
402
- const styleResult = await browserSession.send('Runtime.evaluate', {
403
- expression: `
404
- (function() {
405
- const el = document.querySelectorAll('*')[${element.index}];
406
- if (!el) return null;
407
- const styles = window.getComputedStyle(el);
408
- const result = {};
409
- ${JSON.stringify(visualProperties)}.forEach(prop => {
410
- result[prop] = styles.getPropertyValue(prop);
411
- });
412
- return result;
413
- })()
414
- `,
415
- returnByValue: true
416
- });
417
- if (styleResult.result.value) {
418
- // Use stable selector path as element ID instead of unstable index
419
- // Deduplicate: if key already exists, append numeric suffix to prevent overwriting
420
- let uniqueKey = element.selectorPath;
421
- if (computedStyles[uniqueKey]) {
422
- let suffix = 2;
423
- while (computedStyles[`${element.selectorPath}[${suffix}]`]) {
424
- suffix++;
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 = `${element.selectorPath}[${suffix}]`;
436
+ computedStyles[uniqueKey] = styleResult.result.value;
427
437
  }
428
- computedStyles[uniqueKey] = styleResult.result.value;
429
438
  }
430
- }
431
- catch (err) {
432
- // Skip elements that fail
433
- continue;
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
- // Disable domains and close session
438
- await browserSession.send('CSS.disable');
439
- await browserSession.send('DOM.disable');
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
- catch (error) {
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
- for (const selector of selectors) {
459
- const element = page.locator(selector).first();
460
- const boundingBox = await element.boundingBox();
461
- if (boundingBox) {
462
- layout[selector] = {
463
- ...boundingBox,
464
- top: boundingBox.y,
465
- left: boundingBox.x,
466
- right: boundingBox.x + boundingBox.width,
467
- bottom: boundingBox.y + boundingBox.height,
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 = {
package/dist/types.d.ts CHANGED
@@ -291,6 +291,8 @@ export interface SnapshotPayload {
291
291
  screenshotData?: string;
292
292
  /** Performance timing metrics (optional) */
293
293
  performanceTimings?: PerformanceTimings;
294
+ /** Performance metrics from browser Performance.getMetrics (optional) */
295
+ performanceMetrics?: any;
294
296
  /** Lighthouse results (optional) */
295
297
  lighthouseResults?: LighthouseResults;
296
298
  /**