@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.
Files changed (61) hide show
  1. package/__tests__/.gitkeep +0 -0
  2. package/__tests__/config-integration.spec.ts +102 -0
  3. package/__tests__/snapshot.spec.d.ts +1 -0
  4. package/__tests__/snapshot.spec.js +81 -0
  5. package/__tests__/snapshot.spec.ts +58 -0
  6. package/__tests__/unit/ci.spec.d.ts +1 -0
  7. package/__tests__/unit/ci.spec.js +35 -0
  8. package/__tests__/unit/ci.spec.ts +40 -0
  9. package/__tests__/unit/reporter.spec.d.ts +1 -0
  10. package/__tests__/unit/reporter.spec.js +37 -0
  11. package/__tests__/unit/reporter.spec.ts +43 -0
  12. package/__tests__/unit/structureAnalyzer.spec.js +212 -0
  13. package/__tests__/unit/types.spec.ts +179 -0
  14. package/dist/__tests__/unit/ci.spec.d.ts +1 -0
  15. package/dist/__tests__/unit/ci.spec.js +226 -0
  16. package/dist/__tests__/unit/compression.spec.d.ts +4 -0
  17. package/dist/__tests__/unit/compression.spec.js +46 -0
  18. package/dist/ci.d.ts +30 -0
  19. package/dist/ci.js +117 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.js +47 -0
  22. package/dist/cli/init.d.ts +3 -0
  23. package/dist/cli/init.js +158 -0
  24. package/dist/config/loader.d.ts +29 -0
  25. package/dist/config/loader.js +251 -0
  26. package/dist/domAnalyzer.d.ts +10 -0
  27. package/dist/domAnalyzer.js +285 -0
  28. package/dist/index.d.ts +7 -0
  29. package/dist/index.js +11 -0
  30. package/dist/reporter-entry.d.ts +2 -0
  31. package/dist/reporter-entry.js +5 -0
  32. package/dist/reporter-types.d.ts +2 -0
  33. package/dist/reporter-types.js +2 -0
  34. package/dist/reporter.d.ts +21 -0
  35. package/dist/reporter.js +249 -0
  36. package/dist/snapshot.d.ts +12 -0
  37. package/dist/snapshot.js +601 -0
  38. package/dist/structureAnalyzer.d.ts +12 -0
  39. package/dist/structureAnalyzer.js +288 -0
  40. package/dist/types.d.ts +368 -0
  41. package/dist/types.js +10 -0
  42. package/examples/structure-analysis-example.spec.ts +118 -0
  43. package/examples/structure-analysis.config.ts +159 -0
  44. package/jest.config.js +8 -0
  45. package/package.json +51 -0
  46. package/playwright.config.ts +11 -0
  47. package/src/__tests__/unit/ci.spec.ts +257 -0
  48. package/src/__tests__/unit/compression.spec.ts +52 -0
  49. package/src/ci.ts +140 -0
  50. package/src/cli/index.ts +49 -0
  51. package/src/cli/init.ts +131 -0
  52. package/src/config/loader.ts +238 -0
  53. package/src/index.ts +14 -0
  54. package/src/reporter-entry.ts +6 -0
  55. package/src/reporter-types.ts +5 -0
  56. package/src/reporter.ts +251 -0
  57. package/src/snapshot.ts +632 -0
  58. package/src/structureAnalyzer.ts +338 -0
  59. package/src/types.ts +388 -0
  60. package/tsconfig.jest.json +7 -0
  61. 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
@@ -0,0 +1,8 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ transform: {
4
+ '^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }],
5
+ },
6
+ testEnvironment: 'node',
7
+ testMatch: ['**/__tests__/unit/**/*.spec.{ts,js}'],
8
+ };
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,11 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ export default defineConfig({
4
+ testDir: './__tests__',
5
+ // We don't need a reporter for this test run
6
+ reporter: 'list',
7
+ use: {
8
+ // Basic browser configuration
9
+ browserName: 'chromium',
10
+ },
11
+ });
@@ -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
+ });