@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.
- package/LICENSE +21 -0
- package/README.md +363 -0
- package/bin/vizzly.js +3 -0
- package/dist/cli.js +104 -0
- package/dist/client/index.js +237 -0
- package/dist/commands/doctor.js +158 -0
- package/dist/commands/init.js +102 -0
- package/dist/commands/run.js +224 -0
- package/dist/commands/status.js +164 -0
- package/dist/commands/tdd.js +212 -0
- package/dist/commands/upload.js +181 -0
- package/dist/container/index.js +184 -0
- package/dist/errors/vizzly-error.js +149 -0
- package/dist/index.js +31 -0
- package/dist/screenshot-wrapper.js +68 -0
- package/dist/sdk/index.js +364 -0
- package/dist/server/index.js +522 -0
- package/dist/services/api-service.js +215 -0
- package/dist/services/base-service.js +154 -0
- package/dist/services/build-manager.js +214 -0
- package/dist/services/screenshot-server.js +96 -0
- package/dist/services/server-manager.js +61 -0
- package/dist/services/service-utils.js +171 -0
- package/dist/services/tdd-service.js +444 -0
- package/dist/services/test-runner.js +210 -0
- package/dist/services/uploader.js +413 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/client/index.d.ts +76 -0
- package/dist/types/commands/doctor.d.ts +11 -0
- package/dist/types/commands/init.d.ts +14 -0
- package/dist/types/commands/run.d.ts +13 -0
- package/dist/types/commands/status.d.ts +13 -0
- package/dist/types/commands/tdd.d.ts +13 -0
- package/dist/types/commands/upload.d.ts +13 -0
- package/dist/types/container/index.d.ts +61 -0
- package/dist/types/errors/vizzly-error.d.ts +75 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +153 -0
- package/dist/types/screenshot-wrapper.d.ts +27 -0
- package/dist/types/sdk/index.d.ts +108 -0
- package/dist/types/server/index.d.ts +38 -0
- package/dist/types/services/api-service.d.ts +77 -0
- package/dist/types/services/base-service.d.ts +72 -0
- package/dist/types/services/build-manager.d.ts +68 -0
- package/dist/types/services/screenshot-server.d.ts +10 -0
- package/dist/types/services/server-manager.d.ts +8 -0
- package/dist/types/services/service-utils.d.ts +45 -0
- package/dist/types/services/tdd-service.d.ts +55 -0
- package/dist/types/services/test-runner.d.ts +25 -0
- package/dist/types/services/uploader.d.ts +34 -0
- package/dist/types/types/index.d.ts +373 -0
- package/dist/types/utils/colors.d.ts +12 -0
- package/dist/types/utils/config-helpers.d.ts +6 -0
- package/dist/types/utils/config-loader.d.ts +22 -0
- package/dist/types/utils/console-ui.d.ts +61 -0
- package/dist/types/utils/diagnostics.d.ts +69 -0
- package/dist/types/utils/environment-config.d.ts +54 -0
- package/dist/types/utils/environment.d.ts +36 -0
- package/dist/types/utils/error-messages.d.ts +42 -0
- package/dist/types/utils/fetch-utils.d.ts +1 -0
- package/dist/types/utils/framework-detector.d.ts +5 -0
- package/dist/types/utils/git.d.ts +44 -0
- package/dist/types/utils/help.d.ts +11 -0
- package/dist/types/utils/image-comparison.d.ts +42 -0
- package/dist/types/utils/logger-factory.d.ts +26 -0
- package/dist/types/utils/logger.d.ts +79 -0
- package/dist/types/utils/package-info.d.ts +15 -0
- package/dist/types/utils/package.d.ts +1 -0
- package/dist/types/utils/project-detection.d.ts +19 -0
- package/dist/types/utils/ui-helpers.d.ts +23 -0
- package/dist/utils/colors.js +66 -0
- package/dist/utils/config-helpers.js +8 -0
- package/dist/utils/config-loader.js +120 -0
- package/dist/utils/console-ui.js +226 -0
- package/dist/utils/diagnostics.js +184 -0
- package/dist/utils/environment-config.js +93 -0
- package/dist/utils/environment.js +109 -0
- package/dist/utils/error-messages.js +34 -0
- package/dist/utils/fetch-utils.js +9 -0
- package/dist/utils/framework-detector.js +40 -0
- package/dist/utils/git.js +226 -0
- package/dist/utils/help.js +66 -0
- package/dist/utils/image-comparison.js +172 -0
- package/dist/utils/logger-factory.js +76 -0
- package/dist/utils/logger.js +231 -0
- package/dist/utils/package-info.js +38 -0
- package/dist/utils/package.js +9 -0
- package/dist/utils/project-detection.js +145 -0
- package/dist/utils/ui-helpers.js +86 -0
- package/package.json +103 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
2
|
+
import { ConsoleUI } from '../utils/console-ui.js';
|
|
3
|
+
import { createServiceContainer } from '../container/index.js';
|
|
4
|
+
import { detectBranch, detectCommit, getCommitMessage, generateBuildNameWithGit } from '../utils/git.js';
|
|
5
|
+
import { ApiService } from '../services/api-service.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Construct proper build URL with org/project context
|
|
9
|
+
* @param {string} buildId - Build ID
|
|
10
|
+
* @param {string} apiUrl - API base URL
|
|
11
|
+
* @param {string} apiToken - API token
|
|
12
|
+
* @returns {Promise<string>} Proper build URL
|
|
13
|
+
*/
|
|
14
|
+
async function constructBuildUrl(buildId, apiUrl, apiToken) {
|
|
15
|
+
try {
|
|
16
|
+
const apiService = new ApiService({
|
|
17
|
+
baseUrl: apiUrl,
|
|
18
|
+
token: apiToken,
|
|
19
|
+
command: 'upload'
|
|
20
|
+
});
|
|
21
|
+
const tokenContext = await apiService.getTokenContext();
|
|
22
|
+
const baseUrl = apiUrl.replace(/\/api.*$/, '');
|
|
23
|
+
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
24
|
+
return `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${buildId}`;
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// Fall back to simple URL if context fetch fails
|
|
28
|
+
console.debug('Failed to fetch token context, using fallback URL:', error.message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fallback URL construction
|
|
32
|
+
const baseUrl = apiUrl.replace(/\/api.*$/, '');
|
|
33
|
+
return `${baseUrl}/builds/${buildId}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Upload command implementation
|
|
38
|
+
* @param {string} screenshotsPath - Path to screenshots
|
|
39
|
+
* @param {Object} options - Command options
|
|
40
|
+
* @param {Object} globalOptions - Global CLI options
|
|
41
|
+
*/
|
|
42
|
+
export async function uploadCommand(screenshotsPath, options = {}, globalOptions = {}) {
|
|
43
|
+
// Create UI handler
|
|
44
|
+
const ui = new ConsoleUI({
|
|
45
|
+
json: globalOptions.json,
|
|
46
|
+
verbose: globalOptions.verbose,
|
|
47
|
+
color: !globalOptions.noColor
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Note: ConsoleUI handles cleanup via global process listeners
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
ui.info('Starting upload process...');
|
|
54
|
+
|
|
55
|
+
// Load configuration with CLI overrides
|
|
56
|
+
const allOptions = {
|
|
57
|
+
...globalOptions,
|
|
58
|
+
...options
|
|
59
|
+
};
|
|
60
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
61
|
+
|
|
62
|
+
// Validate API token
|
|
63
|
+
if (!config.apiKey) {
|
|
64
|
+
ui.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
|
|
65
|
+
return; // Won't reach here due to process.exit in error()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Collect git metadata if not provided
|
|
69
|
+
const branch = await detectBranch(options.branch);
|
|
70
|
+
const commit = await detectCommit(options.commit);
|
|
71
|
+
const message = options.message || (await getCommitMessage());
|
|
72
|
+
const buildName = await generateBuildNameWithGit(options.buildName);
|
|
73
|
+
ui.info(`Uploading screenshots from: ${screenshotsPath}`);
|
|
74
|
+
if (globalOptions.verbose) {
|
|
75
|
+
ui.info('Configuration loaded', {
|
|
76
|
+
branch,
|
|
77
|
+
commit: commit?.substring(0, 7),
|
|
78
|
+
environment: config.build.environment,
|
|
79
|
+
buildName: config.build.name
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get uploader service
|
|
84
|
+
ui.startSpinner('Initializing uploader...');
|
|
85
|
+
const container = await createServiceContainer(config, 'upload');
|
|
86
|
+
const uploader = await container.get('uploader');
|
|
87
|
+
|
|
88
|
+
// Prepare upload options with progress callback
|
|
89
|
+
const uploadOptions = {
|
|
90
|
+
screenshotsDir: screenshotsPath,
|
|
91
|
+
buildName,
|
|
92
|
+
branch,
|
|
93
|
+
commit,
|
|
94
|
+
message,
|
|
95
|
+
environment: config.build.environment,
|
|
96
|
+
threshold: config.comparison.threshold,
|
|
97
|
+
metadata: options.metadata ? JSON.parse(options.metadata) : {},
|
|
98
|
+
onProgress: progressData => {
|
|
99
|
+
const {
|
|
100
|
+
message: progressMessage,
|
|
101
|
+
current,
|
|
102
|
+
total,
|
|
103
|
+
phase
|
|
104
|
+
} = progressData;
|
|
105
|
+
ui.progress(progressMessage || `${phase || 'Processing'}: ${current || 0}/${total || 0}`, current, total);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Start upload
|
|
110
|
+
ui.progress('Starting upload...');
|
|
111
|
+
const result = await uploader.upload(uploadOptions);
|
|
112
|
+
ui.success('Upload completed successfully');
|
|
113
|
+
|
|
114
|
+
// Show Vizzly summary
|
|
115
|
+
if (result.buildId) {
|
|
116
|
+
ui.info(`🐻 Vizzly: Uploaded ${result.stats.uploaded} of ${result.stats.total} screenshots to build ${result.buildId}`);
|
|
117
|
+
// Use API-provided URL or construct proper URL with org/project context
|
|
118
|
+
const buildUrl = result.url || (await constructBuildUrl(result.buildId, config.apiUrl, config.apiKey));
|
|
119
|
+
ui.info(`🔗 Vizzly: View results at ${buildUrl}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Wait for build completion if requested
|
|
123
|
+
if (options.wait && result.buildId) {
|
|
124
|
+
ui.info('Waiting for build completion...');
|
|
125
|
+
ui.startSpinner('Processing comparisons...');
|
|
126
|
+
const buildResult = await uploader.waitForBuild(result.buildId);
|
|
127
|
+
ui.success('Build processing completed');
|
|
128
|
+
|
|
129
|
+
// Show build processing results
|
|
130
|
+
if (buildResult.failedComparisons > 0) {
|
|
131
|
+
ui.warning(`${buildResult.failedComparisons} visual comparisons failed`);
|
|
132
|
+
} else {
|
|
133
|
+
ui.success(`All ${buildResult.passedComparisons} visual comparisons passed`);
|
|
134
|
+
}
|
|
135
|
+
// Use API-provided URL or construct proper URL with org/project context
|
|
136
|
+
const buildUrl = buildResult.url || (await constructBuildUrl(result.buildId, config.apiUrl, config.apiKey));
|
|
137
|
+
ui.info(`🔗 Vizzly: View results at ${buildUrl}`);
|
|
138
|
+
}
|
|
139
|
+
ui.cleanup();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
ui.error('Upload failed', error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validate upload options
|
|
147
|
+
* @param {string} screenshotsPath - Path to screenshots
|
|
148
|
+
* @param {Object} options - Command options
|
|
149
|
+
*/
|
|
150
|
+
export function validateUploadOptions(screenshotsPath, options) {
|
|
151
|
+
const errors = [];
|
|
152
|
+
if (!screenshotsPath) {
|
|
153
|
+
errors.push('Screenshots path is required');
|
|
154
|
+
}
|
|
155
|
+
if (options.metadata) {
|
|
156
|
+
try {
|
|
157
|
+
JSON.parse(options.metadata);
|
|
158
|
+
} catch {
|
|
159
|
+
errors.push('Invalid JSON in --metadata option');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (options.threshold !== undefined) {
|
|
163
|
+
const threshold = parseFloat(options.threshold);
|
|
164
|
+
if (isNaN(threshold) || threshold < 0 || threshold > 1) {
|
|
165
|
+
errors.push('Threshold must be a number between 0 and 1');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (options.batchSize !== undefined) {
|
|
169
|
+
const n = parseInt(options.batchSize, 10);
|
|
170
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
171
|
+
errors.push('Batch size must be a positive integer');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (options.uploadTimeout !== undefined) {
|
|
175
|
+
const n = parseInt(options.uploadTimeout, 10);
|
|
176
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
177
|
+
errors.push('Upload timeout must be a positive integer (milliseconds)');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return errors;
|
|
181
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} ServiceDefinition
|
|
6
|
+
* @property {Function} factory - Factory function to create service instance
|
|
7
|
+
* @property {boolean} [singleton=true] - Whether to cache the instance
|
|
8
|
+
* @property {string[]} [dependencies=[]] - Array of dependency names
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Service container for dependency injection and lifecycle management
|
|
13
|
+
*/
|
|
14
|
+
export class ServiceContainer extends EventEmitter {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.services = new Map();
|
|
18
|
+
this.instances = new Map();
|
|
19
|
+
this.starting = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a service
|
|
24
|
+
* @param {string} name - Service name
|
|
25
|
+
* @param {Function|ServiceDefinition} factoryOrDefinition - Factory function or service definition
|
|
26
|
+
*/
|
|
27
|
+
register(name, factoryOrDefinition) {
|
|
28
|
+
const definition = typeof factoryOrDefinition === 'function' ? {
|
|
29
|
+
factory: factoryOrDefinition,
|
|
30
|
+
singleton: true,
|
|
31
|
+
dependencies: []
|
|
32
|
+
} : factoryOrDefinition;
|
|
33
|
+
this.services.set(name, definition);
|
|
34
|
+
this.emit('service:registered', {
|
|
35
|
+
name,
|
|
36
|
+
definition
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a service instance
|
|
42
|
+
* @param {string} name - Service name
|
|
43
|
+
* @returns {Promise<any>} Service instance
|
|
44
|
+
*/
|
|
45
|
+
async get(name) {
|
|
46
|
+
if (!this.services.has(name)) {
|
|
47
|
+
throw new VizzlyError(`Service '${name}' not registered`, 'SERVICE_NOT_FOUND', {
|
|
48
|
+
name
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const definition = this.services.get(name);
|
|
52
|
+
|
|
53
|
+
// Return cached instance for singletons
|
|
54
|
+
if (definition.singleton && this.instances.has(name)) {
|
|
55
|
+
return this.instances.get(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Prevent circular dependencies during startup
|
|
59
|
+
if (this.starting.has(name)) {
|
|
60
|
+
throw new VizzlyError(`Circular dependency detected for service '${name}'`, 'CIRCULAR_DEPENDENCY', {
|
|
61
|
+
name
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
this.starting.set(name, true);
|
|
66
|
+
|
|
67
|
+
// Resolve dependencies
|
|
68
|
+
const deps = await Promise.all((definition.dependencies || []).map(dep => this.get(dep)));
|
|
69
|
+
|
|
70
|
+
// Create instance
|
|
71
|
+
const instance = await definition.factory(...deps);
|
|
72
|
+
|
|
73
|
+
// Cache singleton instances
|
|
74
|
+
if (definition.singleton) {
|
|
75
|
+
this.instances.set(name, instance);
|
|
76
|
+
}
|
|
77
|
+
this.emit('service:created', {
|
|
78
|
+
name,
|
|
79
|
+
instance
|
|
80
|
+
});
|
|
81
|
+
return instance;
|
|
82
|
+
} finally {
|
|
83
|
+
this.starting.delete(name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Start all registered services
|
|
89
|
+
*/
|
|
90
|
+
async startAll() {
|
|
91
|
+
const services = Array.from(this.services.keys());
|
|
92
|
+
for (const name of services) {
|
|
93
|
+
const instance = await this.get(name);
|
|
94
|
+
if (instance && typeof instance.start === 'function') {
|
|
95
|
+
await instance.start();
|
|
96
|
+
this.emit('service:started', {
|
|
97
|
+
name,
|
|
98
|
+
instance
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stop all services in reverse order
|
|
106
|
+
*/
|
|
107
|
+
async stopAll() {
|
|
108
|
+
const instances = Array.from(this.instances.entries()).reverse();
|
|
109
|
+
for (const [name, instance] of instances) {
|
|
110
|
+
if (instance && typeof instance.stop === 'function') {
|
|
111
|
+
await instance.stop();
|
|
112
|
+
this.emit('service:stopped', {
|
|
113
|
+
name,
|
|
114
|
+
instance
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Clear all services and instances
|
|
122
|
+
*/
|
|
123
|
+
clear() {
|
|
124
|
+
this.services.clear();
|
|
125
|
+
this.instances.clear();
|
|
126
|
+
this.starting.clear();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Export singleton instance
|
|
131
|
+
export const container = new ServiceContainer();
|
|
132
|
+
/**
|
|
133
|
+
* Create a configured service container
|
|
134
|
+
* @param {Object} config - Configuration object
|
|
135
|
+
* @returns {ServiceContainer}
|
|
136
|
+
*/
|
|
137
|
+
export async function createServiceContainer(config, command = 'run') {
|
|
138
|
+
const container = new ServiceContainer();
|
|
139
|
+
|
|
140
|
+
// Dynamic ESM imports to avoid circular deps
|
|
141
|
+
const [{
|
|
142
|
+
createComponentLogger
|
|
143
|
+
}, {
|
|
144
|
+
ApiService
|
|
145
|
+
}, {
|
|
146
|
+
createUploader
|
|
147
|
+
}, {
|
|
148
|
+
createTDDService
|
|
149
|
+
}, {
|
|
150
|
+
TestRunner
|
|
151
|
+
}, {
|
|
152
|
+
BuildManager
|
|
153
|
+
}, {
|
|
154
|
+
ServerManager
|
|
155
|
+
}] = await Promise.all([import('../utils/logger-factory.js'), import('../services/api-service.js'), import('../services/uploader.js'), import('../services/tdd-service.js'), import('../services/test-runner.js'), import('../services/build-manager.js'), import('../services/server-manager.js')]);
|
|
156
|
+
|
|
157
|
+
// Create logger instance once
|
|
158
|
+
const logger = createComponentLogger('CONTAINER', {
|
|
159
|
+
level: config.logLevel || (config.verbose ? 'debug' : 'warn'),
|
|
160
|
+
verbose: config.verbose || false
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Register services without circular dependencies
|
|
164
|
+
container.register('logger', () => logger);
|
|
165
|
+
container.register('apiService', () => new ApiService(config, {
|
|
166
|
+
logger
|
|
167
|
+
}));
|
|
168
|
+
container.register('uploader', () => createUploader({
|
|
169
|
+
...config,
|
|
170
|
+
command
|
|
171
|
+
}, {
|
|
172
|
+
logger
|
|
173
|
+
}));
|
|
174
|
+
container.register('buildManager', () => new BuildManager(config, logger));
|
|
175
|
+
container.register('serverManager', () => new ServerManager(config, logger));
|
|
176
|
+
container.register('tddService', () => createTDDService(config, {
|
|
177
|
+
logger
|
|
178
|
+
}));
|
|
179
|
+
container.register('testRunner', {
|
|
180
|
+
factory: async (buildManager, serverManager, tddService) => new TestRunner(config, logger, buildManager, serverManager, tddService),
|
|
181
|
+
dependencies: ['buildManager', 'serverManager', 'tddService']
|
|
182
|
+
});
|
|
183
|
+
return container;
|
|
184
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all Vizzly errors
|
|
3
|
+
* Provides consistent error structure and helpful debugging information
|
|
4
|
+
*/
|
|
5
|
+
export class VizzlyError extends Error {
|
|
6
|
+
constructor(message, code = 'VIZZLY_ERROR', context = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'VizzlyError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.timestamp = new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get a user-friendly error message
|
|
16
|
+
*/
|
|
17
|
+
getUserMessage() {
|
|
18
|
+
return this.message;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get error details for logging
|
|
23
|
+
*/
|
|
24
|
+
toJSON() {
|
|
25
|
+
return {
|
|
26
|
+
name: this.name,
|
|
27
|
+
code: this.code,
|
|
28
|
+
message: this.message,
|
|
29
|
+
context: this.context,
|
|
30
|
+
timestamp: this.timestamp,
|
|
31
|
+
stack: this.stack
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configuration-related errors
|
|
38
|
+
*/
|
|
39
|
+
export class ConfigError extends VizzlyError {
|
|
40
|
+
constructor(message, context = {}) {
|
|
41
|
+
super(message, 'CONFIG_ERROR', context);
|
|
42
|
+
this.name = 'ConfigError';
|
|
43
|
+
}
|
|
44
|
+
getUserMessage() {
|
|
45
|
+
return `Configuration error: ${this.message}. Please check your vizzly.config.js file.`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Authentication/authorization errors
|
|
51
|
+
*/
|
|
52
|
+
export class AuthError extends VizzlyError {
|
|
53
|
+
constructor(message, context = {}) {
|
|
54
|
+
super(message, 'AUTH_ERROR', context);
|
|
55
|
+
this.name = 'AuthError';
|
|
56
|
+
}
|
|
57
|
+
getUserMessage() {
|
|
58
|
+
return `Authentication error: ${this.message}. Please check your VIZZLY_TOKEN.`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Network/connection errors
|
|
64
|
+
*/
|
|
65
|
+
export class NetworkError extends VizzlyError {
|
|
66
|
+
constructor(message, context = {}) {
|
|
67
|
+
super(message, 'NETWORK_ERROR', context);
|
|
68
|
+
this.name = 'NetworkError';
|
|
69
|
+
}
|
|
70
|
+
getUserMessage() {
|
|
71
|
+
return `Network error: ${this.message}. Please check your connection.`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Upload-related errors
|
|
77
|
+
*/
|
|
78
|
+
export class UploadError extends VizzlyError {
|
|
79
|
+
constructor(message, context = {}) {
|
|
80
|
+
super(message, 'UPLOAD_ERROR', context);
|
|
81
|
+
this.name = 'UploadError';
|
|
82
|
+
}
|
|
83
|
+
getUserMessage() {
|
|
84
|
+
return `Upload failed: ${this.message}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Screenshot-related errors
|
|
90
|
+
*/
|
|
91
|
+
export class ScreenshotError extends VizzlyError {
|
|
92
|
+
constructor(message, context = {}) {
|
|
93
|
+
super(message, 'SCREENSHOT_ERROR', context);
|
|
94
|
+
this.name = 'ScreenshotError';
|
|
95
|
+
}
|
|
96
|
+
getUserMessage() {
|
|
97
|
+
return `Screenshot error: ${this.message}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build-related errors
|
|
103
|
+
*/
|
|
104
|
+
export class BuildError extends VizzlyError {
|
|
105
|
+
constructor(message, context = {}) {
|
|
106
|
+
super(message, 'BUILD_ERROR', context);
|
|
107
|
+
this.name = 'BuildError';
|
|
108
|
+
}
|
|
109
|
+
getUserMessage() {
|
|
110
|
+
return `Build error: ${this.message}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Timeout errors
|
|
116
|
+
*/
|
|
117
|
+
export class TimeoutError extends VizzlyError {
|
|
118
|
+
constructor(message, duration, context = {}) {
|
|
119
|
+
super(message, 'TIMEOUT_ERROR', {
|
|
120
|
+
duration,
|
|
121
|
+
...context
|
|
122
|
+
});
|
|
123
|
+
this.name = 'TimeoutError';
|
|
124
|
+
this.duration = duration;
|
|
125
|
+
}
|
|
126
|
+
getUserMessage() {
|
|
127
|
+
return `Operation timed out after ${this.duration}ms: ${this.message}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Validation errors
|
|
133
|
+
*/
|
|
134
|
+
export class ValidationError extends VizzlyError {
|
|
135
|
+
constructor(message, errors = [], context = {}) {
|
|
136
|
+
super(message, 'VALIDATION_ERROR', {
|
|
137
|
+
errors,
|
|
138
|
+
...context
|
|
139
|
+
});
|
|
140
|
+
this.name = 'ValidationError';
|
|
141
|
+
this.errors = errors;
|
|
142
|
+
}
|
|
143
|
+
getUserMessage() {
|
|
144
|
+
if (this.errors.length > 0) {
|
|
145
|
+
return `${this.message}: ${this.errors.join(', ')}`;
|
|
146
|
+
}
|
|
147
|
+
return this.message;
|
|
148
|
+
}
|
|
149
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vizzly CLI & SDK - Main exports
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point. For specific use cases:
|
|
5
|
+
* - Test runners: import from '@vizzly-testing/cli/client'
|
|
6
|
+
* - Custom integrations: import from '@vizzly-testing/cli/sdk'
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Primary SDK export
|
|
10
|
+
export { createVizzly } from './sdk/index.js';
|
|
11
|
+
|
|
12
|
+
// Client exports for convenience
|
|
13
|
+
export { vizzlyScreenshot, configure, setEnabled } from './client/index.js';
|
|
14
|
+
|
|
15
|
+
// Core services (for advanced usage)
|
|
16
|
+
export { createUploader } from './services/uploader.js';
|
|
17
|
+
export { createTDDService } from './services/tdd-service.js';
|
|
18
|
+
|
|
19
|
+
// Service container
|
|
20
|
+
export { ServiceContainer, container } from './container/index.js';
|
|
21
|
+
export { BaseService } from './services/base-service.js';
|
|
22
|
+
|
|
23
|
+
// Utilities
|
|
24
|
+
export { loadConfig } from './utils/config-loader.js';
|
|
25
|
+
export { createLogger } from './utils/logger.js';
|
|
26
|
+
|
|
27
|
+
// Configuration helper
|
|
28
|
+
export { defineConfig } from './utils/config-helpers.js';
|
|
29
|
+
|
|
30
|
+
// Errors
|
|
31
|
+
export { UploadError } from './errors/vizzly-error.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple factory for creating Vizzly instances with shared configuration
|
|
3
|
+
* Users handle their own screenshots, just pass the buffer to Vizzly
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createVizzly as createVizzlySDK } from './vizzly.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a factory that pre-configures Vizzly instances
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} config - Shared configuration
|
|
12
|
+
* @param {Object} [config.defaultProperties] - Default metadata for all screenshots
|
|
13
|
+
* @param {number} [config.defaultThreshold] - Default comparison threshold
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // test-setup.js - Configure once
|
|
17
|
+
* export const createVizzly = vizzlyFactory({
|
|
18
|
+
* defaultProperties: {
|
|
19
|
+
* framework: 'playwright',
|
|
20
|
+
* project: 'web-app'
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // my-test.spec.js - Use everywhere
|
|
25
|
+
* const vizzly = createVizzly();
|
|
26
|
+
*
|
|
27
|
+
* const screenshot = await page.screenshot({ fullPage: true }); // Your method
|
|
28
|
+
* await vizzly.screenshot({
|
|
29
|
+
* name: 'homepage',
|
|
30
|
+
* image: screenshot, // Your buffer
|
|
31
|
+
* properties: { browser: 'chrome' } // Merges with defaults
|
|
32
|
+
* });
|
|
33
|
+
*/
|
|
34
|
+
export function vizzlyFactory(globalConfig) {
|
|
35
|
+
const {
|
|
36
|
+
defaultProperties = {},
|
|
37
|
+
defaultThreshold,
|
|
38
|
+
...vizzlyConfig
|
|
39
|
+
} = globalConfig;
|
|
40
|
+
return function createVizzly(overrideConfig = {}) {
|
|
41
|
+
const vizzly = createVizzlySDK({
|
|
42
|
+
...vizzlyConfig,
|
|
43
|
+
...overrideConfig
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
...vizzly,
|
|
47
|
+
/**
|
|
48
|
+
* Take a screenshot with default properties merged in
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} screenshot - Screenshot object
|
|
51
|
+
* @param {string} screenshot.name - Screenshot name
|
|
52
|
+
* @param {Buffer} screenshot.image - Image buffer from YOUR screenshot method
|
|
53
|
+
* @param {Object} [screenshot.properties] - Additional metadata (merged with defaults)
|
|
54
|
+
* @param {number} [screenshot.threshold] - Comparison threshold (defaults to global)
|
|
55
|
+
*/
|
|
56
|
+
async screenshot(screenshot) {
|
|
57
|
+
return await vizzly.screenshot({
|
|
58
|
+
...screenshot,
|
|
59
|
+
properties: {
|
|
60
|
+
...defaultProperties,
|
|
61
|
+
...screenshot.properties
|
|
62
|
+
},
|
|
63
|
+
threshold: screenshot.threshold || defaultThreshold
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
}
|