@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,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
+ }