@vizzly-testing/cli 0.20.1-beta.0 → 0.20.1-beta.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.
- package/dist/cli.js +177 -2
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +118 -33
- package/dist/commands/finalize.js +8 -3
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +42 -49
- package/dist/commands/logout.js +13 -5
- package/dist/commands/project.js +95 -67
- package/dist/commands/run.js +32 -6
- package/dist/commands/status.js +81 -50
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +4 -25
- package/dist/commands/upload.js +18 -9
- package/dist/commands/whoami.js +40 -38
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +11 -11
- package/dist/server/handlers/tdd-handler.js +113 -7
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +5 -2
- package/dist/server-manager/operations.js +2 -1
- package/dist/tdd/tdd-service.js +190 -126
- package/dist/types/client.d.ts +25 -2
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +12 -7
- package/dist/report-generator/core.js +0 -315
- package/dist/report-generator/index.js +0 -8
- package/dist/report-generator/operations.js +0 -196
- package/dist/services/static-report-generator.js +0 -65
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
|
-
|
|
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:
|
|
221
|
+
color: colorOverride,
|
|
47
222
|
json: process.argv.includes('--json')
|
|
48
223
|
});
|
|
49
224
|
const config = await loadConfig(configPath, {});
|
package/dist/client/index.js
CHANGED
|
@@ -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(
|
|
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
|
-
* @
|
|
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 (
|
|
77
|
+
if (exists(serverJsonPath)) {
|
|
54
78
|
try {
|
|
55
|
-
const serverInfo = JSON.parse(
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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: ${
|
|
218
|
+
throw new Error(`Screenshot failed: ${status} - ${json.error || 'Unknown error'}`);
|
|
159
219
|
}
|
|
160
|
-
|
|
161
|
-
return await response.json();
|
|
220
|
+
return json;
|
|
162
221
|
} catch (error) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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(
|
|
248
|
+
disableVizzly();
|
|
190
249
|
|
|
191
|
-
// Don't throw - just return silently to not break tests
|
|
250
|
+
// Don't throw - just return silently to not break tests
|
|
192
251
|
return null;
|
|
193
252
|
}
|
|
194
253
|
},
|
|
195
254
|
async flush() {
|
|
196
|
-
//
|
|
197
|
-
|
|
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
|
-
|
|
313
|
+
let client = getClient();
|
|
244
314
|
if (!client) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
hasLoggedWarning = true;
|
|
248
|
-
disableVizzly();
|
|
249
|
-
}
|
|
315
|
+
// Silently disable - no server running, nothing to do
|
|
316
|
+
disableVizzly();
|
|
250
317
|
return;
|
|
251
318
|
}
|
|
252
319
|
|