@vizzly-testing/cli 0.29.6 → 0.31.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 +34 -6
- package/dist/api/client.js +6 -3
- package/dist/api/endpoints.js +59 -0
- package/dist/api/index.js +1 -1
- package/dist/auth/core.js +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/cli.js +206 -51
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +870 -0
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +18 -7
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +64 -26
- package/dist/config/core.js +37 -36
- package/dist/context/local-workspace-provider.js +532 -0
- package/dist/context/provider-resolver.js +36 -0
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +2 -1
- package/dist/reporter/reporter-bundle.iife.js +119 -119
- package/dist/reporter-ssr/ssr-entry.js +605 -509
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/assets.js +4 -0
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +226 -148
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/core/hotspot-coverage.js +1 -1
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/hotspot-service.js +1 -14
- package/dist/tdd/tdd-service.js +82 -113
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +47 -7
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/colors.js +46 -44
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +22 -3
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/report-data.js +31 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/dist/utils/security.js +3 -2
- package/package.json +14 -11
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
|
@@ -6,6 +6,169 @@
|
|
|
6
6
|
import * as output from '../../utils/output.js';
|
|
7
7
|
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
8
8
|
import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
|
|
9
|
+
function sendBaselineUnavailable(res) {
|
|
10
|
+
sendError(res, 400, 'Baseline management not available');
|
|
11
|
+
}
|
|
12
|
+
async function parseRequiredComparisonId(req, res) {
|
|
13
|
+
let {
|
|
14
|
+
id
|
|
15
|
+
} = await parseJsonBody(req);
|
|
16
|
+
if (!id) {
|
|
17
|
+
sendError(res, 400, 'Comparison ID required');
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
async function handleComparisonAction({
|
|
23
|
+
req,
|
|
24
|
+
res,
|
|
25
|
+
screenshotHandler,
|
|
26
|
+
actionName,
|
|
27
|
+
action,
|
|
28
|
+
successMessage
|
|
29
|
+
}) {
|
|
30
|
+
if (!screenshotHandler?.[actionName]) {
|
|
31
|
+
sendBaselineUnavailable(res);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
let id = await parseRequiredComparisonId(req, res);
|
|
36
|
+
if (!id) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
await screenshotHandler[actionName](id);
|
|
40
|
+
sendSuccess(res, {
|
|
41
|
+
success: true,
|
|
42
|
+
message: successMessage(id)
|
|
43
|
+
});
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === 'NOT_FOUND') {
|
|
47
|
+
sendError(res, 404, error.message);
|
|
48
|
+
} else {
|
|
49
|
+
output.error(`Error ${action}:`, error);
|
|
50
|
+
sendError(res, 500, error.message);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function handleAcceptAllBaselines(res, screenshotHandler) {
|
|
56
|
+
if (!screenshotHandler?.acceptAllBaselines) {
|
|
57
|
+
sendBaselineUnavailable(res);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
let result = await screenshotHandler.acceptAllBaselines();
|
|
62
|
+
sendSuccess(res, {
|
|
63
|
+
success: true,
|
|
64
|
+
message: `Accepted ${result.count} baselines`,
|
|
65
|
+
count: result.count
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
output.error('Error accepting all baselines:', error);
|
|
70
|
+
sendError(res, 500, error.message);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function handleResetBaselines(res, screenshotHandler) {
|
|
75
|
+
if (!screenshotHandler?.resetBaselines) {
|
|
76
|
+
sendBaselineUnavailable(res);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await screenshotHandler.resetBaselines();
|
|
81
|
+
sendSuccess(res, {
|
|
82
|
+
success: true,
|
|
83
|
+
message: 'Baselines reset to previous state'
|
|
84
|
+
});
|
|
85
|
+
return true;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
output.error('Error resetting baselines:', error);
|
|
88
|
+
sendError(res, 500, error.message);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function shouldFallbackFromOAuth(error) {
|
|
93
|
+
return error.message?.includes('Not authenticated') || error.message?.includes('401');
|
|
94
|
+
}
|
|
95
|
+
async function downloadBaselinesWithOAuth({
|
|
96
|
+
authService,
|
|
97
|
+
tddService,
|
|
98
|
+
buildId,
|
|
99
|
+
organizationSlug,
|
|
100
|
+
projectSlug
|
|
101
|
+
}) {
|
|
102
|
+
output.info(`Downloading baselines from build ${buildId}...`);
|
|
103
|
+
output.debug('baseline', `Using OAuth for ${organizationSlug}/${projectSlug}`);
|
|
104
|
+
let apiResponse = await authService.authenticatedRequest(`/api/cli/${projectSlug}/builds/${buildId}/tdd-baselines`, {
|
|
105
|
+
method: 'GET',
|
|
106
|
+
headers: {
|
|
107
|
+
'X-Organization': organizationSlug
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (!apiResponse) {
|
|
111
|
+
throw new Error(`Build ${buildId} not found or API returned null`);
|
|
112
|
+
}
|
|
113
|
+
return tddService.processDownloadedBaselines(apiResponse, buildId);
|
|
114
|
+
}
|
|
115
|
+
async function downloadBaselinesWithToken(tddService, buildId) {
|
|
116
|
+
output.info(`Downloading baselines from build ${buildId}...`);
|
|
117
|
+
return tddService.downloadBaselines('test', null, buildId, null);
|
|
118
|
+
}
|
|
119
|
+
async function handleBaselineDownload({
|
|
120
|
+
req,
|
|
121
|
+
res,
|
|
122
|
+
tddService,
|
|
123
|
+
authService
|
|
124
|
+
}) {
|
|
125
|
+
if (!tddService) {
|
|
126
|
+
sendServiceUnavailable(res, 'TDD service not available (only available in TDD mode)');
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
let body = await parseJsonBody(req);
|
|
131
|
+
let {
|
|
132
|
+
buildId,
|
|
133
|
+
organizationSlug,
|
|
134
|
+
projectSlug
|
|
135
|
+
} = body;
|
|
136
|
+
if (!buildId) {
|
|
137
|
+
sendError(res, 400, 'buildId is required');
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
let result;
|
|
141
|
+
if (authService && organizationSlug && projectSlug) {
|
|
142
|
+
try {
|
|
143
|
+
result = await downloadBaselinesWithOAuth({
|
|
144
|
+
authService,
|
|
145
|
+
tddService,
|
|
146
|
+
buildId,
|
|
147
|
+
organizationSlug,
|
|
148
|
+
projectSlug
|
|
149
|
+
});
|
|
150
|
+
} catch (oauthError) {
|
|
151
|
+
if (!shouldFallbackFromOAuth(oauthError)) {
|
|
152
|
+
throw oauthError;
|
|
153
|
+
}
|
|
154
|
+
output.debug('baseline', `OAuth failed, trying other auth methods: ${oauthError.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!result) {
|
|
158
|
+
result = await downloadBaselinesWithToken(tddService, buildId);
|
|
159
|
+
}
|
|
160
|
+
sendSuccess(res, {
|
|
161
|
+
success: true,
|
|
162
|
+
message: `Baselines downloaded from build ${buildId}`,
|
|
163
|
+
...result
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
output.error('Error downloading baselines:', error);
|
|
168
|
+
sendError(res, 500, error.message);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
9
172
|
|
|
10
173
|
/**
|
|
11
174
|
* Create baseline router
|
|
@@ -23,204 +186,58 @@ export function createBaselineRouter({
|
|
|
23
186
|
return async function handleBaselineRoute(req, res, pathname) {
|
|
24
187
|
// Accept a single screenshot as baseline
|
|
25
188
|
if (req.method === 'POST' && pathname === '/api/baseline/accept') {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!id) {
|
|
35
|
-
sendError(res, 400, 'Comparison ID required');
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
await screenshotHandler.acceptBaseline(id);
|
|
39
|
-
sendSuccess(res, {
|
|
40
|
-
success: true,
|
|
41
|
-
message: `Baseline accepted for comparison ${id}`
|
|
42
|
-
});
|
|
43
|
-
return true;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
output.error('Error accepting baseline:', error);
|
|
46
|
-
sendError(res, 500, error.message);
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
189
|
+
return handleComparisonAction({
|
|
190
|
+
req,
|
|
191
|
+
res,
|
|
192
|
+
screenshotHandler,
|
|
193
|
+
actionName: 'acceptBaseline',
|
|
194
|
+
action: 'accepting baseline',
|
|
195
|
+
successMessage: id => `Baseline accepted for comparison ${id}`
|
|
196
|
+
});
|
|
49
197
|
}
|
|
50
198
|
|
|
51
199
|
// Accept all screenshots as baseline
|
|
52
200
|
if (req.method === 'POST' && pathname === '/api/baseline/accept-all') {
|
|
53
|
-
|
|
54
|
-
sendError(res, 400, 'Baseline management not available');
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
const result = await screenshotHandler.acceptAllBaselines();
|
|
59
|
-
sendSuccess(res, {
|
|
60
|
-
success: true,
|
|
61
|
-
message: `Accepted ${result.count} baselines`,
|
|
62
|
-
count: result.count
|
|
63
|
-
});
|
|
64
|
-
return true;
|
|
65
|
-
} catch (error) {
|
|
66
|
-
output.error('Error accepting all baselines:', error);
|
|
67
|
-
sendError(res, 500, error.message);
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
201
|
+
return handleAcceptAllBaselines(res, screenshotHandler);
|
|
70
202
|
}
|
|
71
203
|
|
|
72
204
|
// Reject a single comparison (keep current baseline, discard changes)
|
|
73
205
|
if (req.method === 'POST' && pathname === '/api/baseline/reject') {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (!id) {
|
|
83
|
-
sendError(res, 400, 'Comparison ID required');
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
await screenshotHandler.rejectBaseline(id);
|
|
87
|
-
sendSuccess(res, {
|
|
88
|
-
success: true,
|
|
89
|
-
message: `Changes rejected for comparison ${id}`
|
|
90
|
-
});
|
|
91
|
-
return true;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
output.error('Error rejecting baseline:', error);
|
|
94
|
-
sendError(res, 500, error.message);
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
206
|
+
return handleComparisonAction({
|
|
207
|
+
req,
|
|
208
|
+
res,
|
|
209
|
+
screenshotHandler,
|
|
210
|
+
actionName: 'rejectBaseline',
|
|
211
|
+
action: 'rejecting baseline',
|
|
212
|
+
successMessage: id => `Changes rejected for comparison ${id}`
|
|
213
|
+
});
|
|
97
214
|
}
|
|
98
215
|
|
|
99
216
|
// Delete a comparison entirely (removes from report and deletes files)
|
|
100
217
|
if (req.method === 'POST' && pathname === '/api/baseline/delete') {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!id) {
|
|
110
|
-
sendError(res, 400, 'Comparison ID required');
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
await screenshotHandler.deleteComparison(id);
|
|
114
|
-
sendSuccess(res, {
|
|
115
|
-
success: true,
|
|
116
|
-
message: `Comparison ${id} deleted`
|
|
117
|
-
});
|
|
118
|
-
return true;
|
|
119
|
-
} catch (error) {
|
|
120
|
-
if (error.code === 'NOT_FOUND') {
|
|
121
|
-
sendError(res, 404, error.message);
|
|
122
|
-
} else {
|
|
123
|
-
output.error('Error deleting comparison:', error);
|
|
124
|
-
sendError(res, 500, error.message);
|
|
125
|
-
}
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
218
|
+
return handleComparisonAction({
|
|
219
|
+
req,
|
|
220
|
+
res,
|
|
221
|
+
screenshotHandler,
|
|
222
|
+
actionName: 'deleteComparison',
|
|
223
|
+
action: 'deleting comparison',
|
|
224
|
+
successMessage: id => `Comparison ${id} deleted`
|
|
225
|
+
});
|
|
128
226
|
}
|
|
129
227
|
|
|
130
228
|
// Reset baselines to previous state
|
|
131
229
|
if (req.method === 'POST' && pathname === '/api/baseline/reset') {
|
|
132
|
-
|
|
133
|
-
sendError(res, 400, 'Baseline management not available');
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
await screenshotHandler.resetBaselines();
|
|
138
|
-
sendSuccess(res, {
|
|
139
|
-
success: true,
|
|
140
|
-
message: 'Baselines reset to previous state'
|
|
141
|
-
});
|
|
142
|
-
return true;
|
|
143
|
-
} catch (error) {
|
|
144
|
-
output.error('Error resetting baselines:', error);
|
|
145
|
-
sendError(res, 500, error.message);
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
230
|
+
return handleResetBaselines(res, screenshotHandler);
|
|
148
231
|
}
|
|
149
232
|
|
|
150
233
|
// Download baselines from a remote build
|
|
151
234
|
if (req.method === 'POST' && pathname === '/api/baselines/download') {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let {
|
|
159
|
-
buildId,
|
|
160
|
-
organizationSlug,
|
|
161
|
-
projectSlug
|
|
162
|
-
} = body;
|
|
163
|
-
if (!buildId) {
|
|
164
|
-
sendError(res, 400, 'buildId is required');
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Try OAuth authentication first (allows access to any project user has membership to)
|
|
169
|
-
// This is the preferred method when user is logged in via the dashboard
|
|
170
|
-
if (authService && organizationSlug && projectSlug) {
|
|
171
|
-
try {
|
|
172
|
-
output.info(`Downloading baselines from build ${buildId}...`);
|
|
173
|
-
output.debug('baseline', `Using OAuth for ${organizationSlug}/${projectSlug}`);
|
|
174
|
-
|
|
175
|
-
// Use the CLI endpoint which accepts OAuth and checks project membership
|
|
176
|
-
let apiResponse = await authService.authenticatedRequest(`/api/cli/${projectSlug}/builds/${buildId}/tdd-baselines`, {
|
|
177
|
-
method: 'GET',
|
|
178
|
-
headers: {
|
|
179
|
-
'X-Organization': organizationSlug
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
if (!apiResponse) {
|
|
183
|
-
throw new Error(`Build ${buildId} not found or API returned null`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Process the baselines through tddService
|
|
187
|
-
let result = await tddService.processDownloadedBaselines(apiResponse, buildId);
|
|
188
|
-
sendSuccess(res, {
|
|
189
|
-
success: true,
|
|
190
|
-
message: `Baselines downloaded from build ${buildId}`,
|
|
191
|
-
...result
|
|
192
|
-
});
|
|
193
|
-
return true;
|
|
194
|
-
} catch (oauthError) {
|
|
195
|
-
// If OAuth fails with auth error, fall through to other methods
|
|
196
|
-
if (!oauthError.message?.includes('Not authenticated') && !oauthError.message?.includes('401')) {
|
|
197
|
-
throw oauthError;
|
|
198
|
-
}
|
|
199
|
-
output.debug('baseline', `OAuth failed, trying other auth methods: ${oauthError.message}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Fall back to using tddService directly (requires VIZZLY_TOKEN env var)
|
|
204
|
-
output.info(`Downloading baselines from build ${buildId}...`);
|
|
205
|
-
let result = await tddService.downloadBaselines('test',
|
|
206
|
-
// environment
|
|
207
|
-
null,
|
|
208
|
-
// branch (not needed when buildId is specified)
|
|
209
|
-
buildId,
|
|
210
|
-
// specific build to download from
|
|
211
|
-
null // comparisonId (not needed)
|
|
212
|
-
);
|
|
213
|
-
sendSuccess(res, {
|
|
214
|
-
success: true,
|
|
215
|
-
message: `Baselines downloaded from build ${buildId}`,
|
|
216
|
-
...result
|
|
217
|
-
});
|
|
218
|
-
return true;
|
|
219
|
-
} catch (error) {
|
|
220
|
-
output.error('Error downloading baselines:', error);
|
|
221
|
-
sendError(res, 500, error.message);
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
235
|
+
return handleBaselineDownload({
|
|
236
|
+
req,
|
|
237
|
+
res,
|
|
238
|
+
tddService,
|
|
239
|
+
authService
|
|
240
|
+
});
|
|
224
241
|
}
|
|
225
242
|
return false;
|
|
226
243
|
};
|