@testivai/witness-playwright 1.1.1 → 1.1.3

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 (45) 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 +58 -1
  6. package/dist/snapshot.js +166 -147
  7. package/package.json +31 -10
  8. package/__tests__/.gitkeep +0 -0
  9. package/__tests__/config-integration.spec.ts +0 -102
  10. package/__tests__/snapshot.spec.d.ts +0 -1
  11. package/__tests__/snapshot.spec.js +0 -81
  12. package/__tests__/snapshot.spec.ts +0 -58
  13. package/__tests__/unit/ci.spec.d.ts +0 -1
  14. package/__tests__/unit/ci.spec.js +0 -35
  15. package/__tests__/unit/ci.spec.ts +0 -40
  16. package/__tests__/unit/reporter.spec.d.ts +0 -1
  17. package/__tests__/unit/reporter.spec.js +0 -40
  18. package/__tests__/unit/reporter.spec.ts +0 -72
  19. package/__tests__/unit/structureAnalyzer.spec.js +0 -212
  20. package/__tests__/unit/types.spec.ts +0 -179
  21. package/dist/__tests__/unit/ci.spec.d.ts +0 -1
  22. package/dist/__tests__/unit/ci.spec.js +0 -226
  23. package/dist/__tests__/unit/compression.spec.d.ts +0 -4
  24. package/dist/__tests__/unit/compression.spec.js +0 -46
  25. package/dist/domAnalyzer.d.ts +0 -10
  26. package/dist/domAnalyzer.js +0 -285
  27. package/examples/structure-analysis-example.spec.ts +0 -118
  28. package/examples/structure-analysis.config.ts +0 -159
  29. package/jest.config.js +0 -8
  30. package/playwright.config.ts +0 -11
  31. package/src/__tests__/unit/ci.spec.ts +0 -257
  32. package/src/__tests__/unit/compression.spec.ts +0 -52
  33. package/src/ci.ts +0 -140
  34. package/src/cli/index.ts +0 -49
  35. package/src/cli/init.ts +0 -131
  36. package/src/config/loader.ts +0 -238
  37. package/src/index.ts +0 -14
  38. package/src/reporter-entry.ts +0 -6
  39. package/src/reporter-types.ts +0 -5
  40. package/src/reporter.ts +0 -252
  41. package/src/snapshot.ts +0 -632
  42. package/src/structureAnalyzer.ts +0 -338
  43. package/src/types.ts +0 -390
  44. package/tsconfig.jest.json +0 -7
  45. 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
  }
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,187 @@ 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
+ // 1.5. Local mode: also place the screenshot in the layout expected by
314
+ // @testivai/witness/report (subdirectory keyed by snapshot name).
315
+ // This is what `BaselineStore.listTemp()` and `compareAll()` enumerate.
316
+ if (isLocalMode) {
317
+ const localSnapshotDir = path.join(outputDir, snapshotName);
318
+ await fs.ensureDir(localSnapshotDir);
319
+ await fs.copyFile(screenshotPath, path.join(localSnapshotDir, 'screenshot.png'));
320
+ }
321
+ // 2. Dump page structure (HTML) - skip in local mode
311
322
  // @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
323
+ let structurePath = '';
324
+ if (!isLocalMode) {
325
+ structurePath = path.join(outputDir, `${baseFilename}.html`);
326
+ const htmlContent = await page.content();
327
+ await fs.writeFile(structurePath, htmlContent);
328
+ }
329
+ // 2.5. Capture computed styles using browser session - skip in local mode
316
330
  // @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
