@testivai/witness-playwright 1.0.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/__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/__tests__/unit/structureAnalyzer.spec.js +212 -0
- package/__tests__/unit/types.spec.ts +179 -0
- package/dist/__tests__/unit/ci.spec.d.ts +1 -0
- package/dist/__tests__/unit/ci.spec.js +226 -0
- package/dist/__tests__/unit/compression.spec.d.ts +4 -0
- package/dist/__tests__/unit/compression.spec.js +46 -0
- package/dist/ci.d.ts +30 -0
- package/dist/ci.js +117 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +47 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +158 -0
- package/dist/config/loader.d.ts +29 -0
- package/dist/config/loader.js +251 -0
- package/dist/domAnalyzer.d.ts +10 -0
- package/dist/domAnalyzer.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +11 -0
- package/dist/reporter-entry.d.ts +2 -0
- package/dist/reporter-entry.js +5 -0
- package/dist/reporter-types.d.ts +2 -0
- package/dist/reporter-types.js +2 -0
- package/dist/reporter.d.ts +21 -0
- package/dist/reporter.js +249 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +601 -0
- package/dist/structureAnalyzer.d.ts +12 -0
- package/dist/structureAnalyzer.js +288 -0
- package/dist/types.d.ts +368 -0
- package/dist/types.js +10 -0
- package/examples/structure-analysis-example.spec.ts +118 -0
- package/examples/structure-analysis.config.ts +159 -0
- package/jest.config.js +8 -0
- package/package.json +51 -0
- package/playwright.config.ts +11 -0
- package/src/__tests__/unit/ci.spec.ts +257 -0
- package/src/__tests__/unit/compression.spec.ts +52 -0
- package/src/ci.ts +140 -0
- package/src/cli/index.ts +49 -0
- package/src/cli/init.ts +131 -0
- package/src/config/loader.ts +238 -0
- package/src/index.ts +14 -0
- package/src/reporter-entry.ts +6 -0
- package/src/reporter-types.ts +5 -0
- package/src/reporter.ts +251 -0
- package/src/snapshot.ts +632 -0
- package/src/structureAnalyzer.ts +338 -0
- package/src/types.ts +388 -0
- package/tsconfig.jest.json +7 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { testivai } from '../src';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structure Analysis Example
|
|
6
|
+
* @renamed Was `DOM Analysis Example` — renamed to conceal internal layer terminology (IP protection)
|
|
7
|
+
*/
|
|
8
|
+
test.describe('Structure Analysis Example', () => {
|
|
9
|
+
test('should capture structure analysis with snapshot', async ({ page }, testInfo) => {
|
|
10
|
+
// Navigate to a test page
|
|
11
|
+
await page.goto('https://example.com');
|
|
12
|
+
|
|
13
|
+
// Capture snapshot with structure analysis enabled
|
|
14
|
+
// @renamed: dom → structure (IP protection)
|
|
15
|
+
await testivai.witness(page, testInfo, 'example-homepage', {
|
|
16
|
+
structure: {
|
|
17
|
+
enableFingerprint: true,
|
|
18
|
+
enableStructure: true,
|
|
19
|
+
enableSemantic: true,
|
|
20
|
+
ignoreAttributes: ['data-testid', 'class'],
|
|
21
|
+
ignoreContentPatterns: [/\d{4}-\d{2}-\d{2}/], // Ignore dates
|
|
22
|
+
},
|
|
23
|
+
layout: {
|
|
24
|
+
sensitivity: 2,
|
|
25
|
+
tolerance: 2,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// The structure analysis will be included in the snapshot metadata
|
|
30
|
+
// and can be used for smarter change detection
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should demonstrate structure fingerprint consistency', async ({ page }, testInfo) => {
|
|
34
|
+
// Load a page with dynamic content
|
|
35
|
+
await page.setContent(`
|
|
36
|
+
<html>
|
|
37
|
+
<body>
|
|
38
|
+
<h1>Test Page</h1>
|
|
39
|
+
<p>Current time: <span id="time">12:34:56</span></p>
|
|
40
|
+
<p>Date: <span id="date">2024-01-12</span></p>
|
|
41
|
+
<button data-testid="submit-btn">Submit</button>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
// Capture first snapshot
|
|
47
|
+
await testivai.witness(page, testInfo, 'dynamic-content-1', {
|
|
48
|
+
structure: {
|
|
49
|
+
enableFingerprint: true,
|
|
50
|
+
ignoreContentPatterns: [/\d{4}-\d{2}-\d{2}/, /\d{1,2}:\d{2}:\d{2}/],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Update dynamic content
|
|
55
|
+
await page.evaluate(() => {
|
|
56
|
+
document.getElementById('time')!.textContent = '13:45:67';
|
|
57
|
+
document.getElementById('date')!.textContent = '2024-01-13';
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Capture second snapshot
|
|
61
|
+
await testivai.witness(page, testInfo, 'dynamic-content-2', {
|
|
62
|
+
structure: {
|
|
63
|
+
enableFingerprint: true,
|
|
64
|
+
ignoreContentPatterns: [/\d{4}-\d{2}-\d{2}/, /\d{1,2}:\d{2}:\d{2}/],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Both snapshots should have the same fingerprint despite time/date changes
|
|
69
|
+
// This demonstrates how structure analysis reduces false positives
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should detect structural changes', async ({ page }, testInfo) => {
|
|
73
|
+
// Initial page structure
|
|
74
|
+
await page.setContent(`
|
|
75
|
+
<html>
|
|
76
|
+
<body>
|
|
77
|
+
<header>
|
|
78
|
+
<h1>My Site</h1>
|
|
79
|
+
</header>
|
|
80
|
+
<main>
|
|
81
|
+
<section>
|
|
82
|
+
<h2>Section 1</h2>
|
|
83
|
+
<p>Content here</p>
|
|
84
|
+
</section>
|
|
85
|
+
</main>
|
|
86
|
+
</body>
|
|
87
|
+
</html>
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
// Capture baseline
|
|
91
|
+
await testivai.witness(page, testInfo, 'structure-baseline', {
|
|
92
|
+
structure: {
|
|
93
|
+
enableFingerprint: true,
|
|
94
|
+
enableStructure: true,
|
|
95
|
+
enableSemantic: true,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Add new element
|
|
100
|
+
await page.evaluate(() => {
|
|
101
|
+
const section = document.querySelector('section');
|
|
102
|
+
const button = document.createElement('button');
|
|
103
|
+
button.textContent = 'New Button';
|
|
104
|
+
section!.appendChild(button);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Capture after change
|
|
108
|
+
await testivai.witness(page, testInfo, 'structure-changed', {
|
|
109
|
+
structure: {
|
|
110
|
+
enableFingerprint: true,
|
|
111
|
+
enableStructure: true,
|
|
112
|
+
enableSemantic: true,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// The fingerprint should change, detecting the structural modification
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { TestivAIConfig } from '../src/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example configuration for structure analysis in TestivAI
|
|
5
|
+
*
|
|
6
|
+
* This shows how to configure structure analysis to reduce false positives
|
|
7
|
+
* and focus on meaningful structural changes.
|
|
8
|
+
*
|
|
9
|
+
* @renamed Was `dom-analysis.config.ts` — renamed to conceal internal layer terminology (IP protection)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const structureAnalysisConfig: TestivAIConfig = {
|
|
13
|
+
// Structure analysis settings
|
|
14
|
+
// @renamed: dom → structure (IP protection)
|
|
15
|
+
structure: {
|
|
16
|
+
// Enable fingerprint generation (recommended for quick change detection)
|
|
17
|
+
enableFingerprint: true,
|
|
18
|
+
|
|
19
|
+
// Enable structural analysis (counts elements, depth, etc.)
|
|
20
|
+
enableStructure: true,
|
|
21
|
+
|
|
22
|
+
// Enable semantic analysis (headings, landmarks, etc.)
|
|
23
|
+
enableSemantic: true,
|
|
24
|
+
|
|
25
|
+
// Attributes to ignore (prevents false positives from dynamic attributes)
|
|
26
|
+
ignoreAttributes: [
|
|
27
|
+
'data-testid', // Test attributes
|
|
28
|
+
'data-reactid', // React internal IDs
|
|
29
|
+
'data-reactroot', // React root marker
|
|
30
|
+
'ng-version', // Angular version
|
|
31
|
+
'ng-reflect-router-outlet', // Angular router
|
|
32
|
+
'data-ng-version', // Angular data attribute
|
|
33
|
+
'style', // Inline styles (can be dynamic)
|
|
34
|
+
'class', // CSS classes (often change)
|
|
35
|
+
'id', // IDs (can be auto-generated)
|
|
36
|
+
'aria-busy', // ARIA busy state
|
|
37
|
+
'aria-expanded', // ARIA expanded state
|
|
38
|
+
],
|
|
39
|
+
|
|
40
|
+
// Elements to ignore completely
|
|
41
|
+
ignoreElements: [
|
|
42
|
+
'script', // Scripts don't affect visual output
|
|
43
|
+
'style', // Style tags
|
|
44
|
+
'noscript', // Noscript content
|
|
45
|
+
'meta', // Meta tags
|
|
46
|
+
'link', // Link tags (CSS, favicons)
|
|
47
|
+
'title', // Page title
|
|
48
|
+
],
|
|
49
|
+
|
|
50
|
+
// Content patterns to ignore (prevents false positives from dynamic content)
|
|
51
|
+
ignoreContentPatterns: [
|
|
52
|
+
/\d{4}-\d{2}-\d{2}/, // Dates: 2024-01-12
|
|
53
|
+
/\d{1,2}:\d{2}(:\d{2})?/, // Times: 10:30 or 10:30:45
|
|
54
|
+
/\b\d{4}\b/, // Years: 2024
|
|
55
|
+
/\b\d+\b/, // Any standalone number
|
|
56
|
+
/uuid-/i, // UUID strings
|
|
57
|
+
/_\d+/, // Underscore + number: _123
|
|
58
|
+
/\$\d+\.?\d*/, // Currency: $19.99
|
|
59
|
+
/\b\d+\.\d+\b/, // Decimal numbers
|
|
60
|
+
/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i, // Full UUIDs
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Other TestivAI settings
|
|
65
|
+
layout: {
|
|
66
|
+
sensitivity: 2, // Balanced sensitivity
|
|
67
|
+
tolerance: 2,
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
ai: {
|
|
71
|
+
sensitivity: 2,
|
|
72
|
+
confidence: 0.8,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Configuration for strict structure analysis (fewer false positives)
|
|
78
|
+
* Use this when you want to catch only significant structural changes
|
|
79
|
+
*/
|
|
80
|
+
export const strictStructureConfig: TestivAIConfig = {
|
|
81
|
+
structure: {
|
|
82
|
+
enableFingerprint: true,
|
|
83
|
+
enableStructure: false, // Skip element counts (too sensitive)
|
|
84
|
+
enableSemantic: true, // Focus on semantic changes only
|
|
85
|
+
ignoreAttributes: [
|
|
86
|
+
'data-testid', 'data-reactid', 'data-reactroot', 'ng-version',
|
|
87
|
+
'style', 'class', 'id', 'aria-busy', 'aria-expanded',
|
|
88
|
+
'data-cy', // Cypress testing attributes
|
|
89
|
+
'data-qa', // QA attributes
|
|
90
|
+
],
|
|
91
|
+
ignoreElements: [
|
|
92
|
+
'script', 'style', 'noscript', 'meta', 'link', 'title',
|
|
93
|
+
'span', // Inline elements (too many small changes)
|
|
94
|
+
],
|
|
95
|
+
ignoreContentPatterns: [
|
|
96
|
+
/\d{4}-\d{2}-\d{2}/,
|
|
97
|
+
/\d{1,2}:\d{2}(:\d{2})?/,
|
|
98
|
+
/\b\d{4}\b/,
|
|
99
|
+
/\b\d+\b/,
|
|
100
|
+
/uuid-/i,
|
|
101
|
+
/_\d+/,
|
|
102
|
+
/\$\d+\.?\d*/,
|
|
103
|
+
/\b\d+\.\d+\b/,
|
|
104
|
+
/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i,
|
|
105
|
+
/\w+\d+\w*/, // Alphanumeric with numbers (IDs)
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
layout: {
|
|
109
|
+
sensitivity: 1, // More strict
|
|
110
|
+
tolerance: 1,
|
|
111
|
+
},
|
|
112
|
+
ai: {
|
|
113
|
+
sensitivity: 1, // More conservative
|
|
114
|
+
confidence: 0.9,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Configuration for component-focused analysis
|
|
120
|
+
* Use this when you care about component changes but not structure details
|
|
121
|
+
*/
|
|
122
|
+
export const componentFocusedConfig: TestivAIConfig = {
|
|
123
|
+
structure: {
|
|
124
|
+
enableFingerprint: false, // Skip fingerprint
|
|
125
|
+
enableStructure: false, // Skip structure
|
|
126
|
+
enableSemantic: false, // Skip semantic
|
|
127
|
+
// Focus only on component detection through data-testid
|
|
128
|
+
ignoreAttributes: ['class', 'style', 'id'],
|
|
129
|
+
ignoreElements: ['script', 'style', 'noscript', 'meta'],
|
|
130
|
+
ignoreContentPatterns: [/\d+/], // Only ignore numbers
|
|
131
|
+
},
|
|
132
|
+
layout: {
|
|
133
|
+
sensitivity: 2,
|
|
134
|
+
tolerance: 3, // More lenient for layout
|
|
135
|
+
},
|
|
136
|
+
ai: {
|
|
137
|
+
sensitivity: 3, // More aggressive AI analysis
|
|
138
|
+
confidence: 0.7,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Example usage in a Playwright test
|
|
144
|
+
*/
|
|
145
|
+
/*
|
|
146
|
+
import { test } from '@playwright/test';
|
|
147
|
+
import { testivai } from '@testivai/witness-playwright';
|
|
148
|
+
import { structureAnalysisConfig } from './structure-analysis.config';
|
|
149
|
+
|
|
150
|
+
test('example with structure analysis', async ({ page }) => {
|
|
151
|
+
await page.goto('https://example.com');
|
|
152
|
+
|
|
153
|
+
// Capture snapshot with structure analysis
|
|
154
|
+
await testivai.witness(page, test.info(), 'homepage', structureAnalysisConfig);
|
|
155
|
+
|
|
156
|
+
// The structure analysis will be included in the snapshot metadata
|
|
157
|
+
// and can be used for smarter change detection
|
|
158
|
+
});
|
|
159
|
+
*/
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@testivai/witness-playwright",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Playwright sensor for Testivai Visual Regression Test system",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"testivai": "dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./reporter": "./dist/reporter-entry.js",
|
|
13
|
+
"./cli": "./dist/cli/index.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:unit": "jest"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"playwright",
|
|
22
|
+
"visual-regression",
|
|
23
|
+
"testing",
|
|
24
|
+
"testivai"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@testivai/common": "^0.2.0",
|
|
33
|
+
"axios": "^1.6.0",
|
|
34
|
+
"chalk": "^4.1.2",
|
|
35
|
+
"commander": "^11.0.0",
|
|
36
|
+
"cross-fetch": "^4.0.0",
|
|
37
|
+
"fs-extra": "^11.2.0",
|
|
38
|
+
"sharp": "^0.34.5",
|
|
39
|
+
"simple-git": "^3.21.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@playwright/test": "^1.40.0",
|
|
43
|
+
"@types/fs-extra": "^11.0.4",
|
|
44
|
+
"@types/jest": "^30.0.0",
|
|
45
|
+
"@types/node": "^20.10.0",
|
|
46
|
+
"jest": "^30.2.0",
|
|
47
|
+
"ts-jest": "^29.4.5",
|
|
48
|
+
"ts-node": "^10.9.0",
|
|
49
|
+
"typescript": "^5.3.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { getCiRunId, getCiInfo } from '../../ci';
|
|
2
|
+
|
|
3
|
+
describe('CI Environment Detection', () => {
|
|
4
|
+
const originalEnv = process.env;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Reset env to a clean slate before each test
|
|
8
|
+
process.env = { ...originalEnv };
|
|
9
|
+
// Remove all CI-related env vars
|
|
10
|
+
delete process.env.GITHUB_RUN_ID;
|
|
11
|
+
delete process.env.GITHUB_ACTIONS;
|
|
12
|
+
delete process.env.GITHUB_SERVER_URL;
|
|
13
|
+
delete process.env.GITHUB_REPOSITORY;
|
|
14
|
+
delete process.env.GITHUB_EVENT_NUMBER;
|
|
15
|
+
delete process.env.GITHUB_REF;
|
|
16
|
+
delete process.env.GITLAB_CI;
|
|
17
|
+
delete process.env.CI_PIPELINE_ID;
|
|
18
|
+
delete process.env.CI_MERGE_REQUEST_IID;
|
|
19
|
+
delete process.env.CI_PIPELINE_URL;
|
|
20
|
+
delete process.env.CIRCLECI;
|
|
21
|
+
delete process.env.CIRCLE_WORKFLOW_ID;
|
|
22
|
+
delete process.env.CIRCLE_PULL_REQUEST;
|
|
23
|
+
delete process.env.CIRCLE_BUILD_URL;
|
|
24
|
+
delete process.env.JENKINS_URL;
|
|
25
|
+
delete process.env.BUILD_ID;
|
|
26
|
+
delete process.env.BUILD_URL;
|
|
27
|
+
delete process.env.CHANGE_ID;
|
|
28
|
+
delete process.env.TRAVIS;
|
|
29
|
+
delete process.env.TRAVIS_BUILD_ID;
|
|
30
|
+
delete process.env.TRAVIS_PULL_REQUEST;
|
|
31
|
+
delete process.env.TRAVIS_BUILD_WEB_URL;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
process.env = originalEnv;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getCiRunId', () => {
|
|
39
|
+
it('should return null when not in CI', () => {
|
|
40
|
+
expect(getCiRunId()).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should detect GitHub Actions', () => {
|
|
44
|
+
process.env.GITHUB_RUN_ID = '123456';
|
|
45
|
+
expect(getCiRunId()).toBe('github-123456');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should detect GitLab CI', () => {
|
|
49
|
+
process.env.GITLAB_CI = 'true';
|
|
50
|
+
process.env.CI_PIPELINE_ID = '789';
|
|
51
|
+
expect(getCiRunId()).toBe('gitlab-789');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should detect Jenkins', () => {
|
|
55
|
+
process.env.JENKINS_URL = 'https://jenkins.example.com';
|
|
56
|
+
process.env.BUILD_ID = '42';
|
|
57
|
+
expect(getCiRunId()).toBe('jenkins-42');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should detect CircleCI', () => {
|
|
61
|
+
process.env.CIRCLECI = 'true';
|
|
62
|
+
process.env.CIRCLE_WORKFLOW_ID = 'wf-abc';
|
|
63
|
+
expect(getCiRunId()).toBe('circleci-wf-abc');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should detect Travis CI', () => {
|
|
67
|
+
process.env.TRAVIS = 'true';
|
|
68
|
+
process.env.TRAVIS_BUILD_ID = '999';
|
|
69
|
+
expect(getCiRunId()).toBe('travis-999');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should prioritize GitHub Actions over other providers', () => {
|
|
73
|
+
process.env.GITHUB_RUN_ID = '111';
|
|
74
|
+
process.env.GITLAB_CI = 'true';
|
|
75
|
+
process.env.CI_PIPELINE_ID = '222';
|
|
76
|
+
expect(getCiRunId()).toBe('github-111');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('getCiInfo', () => {
|
|
81
|
+
it('should return null when not in CI', () => {
|
|
82
|
+
expect(getCiInfo()).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('GitHub Actions', () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
process.env.GITHUB_ACTIONS = 'true';
|
|
88
|
+
process.env.GITHUB_RUN_ID = '12345';
|
|
89
|
+
process.env.GITHUB_REPOSITORY = 'owner/repo';
|
|
90
|
+
process.env.GITHUB_SERVER_URL = 'https://github.com';
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return basic GitHub Actions info', () => {
|
|
94
|
+
const info = getCiInfo();
|
|
95
|
+
expect(info).toEqual({
|
|
96
|
+
provider: 'github_actions',
|
|
97
|
+
prNumber: undefined,
|
|
98
|
+
runUrl: 'https://github.com/owner/repo/actions/runs/12345',
|
|
99
|
+
buildId: '12345',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should detect PR number from GITHUB_EVENT_NUMBER', () => {
|
|
104
|
+
process.env.GITHUB_EVENT_NUMBER = '42';
|
|
105
|
+
const info = getCiInfo();
|
|
106
|
+
expect(info?.prNumber).toBe(42);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should detect PR number from GITHUB_REF', () => {
|
|
110
|
+
process.env.GITHUB_REF = 'refs/pull/99/merge';
|
|
111
|
+
const info = getCiInfo();
|
|
112
|
+
expect(info?.prNumber).toBe(99);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should not set PR number for non-PR refs', () => {
|
|
116
|
+
process.env.GITHUB_REF = 'refs/heads/main';
|
|
117
|
+
const info = getCiInfo();
|
|
118
|
+
expect(info?.prNumber).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should use default server URL', () => {
|
|
122
|
+
delete process.env.GITHUB_SERVER_URL;
|
|
123
|
+
const info = getCiInfo();
|
|
124
|
+
expect(info?.runUrl).toBe('https://github.com/owner/repo/actions/runs/12345');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle missing repository', () => {
|
|
128
|
+
delete process.env.GITHUB_REPOSITORY;
|
|
129
|
+
const info = getCiInfo();
|
|
130
|
+
expect(info?.runUrl).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('GitLab CI', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
process.env.GITLAB_CI = 'true';
|
|
137
|
+
process.env.CI_PIPELINE_ID = '456';
|
|
138
|
+
process.env.CI_PIPELINE_URL = 'https://gitlab.com/project/-/pipelines/456';
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return basic GitLab CI info', () => {
|
|
142
|
+
const info = getCiInfo();
|
|
143
|
+
expect(info).toEqual({
|
|
144
|
+
provider: 'gitlab_ci',
|
|
145
|
+
prNumber: undefined,
|
|
146
|
+
runUrl: 'https://gitlab.com/project/-/pipelines/456',
|
|
147
|
+
buildId: '456',
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should detect MR number', () => {
|
|
152
|
+
process.env.CI_MERGE_REQUEST_IID = '7';
|
|
153
|
+
const info = getCiInfo();
|
|
154
|
+
expect(info?.prNumber).toBe(7);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('CircleCI', () => {
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
process.env.CIRCLECI = 'true';
|
|
161
|
+
process.env.CIRCLE_WORKFLOW_ID = 'wf-xyz';
|
|
162
|
+
process.env.CIRCLE_BUILD_URL = 'https://circleci.com/gh/owner/repo/123';
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return basic CircleCI info', () => {
|
|
166
|
+
const info = getCiInfo();
|
|
167
|
+
expect(info).toEqual({
|
|
168
|
+
provider: 'circleci',
|
|
169
|
+
prNumber: undefined,
|
|
170
|
+
runUrl: 'https://circleci.com/gh/owner/repo/123',
|
|
171
|
+
buildId: 'wf-xyz',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should detect PR number from pull request URL', () => {
|
|
176
|
+
process.env.CIRCLE_PULL_REQUEST = 'https://github.com/owner/repo/pull/55';
|
|
177
|
+
const info = getCiInfo();
|
|
178
|
+
expect(info?.prNumber).toBe(55);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('Jenkins', () => {
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
process.env.JENKINS_URL = 'https://jenkins.example.com';
|
|
185
|
+
process.env.BUILD_ID = '100';
|
|
186
|
+
process.env.BUILD_URL = 'https://jenkins.example.com/job/my-job/100/';
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return basic Jenkins info', () => {
|
|
190
|
+
const info = getCiInfo();
|
|
191
|
+
expect(info).toEqual({
|
|
192
|
+
provider: 'jenkins',
|
|
193
|
+
prNumber: undefined,
|
|
194
|
+
runUrl: 'https://jenkins.example.com/job/my-job/100/',
|
|
195
|
+
buildId: '100',
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should detect PR number from CHANGE_ID', () => {
|
|
200
|
+
process.env.CHANGE_ID = '33';
|
|
201
|
+
const info = getCiInfo();
|
|
202
|
+
expect(info?.prNumber).toBe(33);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('Travis CI', () => {
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
process.env.TRAVIS = 'true';
|
|
209
|
+
process.env.TRAVIS_BUILD_ID = '888';
|
|
210
|
+
process.env.TRAVIS_BUILD_WEB_URL = 'https://app.travis-ci.com/owner/repo/builds/888';
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should return basic Travis CI info', () => {
|
|
214
|
+
const info = getCiInfo();
|
|
215
|
+
expect(info).toEqual({
|
|
216
|
+
provider: 'travis_ci',
|
|
217
|
+
prNumber: undefined,
|
|
218
|
+
runUrl: 'https://app.travis-ci.com/owner/repo/builds/888',
|
|
219
|
+
buildId: '888',
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should detect PR number', () => {
|
|
224
|
+
process.env.TRAVIS_PULL_REQUEST = '17';
|
|
225
|
+
const info = getCiInfo();
|
|
226
|
+
expect(info?.prNumber).toBe(17);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should not set PR number when TRAVIS_PULL_REQUEST is false', () => {
|
|
230
|
+
process.env.TRAVIS_PULL_REQUEST = 'false';
|
|
231
|
+
const info = getCiInfo();
|
|
232
|
+
expect(info?.prNumber).toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Payload shape matches Core API CiInfo schema', () => {
|
|
237
|
+
it('should produce fields matching Core API aliases: provider, prNumber, runUrl, buildId', () => {
|
|
238
|
+
process.env.GITHUB_ACTIONS = 'true';
|
|
239
|
+
process.env.GITHUB_RUN_ID = '1';
|
|
240
|
+
process.env.GITHUB_REPOSITORY = 'o/r';
|
|
241
|
+
process.env.GITHUB_EVENT_NUMBER = '10';
|
|
242
|
+
|
|
243
|
+
const info = getCiInfo();
|
|
244
|
+
expect(info).toBeDefined();
|
|
245
|
+
// These are the exact camelCase keys the Core API CiInfo Pydantic model expects via aliases
|
|
246
|
+
expect(info).toHaveProperty('provider');
|
|
247
|
+
expect(info).toHaveProperty('prNumber');
|
|
248
|
+
expect(info).toHaveProperty('runUrl');
|
|
249
|
+
expect(info).toHaveProperty('buildId');
|
|
250
|
+
// Ensure no unexpected snake_case keys
|
|
251
|
+
expect(info).not.toHaveProperty('pr_number');
|
|
252
|
+
expect(info).not.toHaveProperty('run_url');
|
|
253
|
+
expect(info).not.toHaveProperty('build_id');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Playwright SDK compression integration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TestivAIPlaywrightReporter } from '../../reporter';
|
|
6
|
+
import { CompressionHelper } from '@testivai/common';
|
|
7
|
+
|
|
8
|
+
describe('Playwright Reporter Compression', () => {
|
|
9
|
+
let reporter: TestivAIPlaywrightReporter;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
reporter = new TestivAIPlaywrightReporter({
|
|
13
|
+
apiKey: 'test-key',
|
|
14
|
+
apiUrl: 'http://localhost:3000',
|
|
15
|
+
compression: {
|
|
16
|
+
compressUploads: true,
|
|
17
|
+
compressionThreshold: 100, // Small threshold for testing
|
|
18
|
+
compressionLevel: 6,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should initialize with compression helper', () => {
|
|
24
|
+
expect(reporter).toBeDefined();
|
|
25
|
+
// Access private property through type assertion for testing
|
|
26
|
+
const helper = (reporter as any).compressionHelper;
|
|
27
|
+
expect(helper).toBeInstanceOf(CompressionHelper);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should use custom compression options', () => {
|
|
31
|
+
const customReporter = new TestivAIPlaywrightReporter({
|
|
32
|
+
apiKey: 'test-key',
|
|
33
|
+
compression: {
|
|
34
|
+
compressUploads: false,
|
|
35
|
+
compressionLevel: 9,
|
|
36
|
+
compressionThreshold: 1000,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const helper = (customReporter as any).compressionHelper;
|
|
41
|
+
expect(helper).toBeInstanceOf(CompressionHelper);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should use default compression options when none provided', () => {
|
|
45
|
+
const defaultReporter = new TestivAIPlaywrightReporter({
|
|
46
|
+
apiKey: 'test-key',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const helper = (defaultReporter as any).compressionHelper;
|
|
50
|
+
expect(helper).toBeInstanceOf(CompressionHelper);
|
|
51
|
+
});
|
|
52
|
+
});
|