@vizzly-testing/cli 0.20.1-beta.0 → 0.20.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.
Files changed (38) hide show
  1. package/README.md +16 -18
  2. package/dist/cli.js +177 -2
  3. package/dist/client/index.js +144 -77
  4. package/dist/commands/doctor.js +118 -33
  5. package/dist/commands/finalize.js +8 -3
  6. package/dist/commands/init.js +13 -18
  7. package/dist/commands/login.js +42 -49
  8. package/dist/commands/logout.js +13 -5
  9. package/dist/commands/project.js +95 -67
  10. package/dist/commands/run.js +32 -6
  11. package/dist/commands/status.js +81 -50
  12. package/dist/commands/tdd-daemon.js +61 -32
  13. package/dist/commands/tdd.js +14 -26
  14. package/dist/commands/upload.js +18 -9
  15. package/dist/commands/whoami.js +40 -38
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +204 -22
  18. package/dist/server/handlers/tdd-handler.js +113 -7
  19. package/dist/server/http-server.js +9 -3
  20. package/dist/server/routers/baseline.js +58 -0
  21. package/dist/server/routers/dashboard.js +10 -6
  22. package/dist/server/routers/screenshot.js +32 -0
  23. package/dist/server-manager/core.js +5 -2
  24. package/dist/server-manager/operations.js +2 -1
  25. package/dist/services/config-service.js +306 -0
  26. package/dist/tdd/tdd-service.js +190 -126
  27. package/dist/types/client.d.ts +25 -2
  28. package/dist/utils/colors.js +187 -39
  29. package/dist/utils/config-loader.js +3 -6
  30. package/dist/utils/context.js +228 -0
  31. package/dist/utils/output.js +449 -14
  32. package/docs/api-reference.md +173 -8
  33. package/docs/tui-elements.md +560 -0
  34. package/package.json +13 -7
  35. package/dist/report-generator/core.js +0 -315
  36. package/dist/report-generator/index.js +0 -8
  37. package/dist/report-generator/operations.js +0 -196
  38. package/dist/services/static-report-generator.js +0 -65
package/README.md CHANGED
@@ -238,13 +238,13 @@ vizzly run "npm test" --parallel-id "ci-run-123" # For parallel CI builds
238
238
  - `--allow-no-token` - Allow running without API token (useful for local development)
239
239
  - `--token <token>` - API token override
240
240
 
241
- ## Dev Command
241
+ ## TDD Command
242
242
 
243
- For local development with visual testing, use the `dev` command:
243
+ For local development with visual testing, use the `tdd` command:
244
244
 
245
245
  ```bash
246
- # Start interactive dev server (runs in background)
247
- vizzly dev start
246
+ # Start TDD server (runs in background)
247
+ vizzly tdd start
248
248
 
249
249
  # Run your tests in watch mode (same terminal or new one)
250
250
  npm test -- --watch
@@ -252,7 +252,7 @@ npm test -- --watch
252
252
  # View the dashboard at http://localhost:47392
253
253
  ```
254
254
 
255
- **Interactive Dashboard:** The dev dashboard provides real-time feedback:
255
+ **Interactive Dashboard:** The TDD dashboard provides real-time feedback:
256
256
  - **Visual Comparisons** - See diffs as tests run with multiple view modes
257
257
  - **Baseline Management** - Accept/reject changes directly from the UI
258
258
  - **Configuration Editor** - Edit settings without touching config files
@@ -260,23 +260,23 @@ npm test -- --watch
260
260
  - **Test Statistics** - Real-time pass/fail metrics
261
261
  - **Dark Theme** - Easy on the eyes during long sessions
262
262
 
263
- **Dev Subcommands:**
263
+ **TDD Subcommands:**
264
264
 