- }
331
+ let stylesPath = '';
332
+ if (!isLocalMode) {
333
+ stylesPath = path.join(outputDir, `${baseFilename}.css.json`);
334
+ try {
335
+ const browserSession = await page.context().newCDPSession(page);
336
+ // Enable DOM and CSS domains
337
+ await browserSession.send('DOM.enable');
338
+ await browserSession.send('CSS.enable');
339
+ // Get all elements and their computed styles
340
+ const computedStyles = {};
341
+ // Visual properties we care about
342
+ const visualProperties = [
343
+ 'color', 'background-color', 'background-image',
344
+ 'font-size', 'font-weight', 'font-family',
345
+ 'width', 'height', 'padding', 'margin',
346
+ 'border', 'border-radius', 'box-shadow',
347
+ 'display', 'position', 'top', 'left', 'right', 'bottom',
348
+ 'transform', 'opacity', 'visibility', 'z-index'
349
+ ];
350
+ // Execute script to get all elements with unique identifiers
351
+ const elementsData = await browserSession.send('Runtime.evaluate', {
352
+ expression: `
353
+ (function() {
354
+ // Helper to get stable CSS selector path for an element
355
+ function getElementPath(element) {
356
+ if (element.id) {
357
+ return '#' + element.id;
358
358
  }
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})\`;
359
+
360
+ const path = [];
361
+ let current = element;
362
+
363
+ while (current && current !== document.body) {
364
+ let selector = current.tagName.toLowerCase();
365
+
366
+ // Add up to 3 CSS classes for better uniqueness
367
+ // e.g., button.button.primary-button instead of just button.button
368
+ if (current.className && typeof current.className === 'string') {
369
+ const classes = current.className.trim().split(/\\s+/).filter(Boolean);
370
+ const maxClasses = Math.min(classes.length, 3);
371
+ for (let c = 0; c < maxClasses; c++) {
372
+ selector += '.' + classes[c];
373
+ }
367
374
  }
375
+
376
+ // Get nth-child position for uniqueness
377
+ if (current.parentNode) {
378
+ const siblings = Array.from(current.parentNode.children);
379
+ const sameTagSiblings = siblings.filter(s => s.tagName === current.tagName);
380
+ if (sameTagSiblings.length > 1) {
381
+ const index = sameTagSiblings.indexOf(current) + 1;
382
+ selector += \`:nth-of-type(\${index})\`;
383
+ }
384
+ }
385
+
386
+ path.unshift(selector);
387
+ current = current.parentElement;
368
388
  }
369
-
370
- path.unshift(selector);
371
- current = current.parentElement;
389
+
390
+ return path.join(' > ');
372
391
  }
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
392
+
393
+ const elements = document.querySelectorAll('*');
394
+ const result = [];
395
+ elements.forEach((el, index) => {
396
+ const selectorPath = getElementPath(el);
397
+ const tagName = el.tagName.toLowerCase();
398
+ const className = el.className || '';
399
+ result.push({
400
+ selectorPath,
401
+ tagName,
402
+ className,
403
+ index
404
+ });
388
405
  });
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++;
406
+ return result;
407
+ })()
408
+ `,
409
+ returnByValue: true
410
+ });
411
+ if (elementsData.result.value) {
412
+ const elements = elementsData.result.value;
413
+ // Get computed styles for each element (sample first 100 to avoid performance issues)
414
+ const sampleSize = Math.min(elements.length, 100);
415
+ for (let i = 0; i < sampleSize; i++) {
416
+ const element = elements[i];
417
+ try {
418
+ const styleResult = await browserSession.send('Runtime.evaluate', {
419
+ expression: `
420
+ (function() {
421
+ const el = document.querySelectorAll('*')[${element.index}];
422
+ if (!el) return null;
423
+ const styles = window.getComputedStyle(el);
424
+ const result = {};
425
+ ${JSON.stringify(visualProperties)}.forEach(prop => {
426
+ result[prop] = styles.getPropertyValue(prop);
427
+ });
428
+ return result;
429
+ })()
430
+ `,
431
+ returnByValue: true
432
+ });
433
+ if (styleResult.result.value) {
434
+ // Use stable selector path as element ID instead of unstable index
435
+ // Deduplicate: if key already exists, append numeric suffix to prevent overwriting
436
+ let uniqueKey = element.selectorPath;
437
+ if (computedStyles[uniqueKey]) {
438
+ let suffix = 2;
439
+ while (computedStyles[`${element.selectorPath}[${suffix}]`]) {
440
+ suffix++;
441
+ }
442
+ uniqueKey = `${element.selectorPath}[${suffix}]`;
425
443
  }
426
- uniqueKey = `${element.selectorPath}[${suffix}]`;
444
+ computedStyles[uniqueKey] = styleResult.result.value;
427
445
  }
428
- computedStyles[uniqueKey] = styleResult.result.value;
429
446
  }
430
- }
431
- catch (err) {
432
- // Skip elements that fail
433
- continue;
447
+ catch (err) {
448
+ // Skip elements that fail
449
+ continue;
450
+ }
434
451
  }
435
452
  }
453
+ // Disable domains and close session
454
+ await browserSession.send('CSS.disable');
455
+ await browserSession.send('DOM.disable');
456
+ await browserSession.detach();
457
+ // Save computed styles to file
458
+ await fs.writeJson(stylesPath, {
459
+ computed_styles: computedStyles,
460
+ timestamp: Date.now(),
461
+ sample_size: Object.keys(computedStyles).length
462
+ });
463
+ if (process.env.TESTIVAI_DEBUG === 'true') {
464
+ console.log(`[TestivAI] Captured ${Object.keys(computedStyles).length} element styles`);
465
+ }
436
466
  }
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`);
467
+ catch (error) {
468
+ console.warn('[TestivAI] Failed to capture CSS via browser session:', error);
469
+ // Continue without CSS data
449
470
  }
450
471
  }
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
472
+ // 3. Extract bounding boxes for requested selectors - skip in local mode
456
473
  const selectors = effectiveConfig.selectors ?? ['body'];
457
474
  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
- };
475
+ if (!isLocalMode) {
476
+ for (const selector of selectors) {
477
+ const element = page.locator(selector).first();
478
+ const boundingBox = await element.boundingBox();
479
+ if (boundingBox) {
480
+ layout[selector] = {
481
+ ...boundingBox,
482
+ top: boundingBox.y,
483
+ left: boundingBox.x,
484
+ right: boundingBox.x + boundingBox.width,
485
+ bottom: boundingBox.y + boundingBox.height,
486
+ };
487
+ }
469
488
  }
470
489
  }
471
- // 4. Capture performance metrics using browser session (if enabled)
490
+ // 4. Capture performance metrics using browser session (if enabled) - skip in local mode
472
491
  let performanceMetrics = undefined;
473
492
  const metricsEnabled = effectiveConfig.performanceMetrics?.enabled ?? true; // Default: enabled
474
- if (metricsEnabled) {
493
+ if (metricsEnabled && !isLocalMode) {
475
494
  try {
476
495
  // Get browser session from Playwright page
477
496
  const browserSession = await page.context().newCDPSession(page);
@@ -568,10 +587,10 @@ async function snapshot(page, testInfo, name, config) {
568
587
  console.warn('Failed to capture performance metrics:', err);
569
588
  }
570
589
  }
571
- // 5. Structure analysis is now handled on the backend
590
+ // 5. Structure analysis is now handled on the backend - skip in local mode
572
591
  // The SDK just captures the HTML and sends it with the configuration
573
592
  // @renamed: domAnalysis → structureAnalysis (IP protection)
574
- const structureAnalysis = undefined; // Will be populated by backend
593
+ const structureAnalysis = isLocalMode ? undefined : undefined; // Will be populated by backend
575
594
  // 6. Save metadata with configuration and performance data
576
595
  const metadataPath = path.join(outputDir, `${baseFilename}.json`);
577
596
  const metadata = {