@vizzly-testing/cli 0.19.2 → 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 (76) 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/client/index.js +0 -1
  11. package/dist/commands/doctor.js +3 -3
  12. package/dist/commands/finalize.js +41 -15
  13. package/dist/commands/login.js +7 -6
  14. package/dist/commands/logout.js +4 -4
  15. package/dist/commands/project.js +5 -4
  16. package/dist/commands/run.js +158 -90
  17. package/dist/commands/status.js +22 -18
  18. package/dist/commands/tdd.js +105 -78
  19. package/dist/commands/upload.js +61 -26
  20. package/dist/commands/whoami.js +4 -4
  21. package/dist/config/core.js +438 -0
  22. package/dist/config/index.js +13 -0
  23. package/dist/config/operations.js +327 -0
  24. package/dist/index.js +1 -1
  25. package/dist/project/core.js +295 -0
  26. package/dist/project/index.js +13 -0
  27. package/dist/project/operations.js +393 -0
  28. package/dist/report-generator/core.js +315 -0
  29. package/dist/report-generator/index.js +8 -0
  30. package/dist/report-generator/operations.js +196 -0
  31. package/dist/reporter/reporter-bundle.iife.js +16 -16
  32. package/dist/screenshot-server/core.js +157 -0
  33. package/dist/screenshot-server/index.js +11 -0
  34. package/dist/screenshot-server/operations.js +183 -0
  35. package/dist/sdk/index.js +3 -2
  36. package/dist/server/handlers/api-handler.js +14 -5
  37. package/dist/server/handlers/tdd-handler.js +80 -48
  38. package/dist/server-manager/core.js +183 -0
  39. package/dist/server-manager/index.js +81 -0
  40. package/dist/server-manager/operations.js +208 -0
  41. package/dist/services/build-manager.js +2 -69
  42. package/dist/services/index.js +21 -48
  43. package/dist/services/screenshot-server.js +40 -74
  44. package/dist/services/server-manager.js +45 -80
  45. package/dist/services/static-report-generator.js +21 -163
  46. package/dist/services/test-runner.js +90 -249
  47. package/dist/services/uploader.js +56 -358
  48. package/dist/tdd/core/hotspot-coverage.js +112 -0
  49. package/dist/tdd/core/signature.js +101 -0
  50. package/dist/tdd/index.js +19 -0
  51. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  52. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  53. package/dist/tdd/services/baseline-downloader.js +151 -0
  54. package/dist/tdd/services/baseline-manager.js +166 -0
  55. package/dist/tdd/services/comparison-service.js +230 -0
  56. package/dist/tdd/services/hotspot-service.js +71 -0
  57. package/dist/tdd/services/result-service.js +123 -0
  58. package/dist/tdd/tdd-service.js +1081 -0
  59. package/dist/test-runner/core.js +255 -0
  60. package/dist/test-runner/index.js +13 -0
  61. package/dist/test-runner/operations.js +483 -0
  62. package/dist/types/client.d.ts +4 -2
  63. package/dist/types/index.d.ts +5 -0
  64. package/dist/uploader/core.js +396 -0
  65. package/dist/uploader/index.js +11 -0
  66. package/dist/uploader/operations.js +412 -0
  67. package/dist/utils/config-schema.js +8 -3
  68. package/package.json +7 -12
  69. package/dist/services/api-service.js +0 -412
  70. package/dist/services/auth-service.js +0 -226
  71. package/dist/services/config-service.js +0 -369
  72. package/dist/services/html-report-generator.js +0 -455
  73. package/dist/services/project-service.js +0 -326
  74. package/dist/services/report-generator/report.css +0 -411
  75. package/dist/services/report-generator/viewer.js +0 -102
  76. package/dist/services/tdd-service.js +0 -1429
@@ -1,99 +1,65 @@
1
1
  /**
2
2
  * Screenshot Server Service
3
3
  * Listens for and processes screenshots from the test runner
4
+ *
5
+ * This class is a thin wrapper around the functional operations in
6
+ * src/screenshot-server/. It maintains backwards compatibility while
7
+ * delegating to pure functions for testability.
4
8
  */
5
9
 
6
10
  import { createServer } from 'node:http';
7
11
  import { VizzlyError } from '../errors/vizzly-error.js';
12
+ import { handleRequest, parseRequestBody, startServer, stopServer } from '../screenshot-server/index.js';
8
13
  import * as output from '../utils/output.js';
