@vizzly-testing/cli 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +363 -0
  3. package/bin/vizzly.js +3 -0
  4. package/dist/cli.js +104 -0
  5. package/dist/client/index.js +237 -0
  6. package/dist/commands/doctor.js +158 -0
  7. package/dist/commands/init.js +102 -0
  8. package/dist/commands/run.js +224 -0
  9. package/dist/commands/status.js +164 -0
  10. package/dist/commands/tdd.js +212 -0
  11. package/dist/commands/upload.js +181 -0
  12. package/dist/container/index.js +184 -0
  13. package/dist/errors/vizzly-error.js +149 -0
  14. package/dist/index.js +31 -0
  15. package/dist/screenshot-wrapper.js +68 -0
  16. package/dist/sdk/index.js +364 -0
  17. package/dist/server/index.js +522 -0
  18. package/dist/services/api-service.js +215 -0
  19. package/dist/services/base-service.js +154 -0
  20. package/dist/services/build-manager.js +214 -0
  21. package/dist/services/screenshot-server.js +96 -0
  22. package/dist/services/server-manager.js +61 -0
  23. package/dist/services/service-utils.js +171 -0
  24. package/dist/services/tdd-service.js +444 -0
  25. package/dist/services/test-runner.js +210 -0
  26. package/dist/services/uploader.js +413 -0
  27. package/dist/types/cli.d.ts +2 -0
  28. package/dist/types/client/index.d.ts +76 -0
  29. package/dist/types/commands/doctor.d.ts +11 -0
  30. package/dist/types/commands/init.d.ts +14 -0
  31. package/dist/types/commands/run.d.ts +13 -0
  32. package/dist/types/commands/status.d.ts +13 -0
  33. package/dist/types/commands/tdd.d.ts +13 -0
  34. package/dist/types/commands/upload.d.ts +13 -0
  35. package/dist/types/container/index.d.ts +61 -0
  36. package/dist/types/errors/vizzly-error.d.ts +75 -0
  37. package/dist/types/index.d.ts +10 -0
  38. package/dist/types/index.js +153 -0
  39. package/dist/types/screenshot-wrapper.d.ts +27 -0
  40. package/dist/types/sdk/index.d.ts +108 -0
  41. package/dist/types/server/index.d.ts +38 -0
  42. package/dist/types/services/api-service.d.ts +77 -0
  43. package/dist/types/services/base-service.d.ts +72 -0
  44. package/dist/types/services/build-manager.d.ts +68 -0
  45. package/dist/types/services/screenshot-server.d.ts +10 -0
  46. package/dist/types/services/server-manager.d.ts +8 -0
  47. package/dist/types/services/service-utils.d.ts +45 -0
  48. package/dist/types/services/tdd-service.d.ts +55 -0
  49. package/dist/types/services/test-runner.d.ts +25 -0
  50. package/dist/types/services/uploader.d.ts +34 -0
  51. package/dist/types/types/index.d.ts +373 -0
  52. package/dist/types/utils/colors.d.ts +12 -0
  53. package/dist/types/utils/config-helpers.d.ts +6 -0
  54. package/dist/types/utils/config-loader.d.ts +22 -0
  55. package/dist/types/utils/console-ui.d.ts +61 -0
  56. package/dist/types/utils/diagnostics.d.ts +69 -0
  57. package/dist/types/utils/environment-config.d.ts +54 -0
  58. package/dist/types/utils/environment.d.ts +36 -0
  59. package/dist/types/utils/error-messages.d.ts +42 -0
  60. package/dist/types/utils/fetch-utils.d.ts +1 -0
  61. package/dist/types/utils/framework-detector.d.ts +5 -0
  62. package/dist/types/utils/git.d.ts +44 -0
  63. package/dist/types/utils/help.d.ts +11 -0
  64. package/dist/types/utils/image-comparison.d.ts +42 -0
  65. package/dist/types/utils/logger-factory.d.ts +26 -0
  66. package/dist/types/utils/logger.d.ts +79 -0
  67. package/dist/types/utils/package-info.d.ts +15 -0
  68. package/dist/types/utils/package.d.ts +1 -0
  69. package/dist/types/utils/project-detection.d.ts +19 -0
  70. package/dist/types/utils/ui-helpers.d.ts +23 -0
  71. package/dist/utils/colors.js +66 -0
  72. package/dist/utils/config-helpers.js +8 -0
  73. package/dist/utils/config-loader.js +120 -0
  74. package/dist/utils/console-ui.js +226 -0
  75. package/dist/utils/diagnostics.js +184 -0
  76. package/dist/utils/environment-config.js +93 -0
  77. package/dist/utils/environment.js +109 -0
  78. package/dist/utils/error-messages.js +34 -0
  79. package/dist/utils/fetch-utils.js +9 -0
  80. package/dist/utils/framework-detector.js +40 -0
  81. package/dist/utils/git.js +226 -0
  82. package/dist/utils/help.js +66 -0
  83. package/dist/utils/image-comparison.js +172 -0
  84. package/dist/utils/logger-factory.js +76 -0
  85. package/dist/utils/logger.js +231 -0
  86. package/dist/utils/package-info.js +38 -0
  87. package/dist/utils/package.js +9 -0
  88. package/dist/utils/project-detection.js +145 -0
  89. package/dist/utils/ui-helpers.js +86 -0
  90. package/package.json +103 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @module @vizzly-testing/cli/client