265
265
  ```bash
266
- # Start the dev server (primary workflow)
267
- vizzly dev start [options]
266
+ # Start the TDD server (primary workflow)
267
+ vizzly tdd start [options]
268
268
 
269
269
  # Run tests once with ephemeral server (generates static report)
270
- vizzly dev run "npm test" [options]
270
+ vizzly tdd run "npm test" [options]
271
271
 
272
- # Check dev server status
273
- vizzly dev status
272
+ # Check TDD server status
273
+ vizzly tdd status
274
274
 
275
- # Stop a running dev server
276
- vizzly dev stop
275
+ # Stop a running TDD server
276
+ vizzly tdd stop
277
277
  ```
278
278
 
279
- **Dev Command Options:**
279
+ **TDD Command Options:**
280
280
  - `--set-baseline` - Accept current screenshots as new baseline
281
281
  - `--baseline-build <id>` - Use specific build as baseline (requires API token)
282
282
  - `--threshold <number>` - Comparison threshold (CIEDE2000 Delta E, default: 2.0)
@@ -340,10 +340,8 @@ VIZZLY_TOKEN=your-token vizzly doctor --api
340
340
  vizzly doctor --json
341
341
  ```
342
342
 
343
- The `dev` command provides fast local development with immediate visual feedback. See the
344
- [Dev Mode Guide](./docs/dev-mode.md) for complete details on local visual testing.
345
-
346
- > **Note:** The `vizzly tdd` command is deprecated and will be removed in the next major version. Please use `vizzly dev` instead.
343
+ The `tdd` command provides fast local development with immediate visual feedback. See the
344
+ [TDD Mode Guide](./docs/tdd-mode.md) for complete details on local visual testing.
347
345
 
348
346
  ## Configuration
349
347
 
package/dist/cli.js CHANGED
@@ -16,10 +16,178 @@ import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
16
16
  import { createPluginServices } from './plugin-api.js';
17
17
  import { loadPlugins } from './plugin-loader.js';
18
18
  import { createServices } from './services/index.js';
19
+ import { colors } from './utils/colors.js';
19
20
  import { loadConfig } from './utils/config-loader.js';
21
+ import { getContext } from './utils/context.js';
20
22
  import * as output from './utils/output.js';
21
23
  import { getPackageVersion } from './utils/package-info.js';
22
- program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json', 'Machine-readable output').option('--no-color', 'Disable colored output');
24
+
25
+ // Custom help formatting with Observatory design system
26
+ const formatHelp = (cmd, helper) => {
27
+ let c = colors;
28
+ let lines = [];
29
+ let isRootCommand = !cmd.parent;
30
+ let version = getPackageVersion();
31
+
32
+ // Branded header with grizzly bear
33
+ lines.push('');
34
+ if (isRootCommand) {
35
+ // Cute grizzly bear mascot with square eyes (like the Vizzly logo!)
36
+ lines.push(c.brand.amber(' ʕ□ᴥ□ʔ'));
37
+ lines.push(` ${c.brand.amber(c.bold('vizzly'))} ${c.dim(`v${version}`)}`);
38
+ lines.push(` ${c.gray('Visual regression testing for UI teams')}`);
39
+ } else {
40
+ // Compact header for subcommands
41
+ lines.push(` ${c.brand.amber(c.bold('vizzly'))} ${c.white(cmd.name())}`);
42
+ let desc = cmd.description();
43
+ if (desc) {
44
+ lines.push(` ${c.gray(desc)}`);
45
+ }
46
+ }
47
+ lines.push('');
48
+
49
+ // Usage
50
+ let usage = helper.commandUsage(cmd).replace('Usage: ', '');
51
+ lines.push(` ${c.dim('Usage')} ${c.white(usage)}`);
52
+ lines.push('');
53
+
54
+ // Get all subcommands
55
+ let commands = helper.visibleCommands(cmd);
56
+ if (commands.length > 0) {
57
+ if (isRootCommand) {
58
+ // Group commands by category for root help with icons
59
+ let categories = [{
60
+ key: 'core',
61
+ icon: '▸',
62
+ title: 'Core',
63
+ names: ['run', 'tdd', 'upload', 'status', 'finalize']
64
+ }, {
65
+ key: 'setup',
66
+ icon: '▸',
67
+ title: 'Setup',
68
+ names: ['init', 'doctor']
69
+ }, {
70
+ key: 'auth',
71
+ icon: '▸',
72
+ title: 'Account',
73
+ names: ['login', 'logout', 'whoami']
74
+ }, {
75
+ key: 'project',
76
+ icon: '▸',
77
+ title: 'Projects',
78
+ names: ['project:select', 'project:list', 'project:token', 'project:remove']
79
+ }];
80
+ let grouped = {
81
+ core: [],
82
+ setup: [],
83
+ auth: [],
84
+ project: [],
85
+ other: []
86
+ };
87
+ for (let command of commands) {
88
+ let name = command.name();
89
+ if (name === 'help') continue;
90
+ let found = false;
91
+ for (let cat of categories) {
92
+ if (cat.names.includes(name)) {
93
+ grouped[cat.key].push(command);
94
+ found = true;
95
+ break;
96
+ }
97
+ }
98
+ if (!found) grouped.other.push(command);
99
+ }
100
+ for (let cat of categories) {
101
+ let cmds = grouped[cat.key];
102
+ if (cmds.length === 0) continue;
103
+ lines.push(` ${c.brand.amber(cat.icon)} ${c.bold(cat.title)}`);
104
+ for (let command of cmds) {
105
+ let name = command.name();
106
+ let desc = command.description() || '';
107
+ // Truncate long descriptions
108
+ if (desc.length > 48) desc = `${desc.substring(0, 45)}...`;
109
+ lines.push(` ${c.white(name.padEnd(18))} ${c.gray(desc)}`);
110
+ }
111
+ lines.push('');
112
+ }
113
+
114
+ // Plugins (other commands from plugins)
115
+ if (grouped.other.length > 0) {
116
+ lines.push(` ${c.brand.amber('▸')} ${c.bold('Plugins')}`);
117
+ for (let command of grouped.other) {
118
+ let name = command.name();
119
+ let desc = command.description() || '';
120
+ if (desc.length > 48) desc = `${desc.substring(0, 45)}...`;
121
+ lines.push(` ${c.white(name.padEnd(18))} ${c.gray(desc)}`);
122
+ }
123
+ lines.push('');
124
+ }
125
+ } else {
126
+ // For subcommands, simple list
127
+ lines.push(` ${c.brand.amber('▸')} ${c.bold('Commands')}`);
128
+ for (let command of commands) {
129
+ let name = command.name();
130
+ if (name === 'help') continue;
131
+ let desc = command.description() || '';
132
+ if (desc.length > 48) desc = `${desc.substring(0, 45)}...`;
133
+ lines.push(` ${c.white(name.padEnd(18))} ${c.gray(desc)}`);
134
+ }
135
+ lines.push('');
136
+ }
137
+ }
138
+
139
+ // Options - use dimmer styling for less visual weight
140
+ let options = helper.visibleOptions(cmd);
141
+ if (options.length > 0) {
142
+ lines.push(` ${c.brand.amber('▸')} ${c.bold('Options')}`);
143
+ for (let option of options) {
144
+ let flags = option.flags;
145
+ let desc = option.description || '';
146
+ if (desc.length > 40) desc = `${desc.substring(0, 37)}...`;
147
+ lines.push(` ${c.cyan(flags.padEnd(22))} ${c.dim(desc)}`);
148
+ }
149
+ lines.push('');
150
+ }
151
+
152
+ // Quick start examples (only for root command)
153
+ if (isRootCommand) {
154
+ lines.push(` ${c.brand.amber('▸')} ${c.bold('Quick Start')}`);
155
+ lines.push('');
156
+ lines.push(` ${c.dim('# Local visual testing')}`);
157
+ lines.push(` ${c.gray('$')} ${c.white('vizzly tdd start')}`);
158
+ lines.push('');
159
+ lines.push(` ${c.dim('# CI pipeline')}`);
160
+ lines.push(` ${c.gray('$')} ${c.white('vizzly run "npm test" --wait')}`);
161
+ lines.push('');
162
+ }
163
+
164
+ // Dynamic context section (only for root)
165
+ if (isRootCommand) {
166
+ let contextItems = getContext();
167
+ if (contextItems.length > 0) {
168
+ lines.push(` ${c.dim('─'.repeat(52))}`);
169
+ for (let item of contextItems) {
170
+ if (item.type === 'success') {
171
+ lines.push(` ${c.green('✓')} ${c.gray(item.label)} ${c.white(item.value)}`);
172
+ } else if (item.type === 'warning') {
173
+ lines.push(` ${c.yellow('!')} ${c.gray(item.label)} ${c.yellow(item.value)}`);
174
+ } else {
175
+ lines.push(` ${c.dim('○')} ${c.gray(item.label)} ${c.dim(item.value)}`);
176
+ }
177
+ }
178
+ lines.push('');
179
+ }
180
+ }
181
+
182
+ // Footer with links
183
+ lines.push(` ${c.dim('─'.repeat(52))}`);
184
+ lines.push(` ${c.dim('Docs')} ${c.cyan(c.underline('docs.vizzly.dev'))} ${c.dim('GitHub')} ${c.cyan(c.underline('github.com/vizzly-testing/cli'))}`);
185
+ lines.push('');
186
+ return lines.join('\n');
187
+ };
188
+ program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json', 'Machine-readable output').option('--color', 'Force colored output (even in non-TTY)').option('--no-color', 'Disable colored output').configureHelp({
189
+ formatHelp
190
+ });
23
191
 
24
192
  // Load plugins before defining commands
25
193
  // We need to manually parse to get the config option early
@@ -40,10 +208,17 @@ for (let i = 0; i < process.argv.length; i++) {
40
208
 
41
209
  // Configure output early
42
210
  // Priority: --log-level > --verbose > VIZZLY_LOG_LEVEL env var > default ('info')
211
+ // Color priority: --no-color (off) > --color (on) > auto-detect
212
+ let colorOverride;
213
+ if (process.argv.includes('--no-color')) {
214
+ colorOverride = false;
215
+ } else if (process.argv.includes('--color')) {
216
+ colorOverride = true;
217
+ }
43
218
  output.configure({
44
219
  logLevel: logLevelArg,
45
220
  verbose: verboseMode,
46
- color: !process.argv.includes('--no-color'),
221
+ color: colorOverride,
47
222
  json: process.argv.includes('--json')
48
223
  });
49
224
  const config = await loadConfig(configPath, {});
@@ -4,17 +4,40 @@
4
4
  */
5
5
 
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
+ import { request as httpRequest } from 'node:http';
8
+ import { request as httpsRequest } from 'node:https';
7
9
  import { dirname, join, parse } from 'node:path';
8
10
  import { getBuildId, getServerUrl, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
9
11
 
10
12
  // Internal client state
11
13
  let currentClient = null;
12
14
  let isDisabled = false;
13
- let hasLoggedWarning = false;
14
15
 
15
16
  // Default timeout for screenshot requests (30 seconds)
16
17
  const DEFAULT_TIMEOUT_MS = 30000;
17
18
 
19
+ // Log levels for client SDK output control
20
+ export const LOG_LEVELS = {
21
+ debug: 0,
22
+ info: 1,
23
+ warn: 2,
24
+ error: 3
25
+ };
26
+
27
+ /**
28
+ * Check if client should log at the given level
29
+ * Respects VIZZLY_CLIENT_LOG_LEVEL env var (default: 'error' - only show errors)
30
+ * @param {string} level - Log level to check
31
+ * @param {string} [configuredLevel] - Configured log level (defaults to env var)
32
+ * @returns {boolean} Whether to log at this level
33
+ */
34
+ export function shouldLogClient(level, configuredLevel) {
35
+ let configLevel = configuredLevel || process.env.VIZZLY_CLIENT_LOG_LEVEL?.toLowerCase() || 'error';
36
+ let levelValue = LOG_LEVELS[level] ?? 0;
37
+ let configValue = LOG_LEVELS[configLevel] ?? 3;
38
+ return levelValue >= configValue;
39
+ }
40
+
18
41
  /**
19
42
  * Check if Vizzly is currently disabled
20
43
  * @private
@@ -28,31 +51,32 @@ function isVizzlyDisabled() {
28
51
  /**
29
52
  * Disable Vizzly SDK for the current session
30
53
  * @private
31
- * @param {string} [reason] - Optional reason for disabling
32
54
  */
33
- function disableVizzly(reason = 'disabled') {
55
+ function disableVizzly() {
34
56
  isDisabled = true;
35
57
  currentClient = null;
36
- if (reason !== 'disabled') {
37
- console.warn(`Vizzly SDK disabled due to ${reason}. Screenshots will be skipped for the remainder of this session.`);
38
- }
39
58
  }
40
59
 
41
60
  /**
42
61
  * Auto-discover local TDD server by checking for server.json
43
- * @private
62
+ * @param {string} [startDir] - Directory to start search from (defaults to cwd)
63
+ * @param {Object} [deps] - Injectable dependencies for testing
44
64
  * @returns {string|null} Server URL if found
45
65
  */
46
- function autoDiscoverTddServer() {
66
+ export function autoDiscoverTddServer(startDir, deps = {}) {
67
+ let {
68
+ exists = existsSync,
69
+ readFile = readFileSync
70
+ } = deps;
47
71
  try {
48
72
  // Look for .vizzly/server.json in current directory and parent directories
49
- let currentDir = process.cwd();
73
+ let currentDir = startDir || process.cwd();
50
74
  const root = parse(currentDir).root;
51
75
  while (currentDir !== root) {
52
76
  const serverJsonPath = join(currentDir, '.vizzly', 'server.json');
53
- if (existsSync(serverJsonPath)) {
77
+ if (exists(serverJsonPath)) {
54
78
  try {
55
- const serverInfo = JSON.parse(readFileSync(serverJsonPath, 'utf8'));
79
+ const serverInfo = JSON.parse(readFile(serverJsonPath, 'utf8'));
56
80
  if (serverInfo.port) {
57
81
  const url = `http://localhost:${serverInfo.port}`;