9
14
  export class ScreenshotServer {
10
- constructor(config, buildManager) {
15
+ constructor(config, buildManager, options = {}) {
11
16
  this.config = config;
12
17
  this.buildManager = buildManager;
13
18
  this.server = null;
19
+
20
+ // Dependency injection for testing
21
+ this.deps = options.deps || {
22
+ createHttpServer: createServer,
23
+ output,
24
+ createError: (message, code) => new VizzlyError(message, code)
25
+ };
14
26
  }
15
27
  async start() {
16
- this.server = createServer(this.handleRequest.bind(this));
17
- return new Promise((resolve, reject) => {
18
- this.server.listen(this.config.server.port, '127.0.0.1', error => {
19
- if (error) {
20
- reject(new VizzlyError(`Failed to start screenshot server: ${error.message}`, 'SERVER_ERROR'));
21
- } else {
22
- output.info(`Screenshot server listening on http://127.0.0.1:${this.config.server.port}`);
23
- resolve();
24
- }
25
- });
28
+ this.server = await startServer({
29
+ config: this.config,
30
+ requestHandler: this.handleRequest.bind(this),
31
+ deps: {
32
+ createHttpServer: this.deps.createHttpServer,
33
+ createError: this.deps.createError,
34
+ output: this.deps.output
35
+ }
26
36
  });
27
37
  }
28
38
  async stop() {
29
- if (this.server) {
30
- return new Promise(resolve => {
31
- this.server.close(() => {
32
- output.info('Screenshot server stopped');
33
- resolve();
34
- });
35
- });
36
- }
39
+ await stopServer({
40
+ server: this.server,
41
+ deps: {
42
+ output: this.deps.output
43
+ }
44
+ });
37
45
  }
38
46
  async handleRequest(req, res) {
39
- if (req.method === 'POST' && req.url === '/screenshot') {
40
- try {
41
- const body = await this.parseRequestBody(req);
42
- const {
43
- buildId,
44
- name,
45
- image,
46
- properties
47
- } = body;
48
- if (!name || !image) {
49
- res.statusCode = 400;
50
- res.end(JSON.stringify({
51
- error: 'name and image are required'
52
- }));
53
- return;
54
- }
55
-
56
- // Use default buildId if none provided
57
- const effectiveBuildId = buildId || 'default';
58
- await this.buildManager.addScreenshot(effectiveBuildId, {
59
- name,
60
- image,
61
- properties
62
- });
63
- res.statusCode = 200;
64
- res.end(JSON.stringify({
65
- success: true
66
- }));
67
- } catch (error) {
68
- output.error('Failed to process screenshot:', error);
69
- res.statusCode = 500;
70
- res.end(JSON.stringify({
71
- error: 'Internal server error'
72
- }));
47
+ await handleRequest({
48
+ req,
49
+ res,
50
+ deps: {
51
+ buildManager: this.buildManager,
52
+ createError: this.deps.createError,
53
+ output: this.deps.output
73
54
  }
74
- } else {
75
- res.statusCode = 404;
76
- res.end(JSON.stringify({
77
- error: 'Not found'
78
- }));
79
- }
55
+ });
80
56
  }
81
57
  async parseRequestBody(req) {
82
- return new Promise((resolve, reject) => {
83
- let body = '';
84
- req.on('data', chunk => {
85
- body += chunk.toString();
86
- });
87
- req.on('end', () => {
88
- try {
89
- resolve(JSON.parse(body));
90
- } catch {
91
- reject(new VizzlyError('Invalid JSON in request body', 'INVALID_JSON'));
92
- }
93
- });
94
- req.on('error', error => {
95
- reject(new VizzlyError(`Request error: ${error.message}`, 'REQUEST_ERROR'));
96
- });
58
+ return parseRequestBody({
59
+ req,
60
+ deps: {
61
+ createError: this.deps.createError
62
+ }
97
63
  });
98
64
  }
99
65
  }
@@ -1,108 +1,71 @@
1
1
  /**
2
2
  * Server Manager Service
3
3
  * Manages the HTTP server with functional handlers
4
+ *
5
+ * This class is a thin wrapper around the functional operations in
6
+ * src/server-manager/. It maintains backwards compatibility while
7
+ * delegating to pure functions for testability.
4
8
  */
5
9
 
6
10
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
7
- import { join } from 'node:path';
11
+ import { createApiClient } from '../api/index.js';
8
12
  import { createApiHandler } from '../server/handlers/api-handler.js';
9
13
  import { createTddHandler } from '../server/handlers/tdd-handler.js';
10
14
  import { createHttpServer } from '../server/http-server.js';
15
+ import { buildServerInterface, getTddResults, startServer, stopServer } from '../server-manager/index.js';
11
16
  export class ServerManager {
12
17
  constructor(config, options = {}) {
13
18
  this.config = config;
14
19
  this.httpServer = null;
15
20
  this.handler = null;
16
21
  this.services = options.services || {};
22
+ this.tddMode = false;
23
+
24
+ // Dependency injection for testing - defaults to real implementations
25
+ this.deps = options.deps || {
26
+ createHttpServer,
27
+ createTddHandler,
28
+ createApiHandler,
29
+ createApiClient,
30
+ fs: {
31
+ mkdirSync,
32
+ writeFileSync,
33
+ existsSync,
34
+ unlinkSync
35
+ }
36
+ };
17
37
  }
18
38
  async start(buildId = null, tddMode = false, setBaseline = false) {
19
39
  this.buildId = buildId;
20
40
  this.tddMode = tddMode;
21
41
  this.setBaseline = setBaseline;
22
- const port = this.config?.server?.port || 47392;
23
- if (this.tddMode) {
24
- this.handler = createTddHandler(this.config, process.cwd(), this.config?.baselineBuildId, this.config?.baselineComparisonId, this.setBaseline);
25
- await this.handler.initialize();
26
- } else {
27
- const apiService = await this.createApiService();
28
- this.handler = createApiHandler(apiService);
29
- }
30
-
31
- // Pass buildId and tddService in services so http-server can use them
32
- const servicesWithExtras = {
33
- ...this.services,
34
- buildId: this.buildId,
35
- // Expose tddService for baseline download operations (TDD mode only)
36
- tddService: this.tddMode ? this.handler.tddService : null
37
- };
38
- this.httpServer = createHttpServer(port, this.handler, servicesWithExtras);
39
- if (this.httpServer) {
40
- await this.httpServer.start();
41
- }
42
-
43
- // Write server info to .vizzly/server.json for SDK discovery
44
- // This allows SDKs that can't access environment variables (like Swift/iOS)
45
- // to discover both the server port and current build ID
46
- try {
47
- const vizzlyDir = join(process.cwd(), '.vizzly');
48
- mkdirSync(vizzlyDir, {
49
- recursive: true
50
- });
51
- const serverFile = join(vizzlyDir, 'server.json');
52
- const serverInfo = {
53
- port: port.toString(),
54
- pid: process.pid,
55
- startTime: Date.now()
56
- };
57
-
58
- // Include buildId if we have one (for `vizzly run` mode)
59
- if (this.buildId) {
60
- serverInfo.buildId = this.buildId;
61
- }
62
- writeFileSync(serverFile, JSON.stringify(serverInfo, null, 2));
63
- } catch {
64
- // Non-fatal - SDK can still use health check or environment variables
65
- }
66
- }
67
- async createApiService() {
68
- if (!this.config.apiKey) return null;
69
- const {
70
- ApiService
71
- } = await import('./api-service.js');
72
- return new ApiService({
73
- ...this.config,
74
- command: 'run'
42
+ let result = await startServer({
43
+ config: this.config,
44
+ buildId,
45
+ tddMode,
46
+ setBaseline,
47
+ projectRoot: process.cwd(),
48
+ services: this.services,
49
+ deps: this.deps
75
50
  });
51
+ this.httpServer = result.httpServer;
52
+ this.handler = result.handler;
76
53
  }
77
54
  async stop() {
78
- if (this.httpServer) {
79
- await this.httpServer.stop();
80
- }
81
- if (this.handler?.cleanup) {
82
- try {
83
- this.handler.cleanup();
84
- } catch {
85
- // Don't throw - cleanup errors shouldn't fail the stop process
86
- }
87
- }
88
-
89
- // Clean up server.json so the client SDK doesn't try to connect to a dead server
90
- try {
91
- const serverFile = join(process.cwd(), '.vizzly', 'server.json');
92
- if (existsSync(serverFile)) {
93
- unlinkSync(serverFile);
94
- }
95
- } catch {
96
- // Non-fatal - cleanup errors shouldn't fail the stop process
97
- }
55
+ await stopServer({
56
+ httpServer: this.httpServer,
57
+ handler: this.handler,
58
+ projectRoot: process.cwd(),
59
+ deps: this.deps
60
+ });
98
61
  }
99
62
 
100
63
  // Expose server interface for compatibility
101
64
  get server() {
102
- return {
103
- getScreenshotCount: buildId => this.handler?.getScreenshotCount?.(buildId) || 0,
104
- finishBuild: buildId => this.httpServer?.finishBuild?.(buildId)
105
- };
65
+ return buildServerInterface({
66
+ handler: this.handler,
67
+ httpServer: this.httpServer
68
+ });
106
69
  }
107
70
 
108
71
  /**
@@ -110,7 +73,9 @@ export class ServerManager {
110
73
  * Only available in TDD mode after tests have run
111
74
  */
112
75
  async getTddResults() {
113
- if (!this.tddMode || !this.handler?.getResults) return null;
114
- return await this.handler.getResults();
76
+ return getTddResults({
77
+ tddMode: this.tddMode,
78
+ handler: this.handler
79
+ });
115
80
  }
116
81
  }
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Static Report Generator using React Reporter
3
3
  * Generates a self-contained HTML file with the React dashboard and embedded data
4
+ *
5
+ * This class is a thin wrapper around the functional report-generator module.
6
+ * For new code, consider using the functions directly from '../report-generator/'.
4
7
  */
5
8
 
6
9
  import { existsSync } from 'node:fs';
7
10
  import { copyFile, mkdir, writeFile } from 'node:fs/promises';
8
11
  import { dirname, join } from 'node:path';
9
12
  import { fileURLToPath } from 'node:url';
13
+ import { buildFallbackHtmlContent, buildHtmlContent, buildReportDir, buildReportPath, generateReport } from '../report-generator/index.js';
10
14
  import * as output from '../utils/output.js';
11
15
  const __filename = fileURLToPath(import.meta.url);
12
16
  const __dirname = dirname(__filename);
@@ -15,8 +19,8 @@ export class StaticReportGenerator {
15
19
  constructor(workingDir, config) {
16
20
  this.workingDir = workingDir;
17
21
  this.config = config;
18
- this.reportDir = join(workingDir, '.vizzly', 'report');
19
- this.reportPath = join(this.reportDir, 'index.html');
22
+ this.reportDir = buildReportDir(workingDir);
23
+ this.reportPath = buildReportPath(this.reportDir);
20
24
  }
21
25
 
22
26
  /**
@@ -25,100 +29,29 @@ export class StaticReportGenerator {
25
29
  * @returns {Promise<string>} Path to generated report
26
30
  */
27
31
  async generateReport(reportData) {
28
- if (!reportData || typeof reportData !== 'object') {
29
- throw new Error('Invalid report data provided');
30
- }
31
- try {
32
- // Ensure report directory exists
33
- await mkdir(this.reportDir, {
34
- recursive: true
35
- });
36
-
37
- // Copy React bundles to report directory
38
- const bundlePath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.iife.js');
39
- const cssPath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.css');
40
- if (!existsSync(bundlePath) || !existsSync(cssPath)) {
41
- throw new Error('Reporter bundles not found. Run "npm run build:reporter" first.');
32
+ return generateReport({
33
+ reportData,
34
+ workingDir: this.workingDir,
35
+ projectRoot: PROJECT_ROOT,
36
+ deps: {
37
+ mkdir,
38
+ existsSync,
39
+ copyFile,
40
+ writeFile,
41
+ output,
42
+ getDate: () => new Date()
42
43
  }
43
-
44
- // Copy bundles to report directory for self-contained report
45
- await copyFile(bundlePath, join(this.reportDir, 'reporter-bundle.js'));
46
- await copyFile(cssPath, join(this.reportDir, 'reporter-bundle.css'));
47
-
48
- // Generate HTML with embedded data
49
- const htmlContent = this.generateHtmlTemplate(reportData);
50
- await writeFile(this.reportPath, htmlContent, 'utf8');
51
- output.debug('report', 'generated static report');
52
- return this.reportPath;
53
- } catch (error) {
54
- output.error(`Failed to generate static report: ${error.message}`);
55
- throw new Error(`Report generation failed: ${error.message}`);
56
- }
44
+ });
57
45
  }
58
46
 
59
47
  /**
60
48
  * Generate HTML template with embedded React app
61
49
  * @param {Object} reportData - Report data to embed
62
50
  * @returns {string} HTML content
51
+ * @deprecated Use buildHtmlContent from report-generator/core.js
63
52
  */
64
53
  generateHtmlTemplate(reportData) {
65
- // Serialize report data safely
66
- const serializedData = JSON.stringify(reportData).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
67
- return `<!DOCTYPE html>
68
- <html lang="en">
69
- <head>
70
- <meta charset="UTF-8">
71
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
72
- <title>Vizzly Dev Report - ${new Date().toLocaleString()}</title>
73
- <link rel="stylesheet" href="./reporter-bundle.css">
74
- <style>
75
- /* Loading spinner styles */
76
- .reporter-loading {
77
- display: flex;
78
- align-items: center;
79
- justify-content: center;
80
- min-height: 100vh;
81
- background: #0f172a;
82
- color: #f59e0b;
83
- }
84
- .spinner {
85
- width: 48px;
86
- height: 48px;
87
- border: 4px solid rgba(245, 158, 11, 0.2);
88
- border-top-color: #f59e0b;
89
- border-radius: 50%;
90
- animation: spin 1s linear infinite;
91
- margin-bottom: 1rem;
92
- }
93
- @keyframes spin {
94
- to { transform: rotate(360deg); }
95
- }
96
- </style>
97
- </head>
98
- <body>
99
- <div id="vizzly-reporter-root">
100
- <div class="reporter-loading">
101
- <div style="text-align: center;">
102
- <div class="spinner"></div>
103
- <p>Loading Vizzly Report...</p>
104
- </div>
105
- </div>
106
- </div>
107
-
108
- <script>
109
- // Embedded report data (static mode)
110
- window.VIZZLY_REPORTER_DATA = ${serializedData};
111
- window.VIZZLY_STATIC_MODE = true;
112
-
113
- // Generate timestamp for "generated at" display
114
- window.VIZZLY_REPORT_GENERATED_AT = "${new Date().toISOString()}";
115
-
116
- console.log('Vizzly Static Report loaded');
117
- console.log('Report data:', window.VIZZLY_REPORTER_DATA?.summary);
118
- </script>
119
- <script src="./reporter-bundle.js"></script>
120
- </body>
121
- </html>`;
54
+ return buildHtmlContent(reportData, new Date());
122
55
  }
123
56
 
124
57
  /**
@@ -127,81 +60,6 @@ export class StaticReportGenerator {
127
60
  * @returns {string} Minimal HTML content
128
61
  */
129
62
  generateFallbackHtml(reportData) {
130
- const summary = reportData.summary || {};
131
- const comparisons = reportData.comparisons || [];
132
- const failed = comparisons.filter(c => c.status === 'failed');
133
- return `<!DOCTYPE html>
134
- <html lang="en">
135
- <head>
136
- <meta charset="UTF-8">
137
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
138
- <title>Vizzly Dev Report</title>
139
- <style>
140
- body {
141
- font-family: system-ui, -apple-system, sans-serif;
142
- background: #0f172a;
143
- color: #e2e8f0;
144
- padding: 2rem;
145
- }
146
- .container { max-width: 1200px; margin: 0 auto; }
147
- .header { text-align: center; margin-bottom: 2rem; }
148
- .summary {
149
- display: flex;
150
- gap: 2rem;
151
- justify-content: center;
152
- margin: 2rem 0;
153
- }
154
- .stat { text-align: center; }
155
- .stat-number {
156
- font-size: 3rem;
157
- font-weight: bold;
158
- display: block;
159
- }
160
- .warning {
161
- background: #fef3c7;
162
- color: #92400e;
163
- padding: 1rem;
164
- border-radius: 0.5rem;
165
- margin: 2rem 0;
166
- }
167
- </style>
168
- </head>
169
- <body>
170
- <div class="container">
171
- <div class="header">
172
- <h1>🐻 Vizzly Dev Report</h1>
173
- <p>Generated: ${new Date().toLocaleString()}</p>
174
- </div>
175
-
176
- <div class="summary">
177
- <div class="stat">
178
- <span class="stat-number">${summary.total || 0}</span>
179
- <span>Total</span>
180
- </div>
181
- <div class="stat">
182
- <span class="stat-number" style="color: #10b981;">${summary.passed || 0}</span>
183
- <span>Passed</span>
184
- </div>
185
- <div class="stat">
186
- <span class="stat-number" style="color: #ef4444;">${summary.failed || 0}</span>
187
- <span>Failed</span>
188
- </div>
189
- </div>
190
-
191
- <div class="warning">
192
- <strong>⚠️ Limited Report</strong>
193
- <p>This is a fallback report. For the full interactive experience, ensure the reporter bundle is built:</p>
194
- <code>npm run build:reporter</code>
195
- </div>
196
-
197
- ${failed.length > 0 ? `
198
- <h2>Failed Comparisons</h2>
199
- <ul>
200
- ${failed.map(c => `<li>${c.name} - ${c.diffPercentage || 0}% difference</li>`).join('')}
201
- </ul>
202
- ` : '<p style="text-align: center; font-size: 1.5rem;">✅ All tests passed!</p>'}
203
- </div>
204
- </body>
205
- </html>`;
63
+ return buildFallbackHtmlContent(reportData, new Date());
206
64
  }
207
65
  }