@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.
- package/README.md +65 -0
- package/__tests__/.gitkeep +0 -0
- package/__tests__/config-integration.spec.ts +102 -0
- package/__tests__/snapshot.spec.d.ts +1 -0
- package/__tests__/snapshot.spec.js +81 -0
- package/__tests__/snapshot.spec.ts +58 -0
- package/__tests__/unit/ci.spec.d.ts +1 -0
- package/__tests__/unit/ci.spec.js +35 -0
- package/__tests__/unit/ci.spec.ts +40 -0
- package/__tests__/unit/reporter.spec.d.ts +1 -0
- package/__tests__/unit/reporter.spec.js +37 -0
- package/__tests__/unit/reporter.spec.ts +43 -0
- package/dist/ci.d.ts +10 -0
- package/dist/ci.js +35 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +146 -0
- package/dist/config/loader.d.ts +29 -0
- package/dist/config/loader.js +232 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +24 -0
- package/dist/reporter-types.d.ts +2 -0
- package/dist/reporter-types.js +2 -0
- package/dist/reporter.d.ts +16 -0
- package/dist/reporter.js +155 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +122 -0
- package/dist/types.d.ts +181 -0
- package/dist/types.js +10 -0
- package/jest.config.js +11 -0
- package/package.json +47 -0
- package/playwright.config.ts +11 -0
- package/progress.md +620 -0
- package/src/ci.ts +34 -0
- package/src/cli/init.ts +119 -0
- package/src/config/loader.ts +219 -0
- package/src/index.ts +9 -0
- package/src/reporter-types.ts +5 -0
- package/src/reporter.ts +148 -0
- package/src/snapshot.ts +103 -0
- package/src/types.ts +193 -0
- package/test-results/.last-run.json +4 -0
- package/tsconfig.jest.json +7 -0
- 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
|
+
}
|
package/dist/cli/init.js
ADDED
|
@@ -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
|
+
}
|