@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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*
|
|
4
|
+
* Tests to verify that all renamed types are correctly exported
|
|
5
|
+
* and that the public API no longer exposes internal 5-layer terminology.
|
|
6
|
+
*
|
|
7
|
+
* Terminology mapping verified:
|
|
8
|
+
* DOMData → StructureData
|
|
9
|
+
* DOMAnalysis → StructureAnalysis
|
|
10
|
+
* DOMAnalysisConfig → StructureAnalysisConfig
|
|
11
|
+
* DOMChange → StructureChange
|
|
12
|
+
* CSSData → StylesData
|
|
13
|
+
* SnapshotPayload.dom → SnapshotPayload.structure
|
|
14
|
+
* SnapshotPayload.css → SnapshotPayload.styles
|
|
15
|
+
* TestivAIConfig.dom → TestivAIConfig.structure
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
StructureData,
|
|
20
|
+
StructureAnalysis,
|
|
21
|
+
StructureAnalysisConfig,
|
|
22
|
+
StructureChange,
|
|
23
|
+
StylesData,
|
|
24
|
+
SnapshotPayload,
|
|
25
|
+
TestivAIConfig,
|
|
26
|
+
TestivAIProjectConfig,
|
|
27
|
+
LayoutData,
|
|
28
|
+
} from '../../src/types';
|
|
29
|
+
|
|
30
|
+
describe('Renamed SDK Types', () => {
|
|
31
|
+
describe('StructureData (was DOMData)', () => {
|
|
32
|
+
test('should have html field', () => {
|
|
33
|
+
const data: StructureData = { html: '<div>test</div>' };
|
|
34
|
+
expect(data.html).toBe('<div>test</div>');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should accept optional styles field', () => {
|
|
38
|
+
const data: StructureData = { html: '<div/>', styles: { color: 'red' } };
|
|
39
|
+
expect(data.styles).toEqual({ color: 'red' });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('StylesData (was CSSData)', () => {
|
|
44
|
+
test('should have computed_styles field', () => {
|
|
45
|
+
const data: StylesData = {
|
|
46
|
+
computed_styles: {
|
|
47
|
+
'div.container': { color: 'rgb(0,0,0)', 'font-size': '16px' },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
expect(Object.keys(data.computed_styles)).toHaveLength(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('StructureAnalysis (was DOMAnalysis)', () => {
|
|
55
|
+
test('should accept fingerprint', () => {
|
|
56
|
+
const analysis: StructureAnalysis = { fingerprint: 'abc123' };
|
|
57
|
+
expect(analysis.fingerprint).toBe('abc123');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should accept structure info', () => {
|
|
61
|
+
const analysis: StructureAnalysis = {
|
|
62
|
+
structure: {
|
|
63
|
+
totalElements: 10,
|
|
64
|
+
elementTypes: { div: 5 },
|
|
65
|
+
maxDepth: 3,
|
|
66
|
+
interactiveElements: { buttons: 1, inputs: 0, links: 2, forms: 0 },
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
expect(analysis.structure!.totalElements).toBe(10);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should accept semantic info', () => {
|
|
73
|
+
const analysis: StructureAnalysis = {
|
|
74
|
+
semantic: {
|
|
75
|
+
headings: { h1: 1 },
|
|
76
|
+
landmarks: {
|
|
77
|
+
hasHeader: true,
|
|
78
|
+
hasNav: false,
|
|
79
|
+
hasMain: true,
|
|
80
|
+
hasFooter: false,
|
|
81
|
+
hasAside: false,
|
|
82
|
+
},
|
|
83
|
+
lists: { ordered: 0, unordered: 1 },
|
|
84
|
+
tables: 0,
|
|
85
|
+
images: 2,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
expect(analysis.semantic!.landmarks.hasHeader).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('StructureAnalysisConfig (was DOMAnalysisConfig)', () => {
|
|
93
|
+
test('should accept all config fields', () => {
|
|
94
|
+
const config: StructureAnalysisConfig = {
|
|
95
|
+
enableFingerprint: true,
|
|
96
|
+
enableStructure: true,
|
|
97
|
+
enableSemantic: false,
|
|
98
|
+
ignoreAttributes: ['data-testid'],
|
|
99
|
+
ignoreElements: ['script'],
|
|
100
|
+
ignoreContentPatterns: [/\d+/],
|
|
101
|
+
};
|
|
102
|
+
expect(config.enableFingerprint).toBe(true);
|
|
103
|
+
expect(config.ignoreAttributes).toEqual(['data-testid']);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('StructureChange (was DOMChange)', () => {
|
|
108
|
+
test('should have correct shape', () => {
|
|
109
|
+
const change: StructureChange = {
|
|
110
|
+
type: 'fingerprint',
|
|
111
|
+
severity: 'high',
|
|
112
|
+
description: 'Page structure has changed',
|
|
113
|
+
details: { baseline: 'a', current: 'b' },
|
|
114
|
+
};
|
|
115
|
+
expect(change.type).toBe('fingerprint');
|
|
116
|
+
expect(change.severity).toBe('high');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should accept all change types', () => {
|
|
120
|
+
const types: StructureChange['type'][] = ['fingerprint', 'structure', 'semantic', 'component'];
|
|
121
|
+
types.forEach(type => {
|
|
122
|
+
const change: StructureChange = { type, severity: 'low', description: 'test' };
|
|
123
|
+
expect(change.type).toBe(type);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('SnapshotPayload field renames', () => {
|
|
129
|
+
test('should use "structure" field (was "dom")', () => {
|
|
130
|
+
const payload: Partial<SnapshotPayload> = {
|
|
131
|
+
structure: { html: '<div/>' },
|
|
132
|
+
snapshotName: 'test',
|
|
133
|
+
testName: 'test',
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
};
|
|
136
|
+
expect(payload.structure!.html).toBe('<div/>');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should use "styles" field (was "css")', () => {
|
|
140
|
+
const payload: Partial<SnapshotPayload> = {
|
|
141
|
+
styles: { computed_styles: { 'div': { color: 'red' } } },
|
|
142
|
+
};
|
|
143
|
+
expect(payload.styles!.computed_styles['div'].color).toBe('red');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should use "structureAnalysis" field (was "domAnalysis")', () => {
|
|
147
|
+
const payload: Partial<SnapshotPayload> = {
|
|
148
|
+
structureAnalysis: { fingerprint: 'test_hash' },
|
|
149
|
+
};
|
|
150
|
+
expect(payload.structureAnalysis!.fingerprint).toBe('test_hash');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('TestivAIConfig field renames', () => {
|
|
155
|
+
test('should use "structure" field (was "dom")', () => {
|
|
156
|
+
const config: TestivAIConfig = {
|
|
157
|
+
structure: {
|
|
158
|
+
enableFingerprint: true,
|
|
159
|
+
enableStructure: false,
|
|
160
|
+
},
|
|
161
|
+
layout: { sensitivity: 2 },
|
|
162
|
+
};
|
|
163
|
+
expect(config.structure!.enableFingerprint).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('TestivAIProjectConfig field renames', () => {
|
|
168
|
+
test('should use "structure" field (was "dom")', () => {
|
|
169
|
+
const config: TestivAIProjectConfig = {
|
|
170
|
+
layout: { sensitivity: 2, tolerance: 1 },
|
|
171
|
+
ai: { sensitivity: 2, confidence: 0.7 },
|
|
172
|
+
structure: {
|
|
173
|
+
enableFingerprint: true,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
expect(config.structure!.enableFingerprint).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ci_1 = require("../../ci");
|
|
4
|
+
describe('CI Environment Detection', () => {
|
|
5
|
+
const originalEnv = process.env;
|
|
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
|
+
afterAll(() => {
|
|
34
|
+
process.env = originalEnv;
|
|
35
|
+
});
|
|
36
|
+
describe('getCiRunId', () => {
|
|
37
|
+
it('should return null when not in CI', () => {
|
|
38
|
+
expect((0, ci_1.getCiRunId)()).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
it('should detect GitHub Actions', () => {
|
|
41
|
+
process.env.GITHUB_RUN_ID = '123456';
|
|
42
|
+
expect((0, ci_1.getCiRunId)()).toBe('github-123456');
|
|
43
|
+
});
|
|
44
|
+
it('should detect GitLab CI', () => {
|
|
45
|
+
process.env.GITLAB_CI = 'true';
|
|
46
|
+
process.env.CI_PIPELINE_ID = '789';
|
|
47
|
+
expect((0, ci_1.getCiRunId)()).toBe('gitlab-789');
|
|
48
|
+
});
|
|
49
|
+
it('should detect Jenkins', () => {
|
|
50
|
+
process.env.JENKINS_URL = 'https://jenkins.example.com';
|
|
51
|
+
process.env.BUILD_ID = '42';
|
|
52
|
+
expect((0, ci_1.getCiRunId)()).toBe('jenkins-42');
|
|
53
|
+
});
|
|
54
|
+
it('should detect CircleCI', () => {
|
|
55
|
+
process.env.CIRCLECI = 'true';
|
|
56
|
+
process.env.CIRCLE_WORKFLOW_ID = 'wf-abc';
|
|
57
|
+
expect((0, ci_1.getCiRunId)()).toBe('circleci-wf-abc');
|
|
58
|
+
});
|
|
59
|
+
it('should detect Travis CI', () => {
|
|
60
|
+
process.env.TRAVIS = 'true';
|
|
61
|
+
process.env.TRAVIS_BUILD_ID = '999';
|
|
62
|
+
expect((0, ci_1.getCiRunId)()).toBe('travis-999');
|
|
63
|
+
});
|
|
64
|
+
it('should prioritize GitHub Actions over other providers', () => {
|
|
65
|
+
process.env.GITHUB_RUN_ID = '111';
|
|
66
|
+
process.env.GITLAB_CI = 'true';
|
|
67
|
+
process.env.CI_PIPELINE_ID = '222';
|
|
68
|
+
expect((0, ci_1.getCiRunId)()).toBe('github-111');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('getCiInfo', () => {
|
|
72
|
+
it('should return null when not in CI', () => {
|
|
73
|
+
expect((0, ci_1.getCiInfo)()).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
describe('GitHub Actions', () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
process.env.GITHUB_ACTIONS = 'true';
|
|
78
|
+
process.env.GITHUB_RUN_ID = '12345';
|
|
79
|
+
process.env.GITHUB_REPOSITORY = 'owner/repo';
|
|
80
|
+
process.env.GITHUB_SERVER_URL = 'https://github.com';
|
|
81
|
+
});
|
|
82
|
+
it('should return basic GitHub Actions info', () => {
|
|
83
|
+
const info = (0, ci_1.getCiInfo)();
|
|
84
|
+
expect(info).toEqual({
|
|
85
|
+
provider: 'github_actions',
|
|
86
|
+
prNumber: undefined,
|
|
87
|
+
runUrl: 'https://github.com/owner/repo/actions/runs/12345',
|
|
88
|
+
buildId: '12345',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
it('should detect PR number from GITHUB_EVENT_NUMBER', () => {
|
|
92
|
+
process.env.GITHUB_EVENT_NUMBER = '42';
|
|
93
|
+
const info = (0, ci_1.getCiInfo)();
|
|
94
|
+
expect(info?.prNumber).toBe(42);
|
|
95
|
+
});
|
|
96
|
+
it('should detect PR number from GITHUB_REF', () => {
|
|
97
|
+
process.env.GITHUB_REF = 'refs/pull/99/merge';
|
|
98
|
+
const info = (0, ci_1.getCiInfo)();
|
|
99
|
+
expect(info?.prNumber).toBe(99);
|
|
100
|
+
});
|
|
101
|
+
it('should not set PR number for non-PR refs', () => {
|
|
102
|
+
process.env.GITHUB_REF = 'refs/heads/main';
|
|
103
|
+
const info = (0, ci_1.getCiInfo)();
|
|
104
|
+
expect(info?.prNumber).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
it('should use default server URL', () => {
|
|
107
|
+
delete process.env.GITHUB_SERVER_URL;
|
|
108
|
+
const info = (0, ci_1.getCiInfo)();
|
|
109
|
+
expect(info?.runUrl).toBe('https://github.com/owner/repo/actions/runs/12345');
|
|
110
|
+
});
|
|
111
|
+
it('should handle missing repository', () => {
|
|
112
|
+
delete process.env.GITHUB_REPOSITORY;
|
|
113
|
+
const info = (0, ci_1.getCiInfo)();
|
|
114
|
+
expect(info?.runUrl).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('GitLab CI', () => {
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
process.env.GITLAB_CI = 'true';
|
|
120
|
+
process.env.CI_PIPELINE_ID = '456';
|
|
121
|
+
process.env.CI_PIPELINE_URL = 'https://gitlab.com/project/-/pipelines/456';
|
|
122
|
+
});
|
|
123
|
+
it('should return basic GitLab CI info', () => {
|
|
124
|
+
const info = (0, ci_1.getCiInfo)();
|
|
125
|
+
expect(info).toEqual({
|
|
126
|
+
provider: 'gitlab_ci',
|
|
127
|
+
prNumber: undefined,
|
|
128
|
+
runUrl: 'https://gitlab.com/project/-/pipelines/456',
|
|
129
|
+
buildId: '456',
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
it('should detect MR number', () => {
|
|
133
|
+
process.env.CI_MERGE_REQUEST_IID = '7';
|
|
134
|
+
const info = (0, ci_1.getCiInfo)();
|
|
135
|
+
expect(info?.prNumber).toBe(7);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('CircleCI', () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
process.env.CIRCLECI = 'true';
|
|
141
|
+
process.env.CIRCLE_WORKFLOW_ID = 'wf-xyz';
|
|
142
|
+
process.env.CIRCLE_BUILD_URL = 'https://circleci.com/gh/owner/repo/123';
|
|
143
|
+
});
|
|
144
|
+
it('should return basic CircleCI info', () => {
|
|
145
|
+
const info = (0, ci_1.getCiInfo)();
|
|
146
|
+
expect(info).toEqual({
|
|
147
|
+
provider: 'circleci',
|
|
148
|
+
prNumber: undefined,
|
|
149
|
+
runUrl: 'https://circleci.com/gh/owner/repo/123',
|
|
150
|
+
buildId: 'wf-xyz',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
it('should detect PR number from pull request URL', () => {
|
|
154
|
+
process.env.CIRCLE_PULL_REQUEST = 'https://github.com/owner/repo/pull/55';
|
|
155
|
+
const info = (0, ci_1.getCiInfo)();
|
|
156
|
+
expect(info?.prNumber).toBe(55);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('Jenkins', () => {
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
process.env.JENKINS_URL = 'https://jenkins.example.com';
|
|
162
|
+
process.env.BUILD_ID = '100';
|
|
163
|
+
process.env.BUILD_URL = 'https://jenkins.example.com/job/my-job/100/';
|
|
164
|
+
});
|
|
165
|
+
it('should return basic Jenkins info', () => {
|
|
166
|
+
const info = (0, ci_1.getCiInfo)();
|
|
167
|
+
expect(info).toEqual({
|
|
168
|
+
provider: 'jenkins',
|
|
169
|
+
prNumber: undefined,
|
|
170
|
+
runUrl: 'https://jenkins.example.com/job/my-job/100/',
|
|
171
|
+
buildId: '100',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
it('should detect PR number from CHANGE_ID', () => {
|
|
175
|
+
process.env.CHANGE_ID = '33';
|
|
176
|
+
const info = (0, ci_1.getCiInfo)();
|
|
177
|
+
expect(info?.prNumber).toBe(33);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('Travis CI', () => {
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
process.env.TRAVIS = 'true';
|
|
183
|
+
process.env.TRAVIS_BUILD_ID = '888';
|
|
184
|
+
process.env.TRAVIS_BUILD_WEB_URL = 'https://app.travis-ci.com/owner/repo/builds/888';
|
|
185
|
+
});
|
|
186
|
+
it('should return basic Travis CI info', () => {
|
|
187
|
+
const info = (0, ci_1.getCiInfo)();
|
|
188
|
+
expect(info).toEqual({
|
|
189
|
+
provider: 'travis_ci',
|
|
190
|
+
prNumber: undefined,
|
|
191
|
+
runUrl: 'https://app.travis-ci.com/owner/repo/builds/888',
|
|
192
|
+
buildId: '888',
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
it('should detect PR number', () => {
|
|
196
|
+
process.env.TRAVIS_PULL_REQUEST = '17';
|
|
197
|
+
const info = (0, ci_1.getCiInfo)();
|
|
198
|
+
expect(info?.prNumber).toBe(17);
|
|
199
|
+
});
|
|
200
|
+
it('should not set PR number when TRAVIS_PULL_REQUEST is false', () => {
|
|
201
|
+
process.env.TRAVIS_PULL_REQUEST = 'false';
|
|
202
|
+
const info = (0, ci_1.getCiInfo)();
|
|
203
|
+
expect(info?.prNumber).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('Payload shape matches Core API CiInfo schema', () => {
|
|
207
|
+
it('should produce fields matching Core API aliases: provider, prNumber, runUrl, buildId', () => {
|
|
208
|
+
process.env.GITHUB_ACTIONS = 'true';
|
|
209
|
+
process.env.GITHUB_RUN_ID = '1';
|
|
210
|
+
process.env.GITHUB_REPOSITORY = 'o/r';
|
|
211
|
+
process.env.GITHUB_EVENT_NUMBER = '10';
|
|
212
|
+
const info = (0, ci_1.getCiInfo)();
|
|
213
|
+
expect(info).toBeDefined();
|
|
214
|
+
// These are the exact camelCase keys the Core API CiInfo Pydantic model expects via aliases
|
|
215
|
+
expect(info).toHaveProperty('provider');
|
|
216
|
+
expect(info).toHaveProperty('prNumber');
|
|
217
|
+
expect(info).toHaveProperty('runUrl');
|
|
218
|
+
expect(info).toHaveProperty('buildId');
|
|
219
|
+
// Ensure no unexpected snake_case keys
|
|
220
|
+
expect(info).not.toHaveProperty('pr_number');
|
|
221
|
+
expect(info).not.toHaveProperty('run_url');
|
|
222
|
+
expect(info).not.toHaveProperty('build_id');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for Playwright SDK compression integration
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const reporter_1 = require("../../reporter");
|
|
7
|
+
const common_1 = require("@testivai/common");
|
|
8
|
+
describe('Playwright Reporter Compression', () => {
|
|
9
|
+
let reporter;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
reporter = new reporter_1.TestivAIPlaywrightReporter({
|
|
12
|
+
apiKey: 'test-key',
|
|
13
|
+
apiUrl: 'http://localhost:3000',
|
|
14
|
+
compression: {
|
|
15
|
+
compressUploads: true,
|
|
16
|
+
compressionThreshold: 100, // Small threshold for testing
|
|
17
|
+
compressionLevel: 6,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('should initialize with compression helper', () => {
|
|
22
|
+
expect(reporter).toBeDefined();
|
|
23
|
+
// Access private property through type assertion for testing
|
|
24
|
+
const helper = reporter.compressionHelper;
|
|
25
|
+
expect(helper).toBeInstanceOf(common_1.CompressionHelper);
|
|
26
|
+
});
|
|
27
|
+
it('should use custom compression options', () => {
|
|
28
|
+
const customReporter = new reporter_1.TestivAIPlaywrightReporter({
|
|
29
|
+
apiKey: 'test-key',
|
|
30
|
+
compression: {
|
|
31
|
+
compressUploads: false,
|
|
32
|
+
compressionLevel: 9,
|
|
33
|
+
compressionThreshold: 1000,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const helper = customReporter.compressionHelper;
|
|
37
|
+
expect(helper).toBeInstanceOf(common_1.CompressionHelper);
|
|
38
|
+
});
|
|
39
|
+
it('should use default compression options when none provided', () => {
|
|
40
|
+
const defaultReporter = new reporter_1.TestivAIPlaywrightReporter({
|
|
41
|
+
apiKey: 'test-key',
|
|
42
|
+
});
|
|
43
|
+
const helper = defaultReporter.compressionHelper;
|
|
44
|
+
expect(helper).toBeInstanceOf(common_1.CompressionHelper);
|
|
45
|
+
});
|
|
46
|
+
});
|
package/dist/ci.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for detecting CI/CD environments and extracting a unique run identifier.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* CI environment information for integration feedback (e.g., GitHub commit statuses, PR comments).
|
|
6
|
+
*/
|
|
7
|
+
export interface CiInfo {
|
|
8
|
+
/** CI provider name */
|
|
9
|
+
provider: string;
|
|
10
|
+
/** Pull/Merge request number (if available) */
|
|
11
|
+
prNumber?: number;
|
|
12
|
+
/** URL to the CI run (if available) */
|
|
13
|
+
runUrl?: string;
|
|
14
|
+
/** CI build/run identifier */
|
|
15
|
+
buildId?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Gets a unique identifier for the current CI run.
|
|
19
|
+
* This ID is used to group snapshots from parallel shards into a single batch.
|
|
20
|
+
*
|
|
21
|
+
* @returns A unique string identifier for the CI run, or null if not in a known CI environment.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getCiRunId(): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Gets detailed CI environment information including PR number and run URL.
|
|
26
|
+
* Used for integration feedback (GitHub commit statuses, PR comments, etc.).
|
|
27
|
+
*
|
|
28
|
+
* @returns CiInfo object if in a known CI environment, or null otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getCiInfo(): CiInfo | null;
|
package/dist/ci.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
exports.getCiInfo = getCiInfo;
|
|
8
|
+
/**
|
|
9
|
+
* Gets a unique identifier for the current CI run.
|
|
10
|
+
* This ID is used to group snapshots from parallel shards into a single batch.
|
|
11
|
+
*
|
|
12
|
+
* @returns A unique string identifier for the CI run, or null if not in a known CI environment.
|
|
13
|
+
*/
|
|
14
|
+
function getCiRunId() {
|
|
15
|
+
// GitHub Actions
|
|
16
|
+
if (process.env.GITHUB_RUN_ID) {
|
|
17
|
+
return `github-${process.env.GITHUB_RUN_ID}`;
|
|
18
|
+
}
|
|
19
|
+
// GitLab CI
|
|
20
|
+
if (process.env.GITLAB_CI) {
|
|
21
|
+
return `gitlab-${process.env.CI_PIPELINE_ID}`;
|
|
22
|
+
}
|
|
23
|
+
// Jenkins
|
|
24
|
+
if (process.env.JENKINS_URL) {
|
|
25
|
+
return `jenkins-${process.env.BUILD_ID}`;
|
|
26
|
+
}
|
|
27
|
+
// CircleCI
|
|
28
|
+
if (process.env.CIRCLECI) {
|
|
29
|
+
return `circleci-${process.env.CIRCLE_WORKFLOW_ID}`;
|
|
30
|
+
}
|
|
31
|
+
// Travis CI
|
|
32
|
+
if (process.env.TRAVIS) {
|
|
33
|
+
return `travis-${process.env.TRAVIS_BUILD_ID}`;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Gets detailed CI environment information including PR number and run URL.
|
|
39
|
+
* Used for integration feedback (GitHub commit statuses, PR comments, etc.).
|
|
40
|
+
*
|
|
41
|
+
* @returns CiInfo object if in a known CI environment, or null otherwise.
|
|
42
|
+
*/
|
|
43
|
+
function getCiInfo() {
|
|
44
|
+
// GitHub Actions
|
|
45
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
46
|
+
const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
|
|
47
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
48
|
+
const runId = process.env.GITHUB_RUN_ID;
|
|
49
|
+
// PR number: available via GITHUB_EVENT_NUMBER in pull_request events,
|
|
50
|
+
// or from GITHUB_REF (refs/pull/<number>/merge)
|
|
51
|
+
let prNumber;
|
|
52
|
+
if (process.env.GITHUB_EVENT_NUMBER) {
|
|
53
|
+
prNumber = parseInt(process.env.GITHUB_EVENT_NUMBER, 10) || undefined;
|
|
54
|
+
}
|
|
55
|
+
else if (process.env.GITHUB_REF?.startsWith('refs/pull/')) {
|
|
56
|
+
const match = process.env.GITHUB_REF.match(/^refs\/pull\/(\d+)\//);
|
|
57
|
+
if (match) {
|
|
58
|
+
prNumber = parseInt(match[1], 10);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
provider: 'github_actions',
|
|
63
|
+
prNumber,
|
|
64
|
+
runUrl: repo && runId ? `${serverUrl}/${repo}/actions/runs/${runId}` : undefined,
|
|
65
|
+
buildId: runId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// GitLab CI
|
|
69
|
+
if (process.env.GITLAB_CI) {
|
|
70
|
+
const prNumber = process.env.CI_MERGE_REQUEST_IID
|
|
71
|
+
? parseInt(process.env.CI_MERGE_REQUEST_IID, 10) || undefined
|
|
72
|
+
: undefined;
|
|
73
|
+
return {
|
|
74
|
+
provider: 'gitlab_ci',
|
|
75
|
+
prNumber,
|
|
76
|
+
runUrl: process.env.CI_PIPELINE_URL || undefined,
|
|
77
|
+
buildId: process.env.CI_PIPELINE_ID,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// CircleCI
|
|
81
|
+
if (process.env.CIRCLECI) {
|
|
82
|
+
const prNumber = process.env.CIRCLE_PULL_REQUEST
|
|
83
|
+
? parseInt(process.env.CIRCLE_PULL_REQUEST.split('/').pop() || '', 10) || undefined
|
|
84
|
+
: undefined;
|
|
85
|
+
return {
|
|
86
|
+
provider: 'circleci',
|
|
87
|
+
prNumber,
|
|
88
|
+
runUrl: process.env.CIRCLE_BUILD_URL || undefined,
|
|
89
|
+
buildId: process.env.CIRCLE_WORKFLOW_ID,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Jenkins
|
|
93
|
+
if (process.env.JENKINS_URL) {
|
|
94
|
+
const prNumber = process.env.CHANGE_ID
|
|
95
|
+
? parseInt(process.env.CHANGE_ID, 10) || undefined
|
|
96
|
+
: undefined;
|
|
97
|
+
return {
|
|
98
|
+
provider: 'jenkins',
|
|
99
|
+
prNumber,
|
|
100
|
+
runUrl: process.env.BUILD_URL || undefined,
|
|
101
|
+
buildId: process.env.BUILD_ID,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Travis CI
|
|
105
|
+
if (process.env.TRAVIS) {
|
|
106
|
+
const prNumber = process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== 'false'
|
|
107
|
+
? parseInt(process.env.TRAVIS_PULL_REQUEST, 10) || undefined
|
|
108
|
+
: undefined;
|
|
109
|
+
return {
|
|
110
|
+
provider: 'travis_ci',
|
|
111
|
+
prNumber,
|
|
112
|
+
runUrl: process.env.TRAVIS_BUILD_WEB_URL || undefined,
|
|
113
|
+
buildId: process.env.TRAVIS_BUILD_ID,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const init_1 = require("./init");
|
|
10
|
+
const program = new commander_1.Command();
|
|
11
|
+
// Display banner
|
|
12
|
+
const showBanner = () => {
|
|
13
|
+
console.log();
|
|
14
|
+
console.log(chalk_1.default.cyan.bold(' TestivAI'));
|
|
15
|
+
console.log(chalk_1.default.gray(' Catch Visual Bugs Automatically'));
|
|
16
|
+
console.log(chalk_1.default.gray(' AI that catches real bugs, ignores the noise.'));
|
|
17
|
+
console.log();
|
|
18
|
+
};
|
|
19
|
+
program
|
|
20
|
+
.name('testivai')
|
|
21
|
+
.description('TestivAI Playwright SDK CLI')
|
|
22
|
+
.version(require('../../package.json').version)
|
|
23
|
+
.hook('preAction', () => {
|
|
24
|
+
if (!process.argv.includes('--quiet') && !process.argv.includes('-q')) {
|
|
25
|
+
showBanner();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// Global options
|
|
29
|
+
program
|
|
30
|
+
.option('-q, --quiet', 'Suppress banner output (ideal for CI)');
|
|
31
|
+
program
|
|
32
|
+
.command('init')
|
|
33
|
+
.description('Initialize TestivAI configuration file')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
try {
|
|
36
|
+
await (0, init_1.createConfigFile)();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error('❌ Failed to initialize:', error);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
program.parse(process.argv);
|
|
44
|
+
// Show help if no command provided
|
|
45
|
+
if (!process.argv.slice(2).length) {
|
|
46
|
+
program.outputHelp();
|
|
47
|
+
}
|