@vizzly-testing/cli 0.9.1 → 0.10.1

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.
@@ -6,7 +6,6 @@
6
6
  * @param {Object} [options] - Optional configuration
7
7
  * @param {Record<string, any>} [options.properties] - Additional properties to attach to the screenshot
8
8
  * @param {number} [options.threshold=0] - Pixel difference threshold (0-100)
9
- * @param {string} [options.variant] - Variant name for organizing screenshots
10
9
  * @param {boolean} [options.fullPage=false] - Whether this is a full page screenshot
11
10
  *
12
11
  * @returns {Promise<void>}
@@ -33,7 +32,6 @@
33
32
  export function vizzlyScreenshot(name: string, imageBuffer: Buffer, options?: {
34
33
  properties?: Record<string, any>;
35
34
  threshold?: number;
36
- variant?: string;
37
35
  fullPage?: boolean;
38
36
  }): Promise<void>;
39
37
  /**
@@ -5,10 +5,29 @@ export function init(options?: {}): Promise<void>;
5
5
  * Simple configuration setup for Vizzly CLI
6
6
  */
7
7
  export class InitCommand {
8
- constructor(logger: any);
8
+ constructor(logger: any, plugins?: any[]);
9
9
  logger: any;
10
+ plugins: any[];
10
11
  run(options?: {}): Promise<void>;
11
12
  generateConfigFile(configPath: any): Promise<void>;
13
+ /**
14
+ * Generate configuration sections for plugins
15
+ * @returns {string} Plugin config sections as formatted string
16
+ */
17
+ generatePluginConfigs(): string;
18
+ /**
19
+ * Format a plugin's config schema as JavaScript code
20
+ * @param {Object} plugin - Plugin with configSchema
21
+ * @returns {string} Formatted config string
22
+ */
23
+ formatPluginConfig(plugin: any): string;
24
+ /**
25
+ * Format a JavaScript value with proper indentation
26
+ * @param {*} value - Value to format
27
+ * @param {number} depth - Current indentation depth
28
+ * @returns {string} Formatted value
29
+ */
30
+ formatValue(value: any, depth?: number): string;
12
31
  showNextSteps(): void;
13
32
  fileExists(filePath: any): Promise<boolean>;
14
33
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Load and register plugins from node_modules and config
3
+ * @param {string|null} configPath - Path to config file
4
+ * @param {Object} config - Loaded configuration
5
+ * @param {Object} logger - Logger instance
6
+ * @returns {Promise<Array>} Array of loaded plugins
7
+ */
8
+ export function loadPlugins(configPath: string | null, config: any, logger: any): Promise<any[]>;
@@ -2,7 +2,6 @@ export function loadConfig(configPath?: any, cliOverrides?: {}): Promise<{
2
2
  server: {
3
3
  port: number;
4
4
  timeout: number;
5
- screenshotPath: string;
6
5
  };
7
6
  build: {
8
7
  name: string;
@@ -19,6 +18,7 @@ export function loadConfig(configPath?: any, cliOverrides?: {}): Promise<{
19
18
  tdd: {
20
19
  openReport: boolean;
21
20
  };
21
+ plugins: any[];
22
22
  apiKey: string;
23
23
  apiUrl: string;
24
24
  }>;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Validate Vizzly configuration
3
+ * @param {unknown} config - Configuration to validate
4
+ * @returns {Object} Validated configuration
5
+ * @throws {ZodError} If validation fails
6
+ */
7
+ export function validateVizzlyConfig(config: unknown): any;
8
+ /**
9
+ * Safely validate with defaults if config is missing
10
+ * @param {unknown} config - Configuration to validate (can be undefined)
11
+ * @returns {Object} Validated configuration with defaults
12
+ */
13
+ export function validateVizzlyConfigWithDefaults(config: unknown): any;
14
+ /**
15
+ * Core Vizzly configuration schema
16
+ * Allows plugin-specific keys with passthrough for extensibility
17
+ */
18
+ export let vizzlyConfigSchema: z.ZodDefault<z.ZodObject<{
19
+ apiKey: z.ZodOptional<z.ZodString>;
20
+ apiUrl: z.ZodOptional<z.ZodString>;
21
+ server: z.ZodDefault<z.ZodObject<{
22
+ port: z.ZodDefault<z.ZodNumber>;
23
+ timeout: z.ZodDefault<z.ZodNumber>;
24
+ }, "strip", z.ZodTypeAny, {
25
+ port?: number;
26
+ timeout?: number;
27
+ }, {
28
+ port?: number;
29
+ timeout?: number;
30
+ }>>;
31
+ build: z.ZodDefault<z.ZodObject<{
32
+ name: z.ZodDefault<z.ZodString>;
33
+ environment: z.ZodDefault<z.ZodString>;
34
+ branch: z.ZodOptional<z.ZodString>;
35
+ commit: z.ZodOptional<z.ZodString>;
36
+ message: z.ZodOptional<z.ZodString>;
37
+ }, "strip", z.ZodTypeAny, {
38
+ name?: string;
39
+ message?: string;
40
+ environment?: string;
41
+ branch?: string;
42
+ commit?: string;
43
+ }, {
44
+ name?: string;
45
+ message?: string;
46
+ environment?: string;
47
+ branch?: string;
48
+ commit?: string;
49
+ }>>;
50
+ upload: z.ZodDefault<z.ZodObject<{
51
+ screenshotsDir: z.ZodDefault<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
52
+ batchSize: z.ZodDefault<z.ZodNumber>;
53
+ timeout: z.ZodDefault<z.ZodNumber>;
54
+ }, "strip", z.ZodTypeAny, {
55
+ timeout?: number;
56
+ screenshotsDir?: string | string[];
57
+ batchSize?: number;
58
+ }, {
59
+ timeout?: number;
60
+ screenshotsDir?: string | string[];
61
+ batchSize?: number;
62
+ }>>;
63
+ comparison: z.ZodDefault<z.ZodObject<{
64
+ threshold: z.ZodDefault<z.ZodNumber>;
65
+ }, "strip", z.ZodTypeAny, {
66
+ threshold?: number;
67
+ }, {
68
+ threshold?: number;
69
+ }>>;
70
+ tdd: z.ZodDefault<z.ZodObject<{
71
+ openReport: z.ZodDefault<z.ZodBoolean>;
72
+ }, "strip", z.ZodTypeAny, {
73
+ openReport?: boolean;
74
+ }, {
75
+ openReport?: boolean;
76
+ }>>;
77
+ plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
78
+ parallelId: z.ZodOptional<z.ZodString>;
79
+ baselineBuildId: z.ZodOptional<z.ZodString>;
80
+ baselineComparisonId: z.ZodOptional<z.ZodString>;
81
+ eager: z.ZodOptional<z.ZodBoolean>;
82
+ wait: z.ZodOptional<z.ZodBoolean>;
83
+ allowNoToken: z.ZodOptional<z.ZodBoolean>;
84
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
85
+ apiKey: z.ZodOptional<z.ZodString>;
86
+ apiUrl: z.ZodOptional<z.ZodString>;
87
+ server: z.ZodDefault<z.ZodObject<{
88
+ port: z.ZodDefault<z.ZodNumber>;
89
+ timeout: z.ZodDefault<z.ZodNumber>;
90
+ }, "strip", z.ZodTypeAny, {
91
+ port?: number;
92
+ timeout?: number;
93
+ }, {
94
+ port?: number;
95
+ timeout?: number;
96
+ }>>;
97
+ build: z.ZodDefault<z.ZodObject<{
98
+ name: z.ZodDefault<z.ZodString>;
99
+ environment: z.ZodDefault<z.ZodString>;
100
+ branch: z.ZodOptional<z.ZodString>;
101
+ commit: z.ZodOptional<z.ZodString>;
102
+ message: z.ZodOptional<z.ZodString>;
103
+ }, "strip", z.ZodTypeAny, {
104
+ name?: string;
105
+ message?: string;
106
+ environment?: string;
107
+ branch?: string;
108
+ commit?: string;
109
+ }, {
110
+ name?: string;
111
+ message?: string;
112
+ environment?: string;
113
+ branch?: string;
114
+ commit?: string;
115
+ }>>;
116
+ upload: z.ZodDefault<z.ZodObject<{
117
+ screenshotsDir: z.ZodDefault<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
118
+ batchSize: z.ZodDefault<z.ZodNumber>;
119
+ timeout: z.ZodDefault<z.ZodNumber>;
120
+ }, "strip", z.ZodTypeAny, {
121
+ timeout?: number;
122
+ screenshotsDir?: string | string[];
123
+ batchSize?: number;
124
+ }, {
125
+ timeout?: number;
126
+ screenshotsDir?: string | string[];
127
+ batchSize?: number;
128
+ }>>;
129
+ comparison: z.ZodDefault<z.ZodObject<{
130
+ threshold: z.ZodDefault<z.ZodNumber>;
131
+ }, "strip", z.ZodTypeAny, {
132
+ threshold?: number;
133
+ }, {
134
+ threshold?: number;
135
+ }>>;
136
+ tdd: z.ZodDefault<z.ZodObject<{
137
+ openReport: z.ZodDefault<z.ZodBoolean>;
138
+ }, "strip", z.ZodTypeAny, {
139
+ openReport?: boolean;
140
+ }, {
141
+ openReport?: boolean;
142
+ }>>;
143
+ plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
144
+ parallelId: z.ZodOptional<z.ZodString>;
145
+ baselineBuildId: z.ZodOptional<z.ZodString>;
146
+ baselineComparisonId: z.ZodOptional<z.ZodString>;
147
+ eager: z.ZodOptional<z.ZodBoolean>;
148
+ wait: z.ZodOptional<z.ZodBoolean>;
149
+ allowNoToken: z.ZodOptional<z.ZodBoolean>;
150
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
151
+ apiKey: z.ZodOptional<z.ZodString>;
152
+ apiUrl: z.ZodOptional<z.ZodString>;
153
+ server: z.ZodDefault<z.ZodObject<{
154
+ port: z.ZodDefault<z.ZodNumber>;
155
+ timeout: z.ZodDefault<z.ZodNumber>;
156
+ }, "strip", z.ZodTypeAny, {
157
+ port?: number;
158
+ timeout?: number;
159
+ }, {
160
+ port?: number;
161
+ timeout?: number;
162
+ }>>;
163
+ build: z.ZodDefault<z.ZodObject<{
164
+ name: z.ZodDefault<z.ZodString>;
165
+ environment: z.ZodDefault<z.ZodString>;
166
+ branch: z.ZodOptional<z.ZodString>;
167
+ commit: z.ZodOptional<z.ZodString>;
168
+ message: z.ZodOptional<z.ZodString>;
169
+ }, "strip", z.ZodTypeAny, {
170
+ name?: string;
171
+ message?: string;
172
+ environment?: string;
173
+ branch?: string;
174
+ commit?: string;
175
+ }, {
176
+ name?: string;
177
+ message?: string;
178
+ environment?: string;
179
+ branch?: string;
180
+ commit?: string;
181
+ }>>;
182
+ upload: z.ZodDefault<z.ZodObject<{
183
+ screenshotsDir: z.ZodDefault<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
184
+ batchSize: z.ZodDefault<z.ZodNumber>;
185
+ timeout: z.ZodDefault<z.ZodNumber>;
186
+ }, "strip", z.ZodTypeAny, {
187
+ timeout?: number;
188
+ screenshotsDir?: string | string[];
189
+ batchSize?: number;
190
+ }, {
191
+ timeout?: number;
192
+ screenshotsDir?: string | string[];
193
+ batchSize?: number;
194
+ }>>;
195
+ comparison: z.ZodDefault<z.ZodObject<{
196
+ threshold: z.ZodDefault<z.ZodNumber>;
197
+ }, "strip", z.ZodTypeAny, {
198
+ threshold?: number;
199
+ }, {
200
+ threshold?: number;
201
+ }>>;
202
+ tdd: z.ZodDefault<z.ZodObject<{
203
+ openReport: z.ZodDefault<z.ZodBoolean>;
204
+ }, "strip", z.ZodTypeAny, {
205
+ openReport?: boolean;
206
+ }, {
207
+ openReport?: boolean;
208
+ }>>;
209
+ plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
210
+ parallelId: z.ZodOptional<z.ZodString>;
211
+ baselineBuildId: z.ZodOptional<z.ZodString>;
212
+ baselineComparisonId: z.ZodOptional<z.ZodString>;
213
+ eager: z.ZodOptional<z.ZodBoolean>;
214
+ wait: z.ZodOptional<z.ZodBoolean>;
215
+ allowNoToken: z.ZodOptional<z.ZodBoolean>;
216
+ }, z.ZodTypeAny, "passthrough">>>;
217
+ import { z } from 'zod';
@@ -1,6 +1,7 @@
1
1
  import { cosmiconfigSync } from 'cosmiconfig';
2
2
  import { resolve } from 'path';
3
3
  import { getApiToken, getApiUrl, getParallelId } from './environment-config.js';
4
+ import { validateVizzlyConfigWithDefaults } from './config-schema.js';
4
5
  const DEFAULT_CONFIG = {
5
6
  // API Configuration
6
7
  apiKey: getApiToken(),
@@ -8,8 +9,7 @@ const DEFAULT_CONFIG = {
8
9
  // Server Configuration (for run command)
9
10
  server: {
10
11
  port: 47392,
11
- timeout: 30000,
12
- screenshotPath: '/screenshot'
12
+ timeout: 30000
13
13
  },
14
14
  // Build Configuration
15
15
  build: {
@@ -29,9 +29,23 @@ const DEFAULT_CONFIG = {
29
29
  // TDD Configuration
30
30
  tdd: {
31
31
  openReport: false // Whether to auto-open HTML report in browser
32
- }
32
+ },
33
+ // Plugins
34
+ plugins: []
33
35
  };
34
36
  export async function loadConfig(configPath = null, cliOverrides = {}) {
37
+ // 1. Load from config file using cosmiconfig
38
+ const explorer = cosmiconfigSync('vizzly');
39
+ const result = configPath ? explorer.load(configPath) : explorer.search();
40
+ let fileConfig = {};
41
+ if (result && result.config) {
42
+ // Handle ESM default export (cosmiconfig wraps it in { default: {...} })
43
+ fileConfig = result.config.default || result.config;
44
+ }
45
+
46
+ // 2. Validate config file using Zod schema
47
+ const validatedFileConfig = validateVizzlyConfigWithDefaults(fileConfig);
48
+
35
49
  // Create a proper clone of the default config to avoid shared object references
36
50
  const config = {
37
51
  ...DEFAULT_CONFIG,
@@ -49,17 +63,14 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
49
63
  },
50
64
  tdd: {
51
65
  ...DEFAULT_CONFIG.tdd
52
- }
66
+ },
67
+ plugins: [...DEFAULT_CONFIG.plugins]
53
68
  };
54
69
 
55
- // 1. Load from config file using cosmiconfig
56
- const explorer = cosmiconfigSync('vizzly');
57
- const result = configPath ? explorer.load(configPath) : explorer.search();
58
- if (result && result.config) {
59
- mergeConfig(config, result.config);
60
- }
70
+ // Merge validated file config
71
+ mergeConfig(config, validatedFileConfig);
61
72
 
62
- // 2. Override with environment variables
73
+ // 3. Override with environment variables
63
74
  const envApiKey = getApiToken();
64
75
  const envApiUrl = getApiUrl();
65
76
  const envParallelId = getParallelId();
@@ -67,7 +78,7 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
67
78
  if (envApiUrl !== 'https://app.vizzly.dev') config.apiUrl = envApiUrl;
68
79
  if (envParallelId) config.parallelId = envParallelId;
69
80
 
70
- // 3. Apply CLI overrides (highest priority)
81
+ // 4. Apply CLI overrides (highest priority)
71
82
  applyCLIOverrides(config, cliOverrides);
72
83
  return config;
73
84
  }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Configuration schema validation for Vizzly CLI
3
+ * Uses Zod for runtime validation
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ /**
9
+ * Server configuration schema
10
+ */
11
+ let serverSchema = z.object({
12
+ port: z.number().int().positive().default(47392),
13
+ timeout: z.number().int().positive().default(30000)
14
+ });
15
+
16
+ /**
17
+ * Build configuration schema
18
+ */
19
+ let buildSchema = z.object({
20
+ name: z.string().default('Build {timestamp}'),
21
+ environment: z.string().default('test'),
22
+ branch: z.string().optional(),
23
+ commit: z.string().optional(),
24
+ message: z.string().optional()
25
+ });
26
+
27
+ /**
28
+ * Upload configuration schema
29
+ */
30
+ let uploadSchema = z.object({
31
+ screenshotsDir: z.union([z.string(), z.array(z.string())]).default('./screenshots'),
32
+ batchSize: z.number().int().positive().default(10),
33
+ timeout: z.number().int().positive().default(30000)
34
+ });
35
+
36
+ /**
37
+ * Comparison configuration schema
38
+ */
39
+ let comparisonSchema = z.object({
40
+ threshold: z.number().min(0).max(1).default(0.1)
41
+ });
42
+
43
+ /**
44
+ * TDD configuration schema
45
+ */
46
+ let tddSchema = z.object({
47
+ openReport: z.boolean().default(false)
48
+ });
49
+
50
+ /**
51
+ * Core Vizzly configuration schema
52
+ * Allows plugin-specific keys with passthrough for extensibility
53
+ */
54
+ export let vizzlyConfigSchema = z.object({
55
+ // Core Vizzly config
56
+ apiKey: z.string().optional(),
57
+ apiUrl: z.string().url().optional(),
58
+ server: serverSchema.default({
59
+ port: 47392,
60
+ timeout: 30000
61
+ }),
62
+ build: buildSchema.default({
63
+ name: 'Build {timestamp}',
64
+ environment: 'test'
65
+ }),
66
+ upload: uploadSchema.default({
67
+ screenshotsDir: './screenshots',
68
+ batchSize: 10,
69
+ timeout: 30000
70
+ }),
71
+ comparison: comparisonSchema.default({
72
+ threshold: 0.1
73
+ }),
74
+ tdd: tddSchema.default({
75
+ openReport: false
76
+ }),
77
+ plugins: z.array(z.string()).default([]),
78
+ // Additional optional fields
79
+ parallelId: z.string().optional(),
80
+ baselineBuildId: z.string().optional(),
81
+ baselineComparisonId: z.string().optional(),
82
+ eager: z.boolean().optional(),
83
+ wait: z.boolean().optional(),
84
+ allowNoToken: z.boolean().optional()
85
+ }).passthrough() // Allow plugin-specific keys like `staticSite`, `storybook`, etc.
86
+ .default({
87
+ server: {
88
+ port: 47392,
89
+ timeout: 30000
90
+ },
91
+ build: {
92
+ name: 'Build {timestamp}',
93
+ environment: 'test'
94
+ },
95
+ upload: {
96
+ screenshotsDir: './screenshots',
97
+ batchSize: 10,
98
+ timeout: 30000
99
+ },
100
+ comparison: {
101
+ threshold: 0.1
102
+ },
103
+ tdd: {
104
+ openReport: false
105
+ },
106
+ plugins: []
107
+ });
108
+
109
+ /**
110
+ * Validate Vizzly configuration
111
+ * @param {unknown} config - Configuration to validate
112
+ * @returns {Object} Validated configuration
113
+ * @throws {ZodError} If validation fails
114
+ */
115
+ export function validateVizzlyConfig(config) {
116
+ try {
117
+ return vizzlyConfigSchema.parse(config);
118
+ } catch (error) {
119
+ // Re-throw with more context
120
+ throw new Error(`Invalid Vizzly configuration: ${error.message}\n\n` + `Please check your vizzly.config.js file.\n` + `See https://vizzly.dev/docs/configuration for configuration options.`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Safely validate with defaults if config is missing
126
+ * @param {unknown} config - Configuration to validate (can be undefined)
127
+ * @returns {Object} Validated configuration with defaults
128
+ */
129
+ export function validateVizzlyConfigWithDefaults(config) {
130
+ if (!config) {
131
+ return vizzlyConfigSchema.parse({});
132
+ }
133
+ return validateVizzlyConfig(config);
134
+ }
@@ -476,8 +476,7 @@ Configuration loaded via cosmiconfig in this order:
476
476
  // Server Configuration (for run command)
477
477
  server: {
478
478
  port: number, // Server port (default: 47392)
479
- timeout: number, // Timeout in ms (default: 30000)
480
- screenshotPath: string // Screenshot endpoint path
479
+ timeout: number // Timeout in ms (default: 30000)
481
480
  },
482
481
 
483
482
  // Build Configuration