@vizzly-testing/cli 0.20.0 → 0.20.1-beta.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/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +1 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/finalize.js +41 -15
- package/dist/commands/login.js +7 -6
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +5 -4
- package/dist/commands/run.js +158 -90
- package/dist/commands/status.js +22 -18
- package/dist/commands/tdd.js +105 -78
- package/dist/commands/upload.js +61 -26
- package/dist/commands/whoami.js +4 -4
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/report-generator/core.js +315 -0
- package/dist/report-generator/index.js +8 -0
- package/dist/report-generator/operations.js +196 -0
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +80 -48
- package/dist/server-manager/core.js +183 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +208 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/static-report-generator.js +21 -163
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1081 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/package.json +7 -12
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/tdd-service.js +0 -1437
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Operations - Project operations with dependency injection
|
|
3
|
+
*
|
|
4
|
+
* Each operation takes its dependencies as parameters:
|
|
5
|
+
* - mappingStore: for reading/writing project mappings
|
|
6
|
+
* - httpClient: for making API requests (OAuth or API token based)
|
|
7
|
+
*
|
|
8
|
+
* This makes them trivially testable without mocking modules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { buildBuildsUrl, buildMappingResult, buildNoApiServiceError, buildNoAuthError, buildOrgHeader, buildProjectFetchError, buildProjectUrl, buildTokenCreateError, buildTokenRevokeError, buildTokensFetchError, buildTokensUrl, enrichProjectsWithOrg, extractBuilds, extractOrganizations, extractProject, extractProjects, extractToken, extractTokens, mappingsToArray, validateDirectory, validateProjectData } from './core.js';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Mapping Operations
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* List all project mappings
|
|
19
|
+
* @param {Object} mappingStore - Store with getMappings method
|
|
20
|
+
* @returns {Promise<Array>} Array of project mappings with directory included
|
|
21
|
+
*/
|
|
22
|
+
export async function listMappings(mappingStore) {
|
|
23
|
+
let mappings = await mappingStore.getMappings();
|
|
24
|
+
return mappingsToArray(mappings);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get project mapping for a specific directory
|
|
29
|
+
* @param {Object} mappingStore - Store with getMapping method
|
|
30
|
+
* @param {string} directory - Directory path
|
|
31
|
+
* @returns {Promise<Object|null>} Project mapping or null
|
|
32
|
+
*/
|
|
33
|
+
export async function getMapping(mappingStore, directory) {
|
|
34
|
+
return mappingStore.getMapping(directory);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create or update project mapping
|
|
39
|
+
* @param {Object} mappingStore - Store with saveMapping method
|
|
40
|
+
* @param {string} directory - Directory path
|
|
41
|
+
* @param {Object} projectData - Project data
|
|
42
|
+
* @returns {Promise<Object>} Created mapping with directory included
|
|
43
|
+
*/
|
|
44
|
+
export async function createMapping(mappingStore, directory, projectData) {
|
|
45
|
+
let dirValidation = validateDirectory(directory);
|
|
46
|
+
if (!dirValidation.valid) {
|
|
47
|
+
throw dirValidation.error;
|
|
48
|
+
}
|
|
49
|
+
let dataValidation = validateProjectData(projectData);
|
|
50
|
+
if (!dataValidation.valid) {
|
|
51
|
+
throw dataValidation.error;
|
|
52
|
+
}
|
|
53
|
+
await mappingStore.saveMapping(directory, projectData);
|
|
54
|
+
return buildMappingResult(directory, projectData);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Remove project mapping
|
|
59
|
+
* @param {Object} mappingStore - Store with deleteMapping method
|
|
60
|
+
* @param {string} directory - Directory path
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
*/
|
|
63
|
+
export async function removeMapping(mappingStore, directory) {
|
|
64
|
+
let validation = validateDirectory(directory);
|
|
65
|
+
if (!validation.valid) {
|
|
66
|
+
throw validation.error;
|
|
67
|
+
}
|
|
68
|
+
await mappingStore.deleteMapping(directory);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Switch project for a directory (convenience wrapper for createMapping)
|
|
73
|
+
* @param {Object} mappingStore - Store with saveMapping method
|
|
74
|
+
* @param {string} directory - Directory path
|
|
75
|
+
* @param {string} projectSlug - Project slug
|
|
76
|
+
* @param {string} organizationSlug - Organization slug
|
|
77
|
+
* @param {string} token - Project token
|
|
78
|
+
* @returns {Promise<Object>} Updated mapping
|
|
79
|
+
*/
|
|
80
|
+
export async function switchProject(mappingStore, directory, projectSlug, organizationSlug, token) {
|
|
81
|
+
return createMapping(mappingStore, directory, {
|
|
82
|
+
projectSlug,
|
|
83
|
+
organizationSlug,
|
|
84
|
+
token
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// API Operations - List Projects
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* List all projects from API using OAuth authentication
|
|
94
|
+
* @param {Object} oauthClient - OAuth HTTP client with authenticatedRequest method
|
|
95
|
+
* @returns {Promise<Array>} Array of projects with organization info
|
|
96
|
+
*/
|
|
97
|
+
export async function listProjectsWithOAuth(oauthClient) {
|
|
98
|
+
// First get the user's organizations via whoami
|
|
99
|
+
let whoami = await oauthClient.authenticatedRequest('/api/auth/cli/whoami', {
|
|
100
|
+
method: 'GET'
|
|
101
|
+
});
|
|
102
|
+
let organizations = extractOrganizations(whoami);
|
|
103
|
+
if (organizations.length === 0) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fetch projects for each organization
|
|
108
|
+
let allProjects = [];
|
|
109
|
+
for (let org of organizations) {
|
|
110
|
+
try {
|
|
111
|
+
let response = await oauthClient.authenticatedRequest('/api/project', {
|
|
112
|
+
method: 'GET',
|
|
113
|
+
headers: buildOrgHeader(org.slug)
|
|
114
|
+
});
|
|
115
|
+
let projects = extractProjects(response);
|
|
116
|
+
let enriched = enrichProjectsWithOrg(projects, org);
|
|
117
|
+
allProjects.push(...enriched);
|
|
118
|
+
} catch {
|
|
119
|
+
// Silently skip failed orgs
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return allProjects;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* List all projects from API using API token authentication
|
|
127
|
+
* @param {Object} apiClient - API HTTP client with request method
|
|
128
|
+
* @returns {Promise<Array>} Array of projects
|
|
129
|
+
*/
|
|
130
|
+
export async function listProjectsWithApiToken(apiClient) {
|
|
131
|
+
let response = await apiClient.request('/api/project', {
|
|
132
|
+
method: 'GET'
|
|
133
|
+
});
|
|
134
|
+
return extractProjects(response);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* List all projects, trying OAuth first then falling back to API token
|
|
139
|
+
* @param {Object} options - Options
|
|
140
|
+
* @param {Object} [options.oauthClient] - OAuth HTTP client
|
|
141
|
+
* @param {Object} [options.apiClient] - API token HTTP client
|
|
142
|
+
* @returns {Promise<Array>} Array of projects
|
|
143
|
+
*/
|
|
144
|
+
export async function listProjects({
|
|
145
|
+
oauthClient,
|
|
146
|
+
apiClient
|
|
147
|
+
}) {
|
|
148
|
+
// Try OAuth-based request first
|
|
149
|
+
if (oauthClient) {
|
|
150
|
+
try {
|
|
151
|
+
return await listProjectsWithOAuth(oauthClient);
|
|
152
|
+
} catch {
|
|
153
|
+
// Fall back to API token
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fall back to API token-based request
|
|
158
|
+
if (apiClient) {
|
|
159
|
+
try {
|
|
160
|
+
return await listProjectsWithApiToken(apiClient);
|
|
161
|
+
} catch {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// No authentication available
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// API Operations - Get Project
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get project details using OAuth authentication
|
|
176
|
+
* @param {Object} oauthClient - OAuth HTTP client
|
|
177
|
+
* @param {string} projectSlug - Project slug
|
|
178
|
+
* @param {string} organizationSlug - Organization slug
|
|
179
|
+
* @returns {Promise<Object>} Project details
|
|
180
|
+
*/
|
|
181
|
+
export async function getProjectWithOAuth(oauthClient, projectSlug, organizationSlug) {
|
|
182
|
+
let response = await oauthClient.authenticatedRequest(buildProjectUrl(projectSlug), {
|
|
183
|
+
method: 'GET',
|
|
184
|
+
headers: buildOrgHeader(organizationSlug)
|
|
185
|
+
});
|
|
186
|
+
return extractProject(response);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get project details using API token authentication
|
|
191
|
+
* @param {Object} apiClient - API HTTP client
|
|
192
|
+
* @param {string} projectSlug - Project slug
|
|
193
|
+
* @param {string} organizationSlug - Organization slug
|
|
194
|
+
* @returns {Promise<Object>} Project details
|
|
195
|
+
*/
|
|
196
|
+
export async function getProjectWithApiToken(apiClient, projectSlug, organizationSlug) {
|
|
197
|
+
let response = await apiClient.request(buildProjectUrl(projectSlug), {
|
|
198
|
+
method: 'GET',
|
|
199
|
+
headers: buildOrgHeader(organizationSlug)
|
|
200
|
+
});
|
|
201
|
+
return extractProject(response);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get project details, trying OAuth first then falling back to API token
|
|
206
|
+
* @param {Object} options - Options
|
|
207
|
+
* @param {Object} [options.oauthClient] - OAuth HTTP client
|
|
208
|
+
* @param {Object} [options.apiClient] - API token HTTP client
|
|
209
|
+
* @param {string} options.projectSlug - Project slug
|
|
210
|
+
* @param {string} options.organizationSlug - Organization slug
|
|
211
|
+
* @returns {Promise<Object>} Project details
|
|
212
|
+
*/
|
|
213
|
+
export async function getProject({
|
|
214
|
+
oauthClient,
|
|
215
|
+
apiClient,
|
|
216
|
+
projectSlug,
|
|
217
|
+
organizationSlug
|
|
218
|
+
}) {
|
|
219
|
+
// Try OAuth-based request first
|
|
220
|
+
if (oauthClient) {
|
|
221
|
+
try {
|
|
222
|
+
return await getProjectWithOAuth(oauthClient, projectSlug, organizationSlug);
|
|
223
|
+
} catch {
|
|
224
|
+
// Fall back to API token
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Fall back to API token
|
|
229
|
+
if (apiClient) {
|
|
230
|
+
try {
|
|
231
|
+
return await getProjectWithApiToken(apiClient, projectSlug, organizationSlug);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw buildProjectFetchError(error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
throw buildNoAuthError();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// API Operations - Recent Builds
|
|
241
|
+
// ============================================================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get recent builds for a project using OAuth authentication
|
|
245
|
+
* @param {Object} oauthClient - OAuth HTTP client
|
|
246
|
+
* @param {string} projectSlug - Project slug
|
|
247
|
+
* @param {string} organizationSlug - Organization slug
|
|
248
|
+
* @param {Object} options - Query options
|
|
249
|
+
* @returns {Promise<Array>} Array of builds
|
|
250
|
+
*/
|
|
251
|
+
export async function getRecentBuildsWithOAuth(oauthClient, projectSlug, organizationSlug, options = {}) {
|
|
252
|
+
let response = await oauthClient.authenticatedRequest(buildBuildsUrl(projectSlug, options), {
|
|
253
|
+
method: 'GET',
|
|
254
|
+
headers: buildOrgHeader(organizationSlug)
|
|
255
|
+
});
|
|
256
|
+
return extractBuilds(response);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get recent builds for a project using API token authentication
|
|
261
|
+
* @param {Object} apiClient - API HTTP client
|
|
262
|
+
* @param {string} projectSlug - Project slug
|
|
263
|
+
* @param {string} organizationSlug - Organization slug
|
|
264
|
+
* @param {Object} options - Query options
|
|
265
|
+
* @returns {Promise<Array>} Array of builds
|
|
266
|
+
*/
|
|
267
|
+
export async function getRecentBuildsWithApiToken(apiClient, projectSlug, organizationSlug, options = {}) {
|
|
268
|
+
let response = await apiClient.request(buildBuildsUrl(projectSlug, options), {
|
|
269
|
+
method: 'GET',
|
|
270
|
+
headers: buildOrgHeader(organizationSlug)
|
|
271
|
+
});
|
|
272
|
+
return extractBuilds(response);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get recent builds for a project, trying OAuth first then falling back to API token
|
|
277
|
+
* @param {Object} options - Options
|
|
278
|
+
* @param {Object} [options.oauthClient] - OAuth HTTP client
|
|
279
|
+
* @param {Object} [options.apiClient] - API token HTTP client
|
|
280
|
+
* @param {string} options.projectSlug - Project slug
|
|
281
|
+
* @param {string} options.organizationSlug - Organization slug
|
|
282
|
+
* @param {number} [options.limit] - Number of builds to fetch
|
|
283
|
+
* @param {string} [options.branch] - Filter by branch
|
|
284
|
+
* @returns {Promise<Array>} Array of builds
|
|
285
|
+
*/
|
|
286
|
+
export async function getRecentBuilds({
|
|
287
|
+
oauthClient,
|
|
288
|
+
apiClient,
|
|
289
|
+
projectSlug,
|
|
290
|
+
organizationSlug,
|
|
291
|
+
limit,
|
|
292
|
+
branch
|
|
293
|
+
}) {
|
|
294
|
+
let queryOptions = {
|
|
295
|
+
limit,
|
|
296
|
+
branch
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Try OAuth-based request first
|
|
300
|
+
if (oauthClient) {
|
|
301
|
+
try {
|
|
302
|
+
return await getRecentBuildsWithOAuth(oauthClient, projectSlug, organizationSlug, queryOptions);
|
|
303
|
+
} catch {
|
|
304
|
+
// Fall back to API token
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Fall back to API token-based request
|
|
309
|
+
if (apiClient) {
|
|
310
|
+
try {
|
|
311
|
+
return await getRecentBuildsWithApiToken(apiClient, projectSlug, organizationSlug, queryOptions);
|
|
312
|
+
} catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// No authentication available
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// API Operations - Project Tokens
|
|
323
|
+
// ============================================================================
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Create a project token
|
|
327
|
+
* @param {Object} apiClient - API HTTP client with request method
|
|
328
|
+
* @param {string} projectSlug - Project slug
|
|
329
|
+
* @param {string} organizationSlug - Organization slug
|
|
330
|
+
* @param {Object} tokenData - Token data
|
|
331
|
+
* @param {string} tokenData.name - Token name
|
|
332
|
+
* @param {string} [tokenData.description] - Token description
|
|
333
|
+
* @returns {Promise<Object>} Created token
|
|
334
|
+
*/
|
|
335
|
+
export async function createProjectToken(apiClient, projectSlug, organizationSlug, tokenData) {
|
|
336
|
+
if (!apiClient) {
|
|
337
|
+
throw buildNoApiServiceError();
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
let response = await apiClient.request(buildTokensUrl(organizationSlug, projectSlug), {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: {
|
|
343
|
+
'Content-Type': 'application/json'
|
|
344
|
+
},
|
|
345
|
+
body: JSON.stringify(tokenData)
|
|
346
|
+
});
|
|
347
|
+
return extractToken(response);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw buildTokenCreateError(error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* List project tokens
|
|
355
|
+
* @param {Object} apiClient - API HTTP client with request method
|
|
356
|
+
* @param {string} projectSlug - Project slug
|
|
357
|
+
* @param {string} organizationSlug - Organization slug
|
|
358
|
+
* @returns {Promise<Array>} Array of tokens
|
|
359
|
+
*/
|
|
360
|
+
export async function listProjectTokens(apiClient, projectSlug, organizationSlug) {
|
|
361
|
+
if (!apiClient) {
|
|
362
|
+
throw buildNoApiServiceError();
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
let response = await apiClient.request(buildTokensUrl(organizationSlug, projectSlug), {
|
|
366
|
+
method: 'GET'
|
|
367
|
+
});
|
|
368
|
+
return extractTokens(response);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
throw buildTokensFetchError(error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Revoke a project token
|
|
376
|
+
* @param {Object} apiClient - API HTTP client with request method
|
|
377
|
+
* @param {string} projectSlug - Project slug
|
|
378
|
+
* @param {string} organizationSlug - Organization slug
|
|
379
|
+
* @param {string} tokenId - Token ID to revoke
|
|
380
|
+
* @returns {Promise<void>}
|
|
381
|
+
*/
|
|
382
|
+
export async function revokeProjectToken(apiClient, projectSlug, organizationSlug, tokenId) {
|
|
383
|
+
if (!apiClient) {
|
|
384
|
+
throw buildNoApiServiceError();
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
await apiClient.request(buildTokensUrl(organizationSlug, projectSlug, tokenId), {
|
|
388
|
+
method: 'DELETE'
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
throw buildTokenRevokeError(error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Generator Core - Pure functions for report generation
|
|
3
|
+
*
|
|
4
|
+
* No I/O, no side effects - just data transformations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_REPORT_DIR_NAME = 'report';
|
|
14
|
+
export const BUNDLE_FILENAME = 'reporter-bundle.js';
|
|
15
|
+
export const CSS_FILENAME = 'reporter-bundle.css';
|
|
16
|
+
export const INDEX_FILENAME = 'index.html';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Path Building
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build report directory path
|
|
24
|
+
* @param {string} workingDir - Working directory
|
|
25
|
+
* @returns {string} Report directory path
|
|
26
|
+
*/
|
|
27
|
+
export function buildReportDir(workingDir) {
|
|
28
|
+
return join(workingDir, '.vizzly', DEFAULT_REPORT_DIR_NAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build report HTML path
|
|
33
|
+
* @param {string} reportDir - Report directory
|
|
34
|
+
* @returns {string} Report HTML path
|
|
35
|
+
*/
|
|
36
|
+
export function buildReportPath(reportDir) {
|
|
37
|
+
return join(reportDir, INDEX_FILENAME);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build bundle source paths
|
|
42
|
+
* @param {string} projectRoot - Project root directory
|
|
43
|
+
* @returns {{ bundlePath: string, cssPath: string }}
|
|
44
|
+
*/
|
|
45
|
+
export function buildBundleSourcePaths(projectRoot) {
|
|
46
|
+
return {
|
|
47
|
+
bundlePath: join(projectRoot, 'dist', 'reporter', 'reporter-bundle.iife.js'),
|
|
48
|
+
cssPath: join(projectRoot, 'dist', 'reporter', 'reporter-bundle.css')
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build bundle destination paths
|
|
54
|
+
* @param {string} reportDir - Report directory
|
|
55
|
+
* @returns {{ bundleDest: string, cssDest: string }}
|
|
56
|
+
*/
|
|
57
|
+
export function buildBundleDestPaths(reportDir) {
|
|
58
|
+
return {
|
|
59
|
+
bundleDest: join(reportDir, BUNDLE_FILENAME),
|
|
60
|
+
cssDest: join(reportDir, CSS_FILENAME)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Validation
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate report data
|
|
70
|
+
* @param {any} reportData - Report data to validate
|
|
71
|
+
* @returns {{ valid: boolean, error: string|null }}
|
|
72
|
+
*/
|
|
73
|
+
export function validateReportData(reportData) {
|
|
74
|
+
if (!reportData || typeof reportData !== 'object') {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: 'Invalid report data provided'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
valid: true,
|
|
82
|
+
error: null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if bundles exist
|
|
88
|
+
* @param {boolean} bundleExists - Whether bundle JS exists
|
|
89
|
+
* @param {boolean} cssExists - Whether bundle CSS exists
|
|
90
|
+
* @returns {{ valid: boolean, error: string|null }}
|
|
91
|
+
*/
|
|
92
|
+
export function validateBundlesExist(bundleExists, cssExists) {
|
|
93
|
+
if (!bundleExists || !cssExists) {
|
|
94
|
+
return {
|
|
95
|
+
valid: false,
|
|
96
|
+
error: 'Reporter bundles not found. Run "npm run build:reporter" first.'
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
valid: true,
|
|
101
|
+
error: null
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Data Serialization
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Safely serialize data for embedding in HTML script tag
|
|
111
|
+
* Escapes characters that could break out of script context
|
|
112
|
+
* @param {Object} data - Data to serialize
|
|
113
|
+
* @returns {string} Safely serialized JSON string
|
|
114
|
+
*/
|
|
115
|
+
export function serializeForHtml(data) {
|
|
116
|
+
return JSON.stringify(data).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// HTML Template Generation
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate the main HTML template with React reporter
|
|
125
|
+
* @param {Object} options - Template options
|
|
126
|
+
* @param {string} options.serializedData - Serialized report data
|
|
127
|
+
* @param {string} options.timestamp - ISO timestamp string
|
|
128
|
+
* @param {string} options.displayDate - Human-readable date string
|
|
129
|
+
* @returns {string} HTML content
|
|
130
|
+
*/
|
|
131
|
+
export function generateMainTemplate({
|
|
132
|
+
serializedData,
|
|
133
|
+
timestamp,
|
|
134
|
+
displayDate
|
|
135
|
+
}) {
|
|
136
|
+
return `<!DOCTYPE html>
|
|
137
|
+
<html lang="en">
|
|
138
|
+
<head>
|
|
139
|
+
<meta charset="UTF-8">
|
|
140
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
141
|
+
<title>Vizzly Dev Report - ${displayDate}</title>
|
|
142
|
+
<link rel="stylesheet" href="./reporter-bundle.css">
|
|
143
|
+
<style>
|
|
144
|
+
/* Loading spinner styles */
|
|
145
|
+
.reporter-loading {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
justify-content: center;
|
|
149
|
+
min-height: 100vh;
|
|
150
|
+
background: #0f172a;
|
|
151
|
+
color: #f59e0b;
|
|
152
|
+
}
|
|
153
|
+
.spinner {
|
|
154
|
+
width: 48px;
|
|
155
|
+
height: 48px;
|
|
156
|
+
border: 4px solid rgba(245, 158, 11, 0.2);
|
|
157
|
+
border-top-color: #f59e0b;
|
|
158
|
+
border-radius: 50%;
|
|
159
|
+
animation: spin 1s linear infinite;
|
|
160
|
+
margin-bottom: 1rem;
|
|
161
|
+
}
|
|
162
|
+
@keyframes spin {
|
|
163
|
+
to { transform: rotate(360deg); }
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
166
|
+
</head>
|
|
167
|
+
<body>
|
|
168
|
+
<div id="vizzly-reporter-root">
|
|
169
|
+
<div class="reporter-loading">
|
|
170
|
+
<div style="text-align: center;">
|
|
171
|
+
<div class="spinner"></div>
|
|
172
|
+
<p>Loading Vizzly Report...</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<script>
|
|
178
|
+
// Embedded report data (static mode)
|
|
179
|
+
window.VIZZLY_REPORTER_DATA = ${serializedData};
|
|
180
|
+
window.VIZZLY_STATIC_MODE = true;
|
|
181
|
+
|
|
182
|
+
// Generate timestamp for "generated at" display
|
|
183
|
+
window.VIZZLY_REPORT_GENERATED_AT = "${timestamp}";
|
|
184
|
+
|
|
185
|
+
console.log('Vizzly Static Report loaded');
|
|
186
|
+
console.log('Report data:', window.VIZZLY_REPORTER_DATA?.summary);
|
|
187
|
+
</script>
|
|
188
|
+
<script src="./reporter-bundle.js"></script>
|
|
189
|
+
</body>
|
|
190
|
+
</html>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Generate fallback HTML template (when bundles are missing)
|
|
195
|
+
* @param {Object} options - Template options
|
|
196
|
+
* @param {Object} options.summary - Report summary
|
|
197
|
+
* @param {Array} options.comparisons - Comparison results
|
|
198
|
+
* @param {string} options.displayDate - Human-readable date string
|
|
199
|
+
* @returns {string} HTML content
|
|
200
|
+
*/
|
|
201
|
+
export function generateFallbackTemplate({
|
|
202
|
+
summary,
|
|
203
|
+
comparisons,
|
|
204
|
+
displayDate
|
|
205
|
+
}) {
|
|
206
|
+
let failed = comparisons.filter(c => c.status === 'failed');
|
|
207
|
+
let failedSection = failed.length > 0 ? `
|
|
208
|
+
<h2>Failed Comparisons</h2>
|
|
209
|
+
<ul>
|
|
210
|
+
${failed.map(c => `<li>${c.name} - ${c.diffPercentage || 0}% difference</li>`).join('')}
|
|
211
|
+
</ul>
|
|
212
|
+
` : '<p style="text-align: center; font-size: 1.5rem;">✅ All tests passed!</p>';
|
|
213
|
+
return `<!DOCTYPE html>
|
|
214
|
+
<html lang="en">
|
|
215
|
+
<head>
|
|
216
|
+
<meta charset="UTF-8">
|
|
217
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
218
|
+
<title>Vizzly Dev Report</title>
|
|
219
|
+
<style>
|
|
220
|
+
body {
|
|
221
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
222
|
+
background: #0f172a;
|
|
223
|
+
color: #e2e8f0;
|
|
224
|
+
padding: 2rem;
|
|
225
|
+
}
|
|
226
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
227
|
+
.header { text-align: center; margin-bottom: 2rem; }
|
|
228
|
+
.summary {
|
|
229
|
+
display: flex;
|
|
230
|
+
gap: 2rem;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
margin: 2rem 0;
|
|
233
|
+
}
|
|
234
|
+
.stat { text-align: center; }
|
|
235
|
+
.stat-number {
|
|
236
|
+
font-size: 3rem;
|
|
237
|
+
font-weight: bold;
|
|
238
|
+
display: block;
|
|
239
|
+
}
|
|
240
|
+
.warning {
|
|
241
|
+
background: #fef3c7;
|
|
242
|
+
color: #92400e;
|
|
243
|
+
padding: 1rem;
|
|
244
|
+
border-radius: 0.5rem;
|
|
245
|
+
margin: 2rem 0;
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
248
|
+
</head>
|
|
249
|
+
<body>
|
|
250
|
+
<div class="container">
|
|
251
|
+
<div class="header">
|
|
252
|
+
<h1>🐻 Vizzly Dev Report</h1>
|
|
253
|
+
<p>Generated: ${displayDate}</p>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div class="summary">
|
|
257
|
+
<div class="stat">
|
|
258
|
+
<span class="stat-number">${summary.total || 0}</span>
|
|
259
|
+
<span>Total</span>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="stat">
|
|
262
|
+
<span class="stat-number" style="color: #10b981;">${summary.passed || 0}</span>
|
|
263
|
+
<span>Passed</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="stat">
|
|
266
|
+
<span class="stat-number" style="color: #ef4444;">${summary.failed || 0}</span>
|
|
267
|
+
<span>Failed</span>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div class="warning">
|
|
272
|
+
<strong>⚠️ Limited Report</strong>
|
|
273
|
+
<p>This is a fallback report. For the full interactive experience, ensure the reporter bundle is built:</p>
|
|
274
|
+
<code>npm run build:reporter</code>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
${failedSection}
|
|
278
|
+
</div>
|
|
279
|
+
</body>
|
|
280
|
+
</html>`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Build HTML content for the report
|
|
285
|
+
* @param {Object} reportData - Report data
|
|
286
|
+
* @param {Date} date - Current date
|
|
287
|
+
* @returns {string} HTML content
|
|
288
|
+
*/
|
|
289
|
+
export function buildHtmlContent(reportData, date) {
|
|
290
|
+
let serializedData = serializeForHtml(reportData);
|
|
291
|
+
let timestamp = date.toISOString();
|
|
292
|
+
let displayDate = date.toLocaleString();
|
|
293
|
+
return generateMainTemplate({
|
|
294
|
+
serializedData,
|
|
295
|
+
timestamp,
|
|
296
|
+
displayDate
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Build fallback HTML content
|
|
302
|
+
* @param {Object} reportData - Report data
|
|
303
|
+
* @param {Date} date - Current date
|
|
304
|
+
* @returns {string} HTML content
|
|
305
|
+
*/
|
|
306
|
+
export function buildFallbackHtmlContent(reportData, date) {
|
|
307
|
+
let summary = reportData.summary || {};
|
|
308
|
+
let comparisons = reportData.comparisons || [];
|
|
309
|
+
let displayDate = date.toLocaleString();
|
|
310
|
+
return generateFallbackTemplate({
|
|
311
|
+
summary,
|
|
312
|
+
comparisons,
|
|
313
|
+
displayDate
|
|
314
|
+
});
|
|
315
|
+
}
|