3
+ * @description Thin client for test runners - minimal API for taking screenshots
4
+ */
5
+
6
+ import { isVizzlyEnabled, getServerUrl, getBuildId, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
7
+
8
+ // Internal client state
9
+ let currentClient = null;
10
+ let isDisabled = false;
11
+
12
+ /**
13
+ * Check if Vizzly is currently disabled
14
+ * @private
15
+ * @returns {boolean} True if disabled via environment variable or auto-disabled due to failure
16
+ */
17
+ function isVizzlyDisabled() {
18
+ return !isVizzlyEnabled() || isDisabled;
19
+ }
20
+
21
+ /**
22
+ * Disable Vizzly SDK for the current session
23
+ * @private
24
+ * @param {string} [reason] - Optional reason for disabling
25
+ */
26
+ function disableVizzly(reason = 'disabled') {
27
+ isDisabled = true;
28
+ currentClient = null;
29
+ if (reason !== 'disabled') {
30
+ console.warn(`Vizzly SDK disabled due to ${reason}. Screenshots will be skipped for the remainder of this session.`);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get the current client instance
36
+ * @private
37
+ */
38
+ function getClient() {
39
+ if (isVizzlyDisabled()) {
40
+ return null;
41
+ }
42
+ if (!currentClient) {
43
+ // Only try to initialize if VIZZLY_ENABLED is explicitly true
44
+ const serverUrl = getServerUrl();
45
+ if (serverUrl && isVizzlyEnabled()) {
46
+ currentClient = createSimpleClient(serverUrl);
47
+ }
48
+ }
49
+ return currentClient;
50
+ }
51
+
52
+ /**
53
+ * Create a simple HTTP client for screenshots
54
+ * @private
55
+ */
56
+ function createSimpleClient(serverUrl) {
57
+ return {
58
+ async screenshot(name, imageBuffer, options = {}) {
59
+ try {
60
+ const response = await fetch(`${serverUrl}/screenshot`, {
61
+ method: 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json'
64
+ },
65
+ body: JSON.stringify({
66
+ buildId: getBuildId(),
67
+ name,
68
+ image: imageBuffer.toString('base64'),
69
+ properties: options.properties || {},
70
+ threshold: options.threshold || 0,
71
+ variant: options.variant,
72
+ fullPage: options.fullPage || false
73
+ })
74
+ });
75
+ if (!response.ok) {
76
+ const errorData = await response.json().catch(async () => {
77
+ const errorText = await response.text().catch(() => 'Unknown error');
78
+ return {
79
+ error: errorText
80
+ };
81
+ });
82
+
83
+ // In TDD mode, if we get 422 (visual difference), throw with clean message
84
+ if (response.status === 422 && errorData.tddMode && errorData.comparison) {
85
+ const comp = errorData.comparison;
86
+ throw new Error(`Visual difference detected in "${name}"\n` + ` Baseline: ${comp.baseline}\n` + ` Current: ${comp.current}\n` + ` Diff: ${comp.diff}`);
87
+ }
88
+ throw new Error(`Screenshot failed: ${response.status} ${response.statusText} - ${errorData.error || 'Unknown error'}`);
89
+ }
90
+ return await response.json();
91
+ } catch (error) {
92
+ // In TDD mode with visual differences, throw the error to fail the test
93
+ if (error.message.includes('Visual difference detected')) {
94
+ // Clean output for TDD mode - don't spam with additional logs
95
+ throw error;
96
+ }
97
+ console.error(`Failed to save screenshot "${name}":`, error.message);
98
+ console.error(`Vizzly screenshot failed for ${name}: ${error.message}`);
99
+ if (error.message.includes('fetch') || error.code === 'ECONNREFUSED') {
100
+ console.error(`Server URL: ${serverUrl}/screenshot`);
101
+ console.error('This usually means the Vizzly server is not running or not accessible');
102
+ console.error('Check that the server is started and the port is correct');
103
+ } else if (error.message.includes('404') || error.message.includes('Not Found')) {
104
+ console.error(`Server URL: ${serverUrl}/screenshot`);
105
+ console.error('The screenshot endpoint was not found - check server configuration');
106
+ }
107
+
108
+ // Disable the SDK after first failure to prevent spam
109
+ disableVizzly('failure');
110
+
111
+ // Don't throw - just return silently to not break tests (except TDD mode)
112
+ return null;
113
+ }
114
+ },
115
+ async flush() {
116
+ // Simple client doesn't need explicit flushing
117
+ return Promise.resolve();
118
+ }
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Take a screenshot for visual regression testing
124
+ *
125
+ * @param {string} name - Unique name for the screenshot
126
+ * @param {Buffer} imageBuffer - PNG image data as a Buffer
127
+ * @param {Object} [options] - Optional configuration
128
+ * @param {Record<string, any>} [options.properties] - Additional properties to attach to the screenshot
129
+ * @param {number} [options.threshold=0] - Pixel difference threshold (0-100)
130
+ * @param {string} [options.variant] - Variant name for organizing screenshots
131
+ * @param {boolean} [options.fullPage=false] - Whether this is a full page screenshot
132
+ *
133
+ * @returns {Promise<void>}
134
+ *
135
+ * @example
136
+ * // Basic usage
137
+ * import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
138
+ *
139
+ * const screenshot = await page.screenshot();
140
+ * await vizzlyScreenshot('homepage', screenshot);
141
+ *
142
+ * @example
143
+ * // With properties and threshold
144
+ * await vizzlyScreenshot('checkout-form', screenshot, {
145
+ * properties: {
146
+ * browser: 'chrome',
147
+ * viewport: '1920x1080'
148
+ * },
149
+ * threshold: 5
150
+ * });
151
+ *
152
+ * @throws {VizzlyError} When screenshot capture fails or client is not initialized
153
+ */
154
+ export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
155
+ if (isVizzlyDisabled()) {
156
+ return; // Silently skip when disabled
157
+ }
158
+ const client = getClient();
159
+ if (!client) {
160
+ console.warn('Vizzly client not initialized. Screenshots will be skipped.');
161
+ return;
162
+ }
163
+ return client.screenshot(name, imageBuffer, options);
164
+ }
165
+
166
+ /**
167
+ * Wait for all queued screenshots to be processed
168
+ *
169
+ * @returns {Promise<void>}
170
+ *
171
+ * @example
172
+ * afterAll(async () => {
173
+ * await vizzlyFlush();
174
+ * });
175
+ */
176
+ export async function vizzlyFlush() {
177
+ const client = getClient();
178
+ if (client) {
179
+ return client.flush();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Check if the Vizzly client is initialized and ready
185
+ *
186
+ * @returns {boolean} True if client is ready, false otherwise
187
+ */
188
+ export function isVizzlyReady() {
189
+ return !isVizzlyDisabled() && getClient() !== null;
190
+ }
191
+
192
+ /**
193
+ * Configure the client with custom settings
194
+ *
195
+ * @param {Object} config - Configuration options
196
+ * @param {string} [config.serverUrl] - Server URL override
197
+ * @param {boolean} [config.enabled] - Enable/disable screenshots
198
+ */
199
+ export function configure(config = {}) {
200
+ if (config.serverUrl) {
201
+ currentClient = createSimpleClient(config.serverUrl);
202
+ }
203
+ if (typeof config.enabled === 'boolean') {
204
+ setVizzlyEnabled(config.enabled);
205
+ if (!config.enabled) {
206
+ disableVizzly();
207
+ } else {
208
+ isDisabled = false;
209
+ }
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Enable or disable screenshot capture
215
+ * @param {boolean} enabled - Whether to enable screenshots
216
+ */
217
+ export function setEnabled(enabled) {
218
+ configure({
219
+ enabled
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Get information about Vizzly client state
225
+ * @returns {Object} Client information
226
+ */
227
+ export function getVizzlyInfo() {
228
+ const client = getClient();
229
+ return {
230
+ enabled: !isVizzlyDisabled(),
231
+ serverUrl: getServerUrl(),
232
+ ready: !isVizzlyDisabled() && client !== null,
233
+ buildId: getBuildId(),
234
+ tddMode: isTddMode(),
235
+ disabled: isVizzlyDisabled()
236
+ };
237
+ }
@@ -0,0 +1,158 @@
1
+ import { URL } from 'url';
2
+ import { loadConfig } from '../utils/config-loader.js';
3
+ import { ConsoleUI } from '../utils/console-ui.js';
4
+ import { ApiService } from '../services/api-service.js';
5
+ import { ConfigError } from '../errors/vizzly-error.js';
6
+ import { getApiToken } from '../utils/environment-config.js';
7
+
8
+ /**
9
+ * Doctor command implementation - Run diagnostics to check environment
10
+ * @param {Object} options - Command options
11
+ * @param {Object} globalOptions - Global CLI options
12
+ */
13
+ export async function doctorCommand(options = {}, globalOptions = {}) {
14
+ // Create UI handler
15
+ const ui = new ConsoleUI({
16
+ json: globalOptions.json,
17
+ verbose: globalOptions.verbose,
18
+ color: !globalOptions.noColor
19
+ });
20
+
21
+ // Note: ConsoleUI handles cleanup via global process listeners
22
+
23
+ const diagnostics = {
24
+ environment: {
25
+ nodeVersion: null,
26
+ nodeVersionValid: null
27
+ },
28
+ configuration: {
29
+ apiUrl: null,
30
+ apiUrlValid: null,
31
+ threshold: null,
32
+ thresholdValid: null,
33
+ port: null
34
+ },
35
+ connectivity: {
36
+ checked: false,
37
+ ok: null,
38
+ error: null
39
+ }
40
+ };
41
+ let hasErrors = false;
42
+ try {
43
+ // Determine if we'll attempt remote checks (API connectivity)
44
+ const willCheckConnectivity = Boolean(options.api || getApiToken());
45
+
46
+ // Announce preflight, indicating local-only when no token/connectivity is planned
47
+ ui.info(`Running Vizzly preflight${willCheckConnectivity ? '' : ' (local checks only)'}...`);
48
+
49
+ // Node.js version check (require >= 20)
50
+ const nodeVersion = process.version;
51
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
52
+ diagnostics.environment.nodeVersion = nodeVersion;
53
+ diagnostics.environment.nodeVersionValid = nodeMajor >= 20;
54
+ if (nodeMajor >= 20) {
55
+ ui.success(`Node.js version: ${nodeVersion} (supported)`);
56
+ } else {
57
+ hasErrors = true;
58
+ ui.error('Node.js version must be >= 20', {}, 0);
59
+ }
60
+
61
+ // Load configuration (apply global CLI overrides like --config only)
62
+ const config = await loadConfig(globalOptions.config);
63
+
64
+ // Validate apiUrl
65
+ diagnostics.configuration.apiUrl = config.apiUrl;
66
+ try {
67
+ const url = new URL(config.apiUrl);
68
+ if (!['http:', 'https:'].includes(url.protocol)) {
69
+ throw new ConfigError('URL must use http or https');
70
+ }
71
+ diagnostics.configuration.apiUrlValid = true;
72
+ ui.success(`API URL: ${config.apiUrl}`);
73
+ } catch (e) {
74
+ diagnostics.configuration.apiUrlValid = false;
75
+ hasErrors = true;
76
+ ui.error('Invalid apiUrl in configuration (set VIZZLY_API_URL or config file)', e, 0);
77
+ }
78
+
79
+ // Validate threshold (0..1 inclusive)
80
+ const threshold = Number(config?.comparison?.threshold);
81
+ diagnostics.configuration.threshold = threshold;
82
+ const thresholdValid = Number.isFinite(threshold) && threshold >= 0 && threshold <= 1;
83
+ diagnostics.configuration.thresholdValid = thresholdValid;
84
+ if (thresholdValid) {
85
+ ui.success(`Threshold: ${threshold}`);
86
+ } else {
87
+ hasErrors = true;
88
+ ui.error('Invalid threshold (expected number between 0 and 1)', {}, 0);
89
+ }
90
+
91
+ // Report effective port without binding
92
+ const port = config?.server?.port ?? 47392;
93
+ diagnostics.configuration.port = port;
94
+ ui.info(`Effective port: ${port}`);
95
+
96
+ // Optional: API connectivity check when --api is provided or VIZZLY_TOKEN is present
97
+ const autoApi = Boolean(getApiToken());
98
+ if (options.api || autoApi) {
99
+ diagnostics.connectivity.checked = true;
100
+ if (!config.apiKey) {
101
+ diagnostics.connectivity.ok = false;
102
+ diagnostics.connectivity.error = 'Missing API token (VIZZLY_TOKEN)';
103
+ hasErrors = true;
104
+ ui.error('Missing API token for connectivity check', {}, 0);
105
+ } else {
106
+ ui.progress('Checking API connectivity...');
107
+ try {
108
+ const api = new ApiService({
109
+ baseUrl: config.apiUrl,
110
+ token: config.apiKey,
111
+ command: 'doctor'
112
+ });
113
+ // Minimal, read-only call
114
+ await api.getBuilds({
115
+ limit: 1
116
+ });
117
+ diagnostics.connectivity.ok = true;
118
+ ui.success('API connectivity OK');
119
+ } catch (err) {
120
+ diagnostics.connectivity.ok = false;
121
+ diagnostics.connectivity.error = err?.message || String(err);
122
+ hasErrors = true;
123
+ ui.error('API connectivity failed', err, 0);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Summary
129
+ if (hasErrors) {
130
+ ui.warning('Preflight completed with issues.');
131
+ } else {
132
+ ui.success('Preflight passed.');
133
+ }
134
+
135
+ // Emit structured data in json/verbose modes
136
+ if (globalOptions.json || globalOptions.verbose) {
137
+ ui.data({
138
+ passed: !hasErrors,
139
+ diagnostics,
140
+ timestamp: new Date().toISOString()
141
+ });
142
+ }
143
+ } catch (error) {
144
+ hasErrors = true;
145
+ ui.error('Failed to run preflight', error, 0);
146
+ } finally {
147
+ ui.cleanup();
148
+ if (hasErrors) process.exit(1);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Validate doctor options (no specific validation needed)
154
+ * @param {Object} options - Command options
155
+ */
156
+ export function validateDoctorOptions() {
157
+ return []; // No specific validation for now
158
+ }
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { VizzlyError } from '../errors/vizzly-error.js';
5
+ import { createComponentLogger } from '../utils/logger-factory.js';
6
+
7
+ /**
8
+ * Simple configuration setup for Vizzly CLI
9
+ */
10
+ export class InitCommand {
11
+ constructor(logger) {
12
+ this.logger = logger || createComponentLogger('INIT', {
13
+ level: 'info'
14
+ });
15
+ }
16
+ async run(options = {}) {
17
+ this.logger.info('šŸŽÆ Initializing Vizzly configuration...\n');
18
+ try {
19
+ // Check for existing config
20
+ const configPath = path.join(process.cwd(), 'vizzly.config.js');
21
+ const hasConfig = await this.fileExists(configPath);
22
+ if (hasConfig && !options.force) {
23
+ this.logger.info('āŒ A vizzly.config.js file already exists. Use --force to overwrite.');
24
+ return;
25
+ }
26
+
27
+ // Generate config file with defaults
28
+ await this.generateConfigFile(configPath);
29
+
30
+ // Show next steps
31
+ this.showNextSteps();
32
+ this.logger.info('\nāœ… Vizzly CLI setup complete!');
33
+ } catch (error) {
34
+ throw new VizzlyError('Failed to initialize Vizzly configuration', 'INIT_FAILED', {
35
+ error: error.message
36
+ });
37
+ }
38
+ }
39
+ async generateConfigFile(configPath) {
40
+ const configContent = `export default {
41
+ // API configuration
42
+ // Set VIZZLY_TOKEN environment variable or uncomment and set here:
43
+ // apiToken: 'your-token-here',
44
+
45
+ // Screenshot configuration
46
+ screenshots: {
47
+ directory: './screenshots',
48
+ formats: ['png']
49
+ },
50
+
51
+ // Server configuration
52
+ server: {
53
+ port: 47392,
54
+ screenshotPath: '/screenshot'
55
+ },
56
+
57
+ // Comparison configuration
58
+ comparison: {
59
+ threshold: 0.1,
60
+ ignoreAntialiasing: true
61
+ },
62
+
63
+ // Upload configuration
64
+ upload: {
65
+ concurrency: 5,
66
+ timeout: 30000
67
+ }
68
+ };
69
+ `;
70
+ await fs.writeFile(configPath, configContent, 'utf8');
71
+ this.logger.info(`šŸ“„ Created vizzly.config.js`);
72
+ }
73
+ showNextSteps() {
74
+ this.logger.info('\nšŸ“š Next steps:');
75
+ this.logger.info(' 1. Set your API token:');
76
+ this.logger.info(' export VIZZLY_TOKEN="your-api-key"');
77
+ this.logger.info(' 2. Run your tests with Vizzly:');
78
+ this.logger.info(' npx vizzly run "npm test"');
79
+ this.logger.info(' 3. Upload screenshots:');
80
+ this.logger.info(' npx vizzly upload ./screenshots');
81
+ }
82
+ async fileExists(filePath) {
83
+ try {
84
+ await fs.access(filePath);
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ }
91
+
92
+ // Export factory function for CLI
93
+ export function createInitCommand(options) {
94
+ const command = new InitCommand(options.logger);
95
+ return () => command.run(options);
96
+ }
97
+
98
+ // Simple export for direct CLI usage
99
+ export async function init(options = {}) {
100
+ const command = new InitCommand();
101
+ return await command.run(options);
102
+ }