@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.
Files changed (72) hide show
  1. package/dist/api/client.js +134 -0
  2. package/dist/api/core.js +341 -0
  3. package/dist/api/endpoints.js +314 -0
  4. package/dist/api/index.js +19 -0
  5. package/dist/auth/client.js +91 -0
  6. package/dist/auth/core.js +176 -0
  7. package/dist/auth/index.js +30 -0
  8. package/dist/auth/operations.js +148 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/commands/doctor.js +3 -3
  11. package/dist/commands/finalize.js +41 -15
  12. package/dist/commands/login.js +7 -6
  13. package/dist/commands/logout.js +4 -4
  14. package/dist/commands/project.js +5 -4
  15. package/dist/commands/run.js +158 -90
  16. package/dist/commands/status.js +22 -18
  17. package/dist/commands/tdd.js +105 -78
  18. package/dist/commands/upload.js +61 -26
  19. package/dist/commands/whoami.js +4 -4
  20. package/dist/config/core.js +438 -0
  21. package/dist/config/index.js +13 -0
  22. package/dist/config/operations.js +327 -0
  23. package/dist/index.js +1 -1
  24. package/dist/project/core.js +295 -0
  25. package/dist/project/index.js +13 -0
  26. package/dist/project/operations.js +393 -0
  27. package/dist/report-generator/core.js +315 -0
  28. package/dist/report-generator/index.js +8 -0
  29. package/dist/report-generator/operations.js +196 -0
  30. package/dist/reporter/reporter-bundle.iife.js +16 -16
  31. package/dist/screenshot-server/core.js +157 -0
  32. package/dist/screenshot-server/index.js +11 -0
  33. package/dist/screenshot-server/operations.js +183 -0
  34. package/dist/sdk/index.js +3 -2
  35. package/dist/server/handlers/api-handler.js +14 -5
  36. package/dist/server/handlers/tdd-handler.js +80 -48
  37. package/dist/server-manager/core.js +183 -0
  38. package/dist/server-manager/index.js +81 -0
  39. package/dist/server-manager/operations.js +208 -0
  40. package/dist/services/build-manager.js +2 -69
  41. package/dist/services/index.js +21 -48
  42. package/dist/services/screenshot-server.js +40 -74
  43. package/dist/services/server-manager.js +45 -80
  44. package/dist/services/static-report-generator.js +21 -163
  45. package/dist/services/test-runner.js +90 -250
  46. package/dist/services/uploader.js +56 -358
  47. package/dist/tdd/core/hotspot-coverage.js +112 -0
  48. package/dist/tdd/core/signature.js +101 -0
  49. package/dist/tdd/index.js +19 -0
  50. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  51. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  52. package/dist/tdd/services/baseline-downloader.js +151 -0
  53. package/dist/tdd/services/baseline-manager.js +166 -0
  54. package/dist/tdd/services/comparison-service.js +230 -0
  55. package/dist/tdd/services/hotspot-service.js +71 -0
  56. package/dist/tdd/services/result-service.js +123 -0
  57. package/dist/tdd/tdd-service.js +1081 -0
  58. package/dist/test-runner/core.js +255 -0
  59. package/dist/test-runner/index.js +13 -0
  60. package/dist/test-runner/operations.js +483 -0
  61. package/dist/uploader/core.js +396 -0
  62. package/dist/uploader/index.js +11 -0
  63. package/dist/uploader/operations.js +412 -0
  64. package/package.json +7 -12
  65. package/dist/services/api-service.js +0 -412
  66. package/dist/services/auth-service.js +0 -226
  67. package/dist/services/config-service.js +0 -369
  68. package/dist/services/html-report-generator.js +0 -455
  69. package/dist/services/project-service.js +0 -326
  70. package/dist/services/report-generator/report.css +0 -411
  71. package/dist/services/report-generator/viewer.js +0 -102
  72. 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
+ }