@testivai/witness-playwright 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +65 -0
  2. package/__tests__/.gitkeep +0 -0
  3. package/__tests__/config-integration.spec.ts +102 -0
  4. package/__tests__/snapshot.spec.d.ts +1 -0
  5. package/__tests__/snapshot.spec.js +81 -0
  6. package/__tests__/snapshot.spec.ts +58 -0
  7. package/__tests__/unit/ci.spec.d.ts +1 -0
  8. package/__tests__/unit/ci.spec.js +35 -0
  9. package/__tests__/unit/ci.spec.ts +40 -0
  10. package/__tests__/unit/reporter.spec.d.ts +1 -0
  11. package/__tests__/unit/reporter.spec.js +37 -0
  12. package/__tests__/unit/reporter.spec.ts +43 -0
  13. package/dist/ci.d.ts +10 -0
  14. package/dist/ci.js +35 -0
  15. package/dist/cli/init.d.ts +3 -0
  16. package/dist/cli/init.js +146 -0
  17. package/dist/config/loader.d.ts +29 -0
  18. package/dist/config/loader.js +232 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.js +24 -0
  21. package/dist/reporter-types.d.ts +2 -0
  22. package/dist/reporter-types.js +2 -0
  23. package/dist/reporter.d.ts +16 -0
  24. package/dist/reporter.js +155 -0
  25. package/dist/snapshot.d.ts +12 -0
  26. package/dist/snapshot.js +122 -0
  27. package/dist/types.d.ts +181 -0
  28. package/dist/types.js +10 -0
  29. package/jest.config.js +11 -0
  30. package/package.json +47 -0
  31. package/playwright.config.ts +11 -0
  32. package/progress.md +620 -0
  33. package/src/ci.ts +34 -0
  34. package/src/cli/init.ts +119 -0
  35. package/src/config/loader.ts +219 -0
  36. package/src/index.ts +9 -0
  37. package/src/reporter-types.ts +5 -0
  38. package/src/reporter.ts +148 -0
  39. package/src/snapshot.ts +103 -0
  40. package/src/types.ts +193 -0
  41. package/test-results/.last-run.json +4 -0
  42. package/tsconfig.jest.json +7 -0
  43. package/tsconfig.json +19 -0
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @testivai/witness-playwright
2
+
3
+ **Status**: ✅ Production Ready | **Last Updated**: December 21, 2025
4
+
5
+ **Project:** @testivai/witness-playwright (MVP - 1 Month Plan)
6
+
7
+ **Role:** The "Sensor" or The "Witness"
8
+
9
+ **Goal:** To provide a lightweight Playwright integration that (1) captures snapshots, DOM, and layout/bounding-box data, and (2) a custom reporter that uploads this data, along with viewport/browser context, in a batch-oriented way. The goal is to observe the application under test and collect evidence (snapshots, DOM, layout data) without burdening the developer with complex configuration.
10
+
11
+ ## Architecture
12
+
13
+ This package provides two main exports:
14
+
15
+ ## Configuration
16
+
17
+ To use the reporter, you need to configure it in your `playwright.config.ts` file. You must also provide your API URL and Key via environment variables (`TESTIVAI_API_URL` and `TESTIVAI_API_KEY`).
18
+
19
+ ```typescript
20
+ // playwright.config.ts
21
+ import { defineConfig } from '@playwright/test';
22
+
23
+ export default defineConfig({
24
+ // ... other config
25
+
26
+ reporter: [
27
+ ['list'], // You can use other reporters alongside it
28
+ ['@testivai/witness-playwright/reporter']
29
+ ],
30
+ });
31
+ ```
32
+
33
+ The reporter will automatically detect if it is running in a CI environment (such as GitHub Actions, GitLab, Jenkins, etc.) and will tag the evidence batch with a unique run ID. This ensures that snapshots from parallel jobs are correctly grouped together into a single test run.
34
+
35
+ ## Usage
36
+
37
+ Here is a basic example of how to use the `snapshot` function within a Playwright test file:
38
+
39
+ ```typescript
40
+ import { test } from '@playwright/test';
41
+ import { testivai } from '@testivai/witness-playwright';
42
+
43
+ test('my example test', async ({ page }, testInfo) => {
44
+ await page.goto('https://example.com');
45
+
46
+ // Capture a snapshot with a custom name
47
+ await testivai.witness(page, testInfo, 'example-home');
48
+
49
+ // Or capture a snapshot without a name (one will be generated from the URL)
50
+ await testivai.witness(page, testInfo);
51
+ });
52
+ ```
53
+
54
+ 1. **`testivai.witness()`**: A utility function to be called by the user within their Playwright tests to capture evidence.
55
+ 2. **`TestivAIPlaywrightReporter`**: A custom Playwright reporter, configured in `playwright.config.ts`, that runs after all tests are complete. It is responsible for collecting all the evidence captured during the test run, batching it together with context (like Git and browser information), and uploading it to the Testivai service.
56
+
57
+ ## 📦 Installation
58
+
59
+ ```bash
60
+ npm install @testivai/witness-playwright
61
+ ```
62
+
63
+ ## 📊 Progress
64
+
65
+ See [PROGRESS.md](./PROGRESS.md) for detailed development progress.
File without changes
@@ -0,0 +1,102 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { snapshot } from '../src/snapshot';
3
+ import * as fs from 'fs-extra';
4
+ import * as path from 'path';
5
+
6
+ test.describe('Configuration Integration', () => {
7
+
8
+ test('loads default configuration when no config file exists', async ({ page }) => {
9
+ // Remove any existing config file
10
+ const configPath = path.join(process.cwd(), 'testivai.config.ts');
11
+ if (await fs.pathExists(configPath)) {
12
+ await fs.remove(configPath);
13
+ }
14
+
15
+ await page.goto('data:text/html,<html><body><h1>Test Page</h1></body></html>');
16
+
17
+ // Mock TestInfo object
18
+ const mockTestInfo = {
19
+ title: 'Configuration Test'
20
+ } as any;
21
+
22
+ // This should use default configuration
23
+ await snapshot(page, mockTestInfo, 'test-no-config');
24
+
25
+ // Check that metadata was created with default config
26
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp');
27
+ const files = await fs.readdir(tempDir);
28
+ const metadataFile = files.find(f => f.endsWith('.json'));
29
+
30
+ expect(metadataFile).toBeDefined();
31
+
32
+ const metadata = await fs.readJson(path.join(tempDir, metadataFile!));
33
+ expect(metadata.testivaiConfig).toBeDefined();
34
+ expect(metadata.testivaiConfig.layout.sensitivity).toBe(2);
35
+ expect(metadata.testivaiConfig.layout.tolerance).toBe(1.0);
36
+ expect(metadata.testivaiConfig.ai.sensitivity).toBe(2);
37
+ expect(metadata.testivaiConfig.ai.confidence).toBe(0.7);
38
+ });
39
+
40
+ test('uses custom configuration when provided', async ({ page }) => {
41
+ await page.goto('data:text/html,<html><body><h1>Test Page</h1></body></html>');
42
+
43
+ // Mock TestInfo object
44
+ const mockTestInfo = {
45
+ title: 'Custom Config Test'
46
+ } as any;
47
+
48
+ // Test with custom configuration
49
+ const customConfig = {
50
+ layout: {
51
+ sensitivity: 0, // Strict
52
+ tolerance: 0.5
53
+ },
54
+ ai: {
55
+ sensitivity: 4, // Aggressive
56
+ confidence: 0.9
57
+ },
58
+ selectors: ['h1']
59
+ };
60
+
61
+ await snapshot(page, mockTestInfo, 'test-custom-config', customConfig);
62
+
63
+ // Check that metadata was created with custom config
64
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp');
65
+ const files = await fs.readdir(tempDir);
66
+ const metadataFile = files.find(f => f.includes('test_custom_config') && f.endsWith('.json'));
67
+
68
+ expect(metadataFile).toBeDefined();
69
+
70
+ const metadata = await fs.readJson(path.join(tempDir, metadataFile!));
71
+ expect(metadata.testivaiConfig).toBeDefined();
72
+ expect(metadata.testivaiConfig.layout.sensitivity).toBe(0);
73
+ expect(metadata.testivaiConfig.layout.tolerance).toBe(0.5);
74
+ expect(metadata.testivaiConfig.ai.sensitivity).toBe(4);
75
+ expect(metadata.testivaiConfig.ai.confidence).toBe(0.9);
76
+ expect(metadata.testivaiConfig.selectors).toEqual(['h1']);
77
+ });
78
+
79
+ test('CLI generates valid configuration file', async () => {
80
+ // Remove existing config
81
+ const configPath = path.join(process.cwd(), 'testivai.config.ts');
82
+ if (await fs.pathExists(configPath)) {
83
+ await fs.remove(configPath);
84
+ }
85
+
86
+ // Run CLI init using direct file system operations
87
+ const { createConfigFile } = require('../dist/cli/init');
88
+ await createConfigFile();
89
+
90
+ // Verify config file was created
91
+ expect(await fs.pathExists(configPath)).toBe(true);
92
+
93
+ // Verify config file content
94
+ const configContent = await fs.readFile(configPath, 'utf8');
95
+ expect(configContent).toContain('export default');
96
+ expect(configContent).toContain('sensitivity: 2');
97
+ expect(configContent).toContain('tolerance: 1.0');
98
+ expect(configContent).toContain('confidence: 0.7');
99
+ expect(configContent).toContain('Custom Configuration Examples');
100
+ });
101
+
102
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
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
+ const test_1 = require("@playwright/test");
37
+ const fs = __importStar(require("fs-extra"));
38
+ const path = __importStar(require("path"));
39
+ const src_1 = require("../src");
40
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp');
41
+ test_1.test.describe('testivai.witness()', () => {
42
+ // Ensure the temp directory is clean before each test
43
+ test_1.test.beforeEach(async () => {
44
+ await fs.emptyDir(tempDir);
45
+ });
46
+ test_1.test.afterAll(async () => {
47
+ await fs.emptyDir(tempDir);
48
+ });
49
+ (0, test_1.test)('should create all evidence files for a named snapshot', async ({ page }, testInfo) => {
50
+ await page.setContent('<body><h1>Hello Snapshot!</h1></body>');
51
+ await src_1.testivai.witness(page, testInfo, 'test-snapshot');
52
+ const files = await fs.readdir(tempDir);
53
+ (0, test_1.expect)(files).toHaveLength(3);
54
+ const jsonFile = files.find(f => f.endsWith('.json'));
55
+ const htmlFile = files.find(f => f.endsWith('.html'));
56
+ const pngFile = files.find(f => f.endsWith('.png'));
57
+ (0, test_1.expect)(jsonFile, 'JSON file should exist').toBeDefined();
58
+ (0, test_1.expect)(htmlFile, 'HTML file should exist').toBeDefined();
59
+ (0, test_1.expect)(pngFile, 'PNG file should exist').toBeDefined();
60
+ // Verify JSON metadata content
61
+ const metadata = await fs.readJson(path.join(tempDir, jsonFile));
62
+ (0, test_1.expect)(metadata.snapshotName).toBe('test-snapshot');
63
+ (0, test_1.expect)(metadata.testName).toBe('should create all evidence files for a named snapshot');
64
+ (0, test_1.expect)(metadata.layout.body).toBeDefined();
65
+ (0, test_1.expect)(typeof metadata.layout.body.x).toBe('number');
66
+ // Verify HTML content
67
+ const htmlContent = await fs.readFile(path.join(tempDir, htmlFile), 'utf-8');
68
+ (0, test_1.expect)(htmlContent).toContain('<h1>Hello Snapshot!</h1>');
69
+ });
70
+ (0, test_1.test)('should generate a snapshot name from the URL if not provided', async ({ page }, testInfo) => {
71
+ // Using a data URL is a reliable way to set a page URL in a test environment
72
+ await page.goto('data:text/html,<h2>Page Title</h2>');
73
+ await src_1.testivai.witness(page, testInfo);
74
+ const files = await fs.readdir(tempDir);
75
+ const jsonFile = files.find(f => f.endsWith('.json'));
76
+ (0, test_1.expect)(jsonFile, 'JSON file should exist').toBeDefined();
77
+ const metadata = await fs.readJson(path.join(tempDir, jsonFile));
78
+ // The default name for a data URL or empty path is 'snapshot'
79
+ (0, test_1.expect)(metadata.snapshotName).toBe('snapshot');
80
+ });
81
+ });
@@ -0,0 +1,58 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import * as fs from 'fs-extra';
3
+ import * as path from 'path';
4
+ import { testivai } from '../src';
5
+
6
+ const tempDir = path.join(process.cwd(), '.testivai', 'temp');
7
+
8
+ test.describe('testivai.witness()', () => {
9
+ // Ensure the temp directory is clean before each test
10
+ test.beforeEach(async () => {
11
+ await fs.emptyDir(tempDir);
12
+ });
13
+
14
+ test.afterAll(async () => {
15
+ await fs.emptyDir(tempDir);
16
+ });
17
+
18
+ test('should create all evidence files for a named snapshot', async ({ page }, testInfo) => {
19
+ await page.setContent('<body><h1>Hello Snapshot!</h1></body>');
20
+ await testivai.witness(page, testInfo, 'test-snapshot');
21
+
22
+ const files = await fs.readdir(tempDir);
23
+ expect(files).toHaveLength(3);
24
+
25
+ const jsonFile = files.find(f => f.endsWith('.json'));
26
+ const htmlFile = files.find(f => f.endsWith('.html'));
27
+ const pngFile = files.find(f => f.endsWith('.png'));
28
+
29
+ expect(jsonFile, 'JSON file should exist').toBeDefined();
30
+ expect(htmlFile, 'HTML file should exist').toBeDefined();
31
+ expect(pngFile, 'PNG file should exist').toBeDefined();
32
+
33
+ // Verify JSON metadata content
34
+ const metadata = await fs.readJson(path.join(tempDir, jsonFile!));
35
+ expect(metadata.snapshotName).toBe('test-snapshot');
36
+ expect(metadata.testName).toBe('should create all evidence files for a named snapshot');
37
+ expect(metadata.layout.body).toBeDefined();
38
+ expect(typeof metadata.layout.body.x).toBe('number');
39
+
40
+ // Verify HTML content
41
+ const htmlContent = await fs.readFile(path.join(tempDir, htmlFile!), 'utf-8');
42
+ expect(htmlContent).toContain('<h1>Hello Snapshot!</h1>');
43
+ });
44
+
45
+ test('should generate a snapshot name from the URL if not provided', async ({ page }, testInfo) => {
46
+ // Using a data URL is a reliable way to set a page URL in a test environment
47
+ await page.goto('data:text/html,<h2>Page Title</h2>');
48
+ await testivai.witness(page, testInfo);
49
+
50
+ const files = await fs.readdir(tempDir);
51
+ const jsonFile = files.find(f => f.endsWith('.json'));
52
+ expect(jsonFile, 'JSON file should exist').toBeDefined();
53
+
54
+ const metadata = await fs.readJson(path.join(tempDir, jsonFile!));
55
+ // The default name for a data URL or empty path is 'snapshot'
56
+ expect(metadata.snapshotName).toBe('snapshot');
57
+ });
58
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ci_1 = require("../../src/ci");
4
+ describe('getCiRunId', () => {
5
+ const OLD_ENV = process.env;
6
+ beforeEach(() => {
7
+ jest.resetModules(); // Clears the module cache
8
+ // Start with a clean environment, preserving only essential vars like PATH
9
+ process.env = { PATH: OLD_ENV.PATH };
10
+ });
11
+ afterEach(() => {
12
+ process.env = OLD_ENV; // Restore original environment
13
+ });
14
+ test('should return GitHub run ID when in GitHub Actions', () => {
15
+ process.env.GITHUB_RUN_ID = 'github123';
16
+ expect((0, ci_1.getCiRunId)()).toBe('github-github123');
17
+ });
18
+ test('should return GitLab pipeline ID when in GitLab CI', () => {
19
+ process.env.GITLAB_CI = 'true';
20
+ process.env.CI_PIPELINE_ID = 'gitlab456';
21
+ expect((0, ci_1.getCiRunId)()).toBe('gitlab-gitlab456');
22
+ });
23
+ test('should return Jenkins build ID when in Jenkins', () => {
24
+ process.env.JENKINS_URL = 'http://jenkins.example.com';
25
+ process.env.BUILD_ID = 'jenkins789';
26
+ expect((0, ci_1.getCiRunId)()).toBe('jenkins-jenkins789');
27
+ });
28
+ test('should return null when not in a known CI environment', () => {
29
+ // Ensure no CI variables are set
30
+ delete process.env.GITHUB_RUN_ID;
31
+ delete process.env.GITLAB_CI;
32
+ delete process.env.JENKINS_URL;
33
+ expect((0, ci_1.getCiRunId)()).toBeNull();
34
+ });
35
+ });
@@ -0,0 +1,40 @@
1
+ import { getCiRunId } from '../../src/ci';
2
+
3
+ describe('getCiRunId', () => {
4
+ const OLD_ENV = process.env;
5
+
6
+ beforeEach(() => {
7
+ jest.resetModules(); // Clears the module cache
8
+ // Start with a clean environment, preserving only essential vars like PATH
9
+ process.env = { PATH: OLD_ENV.PATH };
10
+ });
11
+
12
+ afterEach(() => {
13
+ process.env = OLD_ENV; // Restore original environment
14
+ });
15
+
16
+ test('should return GitHub run ID when in GitHub Actions', () => {
17
+ process.env.GITHUB_RUN_ID = 'github123';
18
+ expect(getCiRunId()).toBe('github-github123');
19
+ });
20
+
21
+ test('should return GitLab pipeline ID when in GitLab CI', () => {
22
+ process.env.GITLAB_CI = 'true';
23
+ process.env.CI_PIPELINE_ID = 'gitlab456';
24
+ expect(getCiRunId()).toBe('gitlab-gitlab456');
25
+ });
26
+
27
+ test('should return Jenkins build ID when in Jenkins', () => {
28
+ process.env.JENKINS_URL = 'http://jenkins.example.com';
29
+ process.env.BUILD_ID = 'jenkins789';
30
+ expect(getCiRunId()).toBe('jenkins-jenkins789');
31
+ });
32
+
33
+ test('should return null when not in a known CI environment', () => {
34
+ // Ensure no CI variables are set
35
+ delete process.env.GITHUB_RUN_ID;
36
+ delete process.env.GITLAB_CI;
37
+ delete process.env.JENKINS_URL;
38
+ expect(getCiRunId()).toBeNull();
39
+ });
40
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const reporter_1 = require("../../src/reporter");
4
+ // Mock the module that imports Playwright-specific types.
5
+ // This prevents a type-checking conflict within the Jest environment.
6
+ jest.mock('../../src/reporter-types', () => ({}));
7
+ describe('TestivAIPlaywrightReporter', () => {
8
+ const OLD_ENV = process.env;
9
+ beforeEach(() => {
10
+ jest.resetModules();
11
+ process.env = { ...OLD_ENV };
12
+ });
13
+ afterAll(() => {
14
+ process.env = OLD_ENV;
15
+ });
16
+ test('should initialize with API URL and key from environment variables', () => {
17
+ process.env.TESTIVAI_API_URL = 'http://env.api';
18
+ process.env.TESTIVAI_API_KEY = 'env-key';
19
+ const reporter = new reporter_1.TestivAIPlaywrightReporter();
20
+ // Accessing private options for testing purposes
21
+ expect(reporter.options.apiUrl).toBe('http://env.api');
22
+ expect(reporter.options.apiKey).toBe('env-key');
23
+ });
24
+ test('should be disabled if API URL is not provided', async () => {
25
+ const reporter = new reporter_1.TestivAIPlaywrightReporter();
26
+ // onBegin should set the apiUrl to undefined, disabling the reporter.
27
+ await reporter.onBegin({}, {});
28
+ expect(reporter.options.apiUrl).toBeUndefined();
29
+ });
30
+ test('should be disabled if API Key is not provided', async () => {
31
+ process.env.TESTIVAI_API_URL = 'http://env.api';
32
+ const reporter = new reporter_1.TestivAIPlaywrightReporter();
33
+ // onBegin should set the apiUrl to undefined, disabling the reporter.
34
+ await reporter.onBegin({}, {});
35
+ expect(reporter.options.apiUrl).toBeUndefined();
36
+ });
37
+ });
@@ -0,0 +1,43 @@
1
+ import { TestivAIPlaywrightReporter } from '../../src/reporter';
2
+
3
+ // Mock the module that imports Playwright-specific types.
4
+ // This prevents a type-checking conflict within the Jest environment.
5
+ jest.mock('../../src/reporter-types', () => ({}));
6
+
7
+ describe('TestivAIPlaywrightReporter', () => {
8
+ const OLD_ENV = process.env;
9
+
10
+ beforeEach(() => {
11
+ jest.resetModules();
12
+ process.env = { ...OLD_ENV };
13
+ });
14
+
15
+ afterAll(() => {
16
+ process.env = OLD_ENV;
17
+ });
18
+
19
+ test('should initialize with API URL and key from environment variables', () => {
20
+ process.env.TESTIVAI_API_URL = 'http://env.api';
21
+ process.env.TESTIVAI_API_KEY = 'env-key';
22
+
23
+ const reporter = new TestivAIPlaywrightReporter();
24
+ // Accessing private options for testing purposes
25
+ expect((reporter as any).options.apiUrl).toBe('http://env.api');
26
+ expect((reporter as any).options.apiKey).toBe('env-key');
27
+ });
28
+
29
+ test('should be disabled if API URL is not provided', async () => {
30
+ const reporter = new TestivAIPlaywrightReporter();
31
+ // onBegin should set the apiUrl to undefined, disabling the reporter.
32
+ await reporter.onBegin({} as any, {} as any);
33
+ expect((reporter as any).options.apiUrl).toBeUndefined();
34
+ });
35
+
36
+ test('should be disabled if API Key is not provided', async () => {
37
+ process.env.TESTIVAI_API_URL = 'http://env.api';
38
+ const reporter = new TestivAIPlaywrightReporter();
39
+ // onBegin should set the apiUrl to undefined, disabling the reporter.
40
+ await reporter.onBegin({} as any, {} as any);
41
+ expect((reporter as any).options.apiUrl).toBeUndefined();
42
+ });
43
+ });
package/dist/ci.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Utilities for detecting CI/CD environments and extracting a unique run identifier.
3
+ */
4
+ /**
5
+ * Gets a unique identifier for the current CI run.
6
+ * This ID is used to group snapshots from parallel shards into a single batch.
7
+ *
8
+ * @returns A unique string identifier for the CI run, or null if not in a known CI environment.
9
+ */
10
+ export declare function getCiRunId(): string | null;
package/dist/ci.js ADDED
@@ -0,0 +1,35 @@
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
+ /**
8
+ * Gets a unique identifier for the current CI run.
9
+ * This ID is used to group snapshots from parallel shards into a single batch.
10
+ *
11
+ * @returns A unique string identifier for the CI run, or null if not in a known CI environment.
12
+ */
13
+ function getCiRunId() {
14
+ // GitHub Actions
15
+ if (process.env.GITHUB_RUN_ID) {
16
+ return `github-${process.env.GITHUB_RUN_ID}`;
17
+ }
18
+ // GitLab CI
19
+ if (process.env.GITLAB_CI) {
20
+ return `gitlab-${process.env.CI_PIPELINE_ID}`;
21
+ }
22
+ // Jenkins
23
+ if (process.env.JENKINS_URL) {
24
+ return `jenkins-${process.env.BUILD_ID}`;
25
+ }
26
+ // CircleCI
27
+ if (process.env.CIRCLECI) {
28
+ return `circleci-${process.env.CIRCLE_WORKFLOW_ID}`;
29
+ }
30
+ // Travis CI
31
+ if (process.env.TRAVIS) {
32
+ return `travis-${process.env.TRAVIS_BUILD_ID}`;
33
+ }
34
+ return null;
35
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ declare function createConfigFile(): Promise<void>;
3
+ export { createConfigFile };
@@ -0,0 +1,146 @@
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
+ layout: {
105
+ sensitivity: 2, // Balanced sensitivity (0-4 scale)
106
+ tolerance: 1.0, // 1 pixel base tolerance
107
+ },
108
+ ai: {
109
+ sensitivity: 2, // Balanced AI analysis (0-4 scale)
110
+ confidence: 0.7, // 70% confidence required for AI_BUG
111
+ }
112
+ };
113
+ `;
114
+ async function createConfigFile() {
115
+ const configPath = path.join(process.cwd(), 'testivai.config.ts');
116
+ // Check if config already exists
117
+ if (await fs.pathExists(configPath)) {
118
+ console.log('❌ TestivAI config already exists at:', configPath);
119
+ console.log(' Delete it first if you want to regenerate it.');
120
+ process.exit(1);
121
+ }
122
+ try {
123
+ // Create the config file
124
+ await fs.writeFile(configPath, DEFAULT_CONFIG, 'utf8');
125
+ console.log('✅ TestivAI configuration created successfully!');
126
+ console.log('📁 Config file:', configPath);
127
+ console.log('');
128
+ console.log('📖 Next steps:');
129
+ console.log(' 1. Review and customize the configuration in testivai.config.ts');
130
+ console.log(' 2. Update your playwright.config.ts to use TestivAI reporter');
131
+ console.log(' 3. Run your tests with: npx playwright test');
132
+ console.log('');
133
+ console.log('💡 Need help? Check the comments in testivai.config.ts for examples');
134
+ }
135
+ catch (error) {
136
+ console.error('❌ Failed to create configuration file:', error);
137
+ process.exit(1);
138
+ }
139
+ }
140
+ // Main execution
141
+ if (require.main === module) {
142
+ createConfigFile().catch(error => {
143
+ console.error('❌ CLI init failed:', error);
144
+ process.exit(1);
145
+ });
146
+ }