58
82
  return url;
@@ -97,6 +121,63 @@ function getClient() {
97
121
  return currentClient;
98
122
  }
99
123
 
124
+ /**
125
+ * Make HTTP/HTTPS request without keep-alive (so process can exit promptly)
126
+ * @private
127
+ * @param {string} url - Full URL to POST to (http or https)
128
+ * @param {object} body - JSON-serializable request body
129
+ * @param {number} timeoutMs - Request timeout in milliseconds
130
+ * @returns {Promise<{status: number, json: any}>} Response status and parsed JSON body
131
+ */
132
+ function httpPost(url, body, timeoutMs) {
133
+ return new Promise((resolve, reject) => {
134
+ let parsedUrl = new URL(url);
135
+ let data = JSON.stringify(body);
136
+ let isHttps = parsedUrl.protocol === 'https:';
137
+ let request = isHttps ? httpsRequest : httpRequest;
138
+ let req = request({
139
+ hostname: parsedUrl.hostname,
140
+ port: parsedUrl.port || (isHttps ? 443 : 80),
141
+ path: parsedUrl.pathname,
142
+ method: 'POST',
143
+ headers: {
144
+ 'Content-Type': 'application/json',
145
+ 'Content-Length': Buffer.byteLength(data),
146
+ Connection: 'close'
147
+ },
148
+ agent: false // Disable keep-alive agent so process can exit promptly
149
+ }, res => {
150
+ let chunks = [];
151
+ res.on('data', chunk => chunks.push(chunk));
152
+ res.on('end', () => {
153
+ let responseBody = Buffer.concat(chunks).toString();
154
+ let json = null;
155
+ try {
156
+ json = JSON.parse(responseBody);
157
+ } catch (err) {
158
+ if (shouldLogClient('debug')) {
159
+ console.debug(`[vizzly] Failed to parse response: ${err.message}`);
160
+ }
161
+ json = {
162
+ error: responseBody
163
+ };
164
+ }
165
+ resolve({
166
+ status: res.statusCode,
167
+ json
168
+ });
169
+ });
170
+ });
171
+ req.on('error', reject);
172
+ req.setTimeout(timeoutMs, () => {
173
+ req.destroy();
174
+ reject(new Error('Request timeout'));
175
+ });
176
+ req.write(data);
177
+ req.end();
178
+ });
179
+ }
180
+
100
181
  /**
101
182
  * Create a simple HTTP client for screenshots
102
183
  * @private
@@ -104,50 +185,29 @@ function getClient() {
104
185
  function createSimpleClient(serverUrl) {
105
186
  return {
106
187
  async screenshot(name, imageBuffer, options = {}) {
107
- const controller = new AbortController();
108
- const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
109
188
  try {
110
189
  // If it's a string, assume it's a file path and send directly
111
190
  // Otherwise it's a Buffer, so convert to base64
112
191
  const image = typeof imageBuffer === 'string' ? imageBuffer : imageBuffer.toString('base64');
113
- const response = await fetch(`${serverUrl}/screenshot`, {
114
- method: 'POST',
115
- headers: {
116
- 'Content-Type': 'application/json'
117
- },
118
- body: JSON.stringify({
119
- buildId: getBuildId(),
120
- name,
121
- image,
122
- properties: options,
123
- fullPage: options.fullPage || false
124
- }),
125
- signal: controller.signal
126
- });
127
- if (!response.ok) {
128
- const errorData = await response.json().catch(async () => {
129
- const errorText = await response.text().catch(() => 'Unknown error');
130
- return {
131
- error: errorText
132
- };
133
- });
134
-
135
- // In TDD mode, if we get 422 (visual difference), log but DON'T throw
192
+ const {
193
+ status,
194
+ json
195
+ } = await httpPost(`${serverUrl}/screenshot`, {
196
+ buildId: getBuildId(),
197
+ name,
198
+ image,
199
+ properties: options,
200
+ fullPage: options.fullPage || false
201
+ }, DEFAULT_TIMEOUT_MS);
202
+ if (status < 200 || status >= 300) {
203
+ // In TDD mode, if we get 422 (visual difference), don't throw
136
204
  // This allows all screenshots in the test to be captured and compared
137
- if (response.status === 422 && errorData.tddMode && errorData.comparison) {
138
- const comp = errorData.comparison;
139
- const diffPercent = comp.diffPercentage ? comp.diffPercentage.toFixed(2) : '0.00';
140
-
141
- // Extract port from serverUrl (e.g., "http://localhost:47392" -> "47392")
142
- const urlMatch = serverUrl.match(/:(\d+)/);
143
- const port = urlMatch ? urlMatch[1] : '47392';
144
- const dashboardUrl = `http://localhost:${port}/dashboard`;
145
-
146
- // Just log warning - don't throw by default in TDD mode
147
- // This allows all screenshots to be captured
148
- console.warn(`⚠️ Visual diff: ${comp.name} (${diffPercent}%) → ${dashboardUrl}`);
205
+ // The summary will show all failures at the end
206
+ if (status === 422 && json.tddMode && json.comparison) {
207
+ let comp = json.comparison;
149
208
 
150
209
  // Return success so test continues and captures remaining screenshots
210
+ // Visual diff details will be shown in the summary after tests complete
151
211
  return {
152
212
  success: true,
153
213
  status: 'failed',
@@ -155,46 +215,56 @@ function createSimpleClient(serverUrl) {
155
215
  diffPercentage: comp.diffPercentage
156
216
  };
157
217
  }
158
- throw new Error(`Screenshot failed: ${response.status} ${response.statusText} - ${errorData.error || 'Unknown error'}`);
218
+ throw new Error(`Screenshot failed: ${status} - ${json.error || 'Unknown error'}`);
159
219
  }
160
- clearTimeout(timeoutId);
161
- return await response.json();
220
+ return json;
162
221
  } catch (error) {
163
- clearTimeout(timeoutId);
164
-
165
- // Handle timeout (AbortError)
166
- if (error.name === 'AbortError') {
167
- console.error(`Vizzly screenshot timed out for "${name}" after ${DEFAULT_TIMEOUT_MS / 1000}s`);
168
- console.error('The server may be overloaded or unresponsive. Check server health.');
169
- disableVizzly('timeout');
222
+ // Handle timeout
223
+ if (error.message === 'Request timeout') {
224
+ if (shouldLogClient('error')) {
225
+ console.error(`[vizzly] Screenshot timed out for "${name}" after ${DEFAULT_TIMEOUT_MS / 1000}s`);
226
+ }
227
+ disableVizzly();
170
228
  return null;
171
229
  }
172
230
 
173
231
  // In TDD mode with visual differences, throw the error to fail the test
174
232
  if (error.message?.toLowerCase().includes('visual diff')) {
175
- // Clean output for TDD mode - don't spam with additional logs
176
233
  throw error;
177
234
  }
178
- console.error(`Vizzly screenshot failed for ${name}:`, error.message);
179
- if (error.message?.includes('fetch') || error.code === 'ECONNREFUSED') {
180
- console.error(`Server URL: ${serverUrl}/screenshot`);
181
- console.error('This usually means the Vizzly server is not running or not accessible');
182
- console.error('Check that the server is started and the port is correct');
183
- } else if (error.message?.includes('404') || error.message?.includes('Not Found')) {
184
- console.error(`Server URL: ${serverUrl}/screenshot`);
185
- console.error('The screenshot endpoint was not found - check server configuration');
235
+
236
+ // Log connection errors (these indicate setup problems)
237
+ if (shouldLogClient('error')) {
238
+ if (error.code === 'ECONNREFUSED') {
239
+ console.error(`[vizzly] Server not accessible at ${serverUrl}/screenshot`);
240
+ } else if (error.message?.includes('404') || error.message?.includes('Not Found')) {
241
+ console.error(`[vizzly] Screenshot endpoint not found at ${serverUrl}/screenshot`);
242
+ } else {
243
+ console.error(`[vizzly] Screenshot failed for ${name}: ${error.message}`);
244
+ }
186
245
  }
187
246
 
188
247
  // Disable the SDK after first failure to prevent spam
189
- disableVizzly('failure');
248
+ disableVizzly();
190
249
 
191
- // Don't throw - just return silently to not break tests (except TDD mode)
250
+ // Don't throw - just return silently to not break tests
192
251
  return null;
193
252
  }
194
253
  },
195
254
  async flush() {
196
- // Simple client doesn't need explicit flushing
197
- return Promise.resolve();
255
+ // Call the /flush endpoint to signal test completion and trigger summary output
256
+ try {
257
+ let {
258
+ status,
259
+ json
260
+ } = await httpPost(`${serverUrl}/flush`, {}, DEFAULT_TIMEOUT_MS);
261
+ if (status >= 200 && status < 300) {
262
+ return json;
263
+ }
264
+ } catch {
265
+ // Silently ignore flush errors - server may not be running
266
+ }
267
+ return null;
198
268
  }
199
269
  };
200
270
  }
@@ -240,13 +310,10 @@ export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
240
310
  if (isVizzlyDisabled()) {
241
311
  return; // Silently skip when disabled
242
312
  }
243
- const client = getClient();
313
+ let client = getClient();
244
314
  if (!client) {
245
- if (!hasLoggedWarning) {
246
- console.warn('Vizzly client not initialized. Screenshots will be skipped.');
247
- hasLoggedWarning = true;
248
- disableVizzly();
249
- }
315
+ // Silently disable - no server running, nothing to do
316
+ disableVizzly();
250
317
  return;
251
318
  }
252
319