@vizzly-testing/cli 0.37.0 → 0.38.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/README.md +14 -1
- package/dist/api/core.js +6 -0
- package/dist/auth/client.js +4 -1
- package/dist/auth/core.js +64 -3
- package/dist/auth/operations.js +21 -11
- package/dist/cli.js +14 -0
- package/dist/commands/login.js +71 -137
- package/dist/commands/run.js +5 -1
- package/dist/commands/tdd-screenshots.js +417 -0
- package/dist/commands/tdd.js +5 -1
- package/dist/commands/upload.js +5 -1
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +39 -104
- package/dist/server/routers/auth.js +32 -42
- package/dist/test-runner/core.js +2 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/uploader/core.js +2 -0
- package/dist/utils/browser.js +7 -1
- package/dist/utils/ci-env.js +43 -4
- package/dist/utils/git.js +46 -6
- package/dist/utils/login-poll-wait.js +50 -0
- package/package.json +27 -27
- package/skills/vizzly/references/cli-context.md +25 -0
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Handles authentication endpoints (device flow login, logout, status)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { buildTokens, normalizeDeviceFlowResponse } from '../../auth/core.js';
|
|
6
7
|
import * as output from '../../utils/output.js';
|
|
7
8
|
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
8
|
-
import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
|
|
9
|
+
import { sendError, sendJson, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Create auth router
|
|
@@ -50,22 +51,20 @@ export function createAuthRouter({
|
|
|
50
51
|
// Initiate device flow login
|
|
51
52
|
if (req.method === 'POST' && pathname === '/api/auth/login') {
|
|
52
53
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
expiresIn: deviceFlow.expires_in,
|
|
54
|
+
let deviceFlow = await authService.initiateDeviceFlow();
|
|
55
|
+
let flow = normalizeDeviceFlowResponse(deviceFlow);
|
|
56
|
+
sendSuccess(res, {
|
|
57
|
+
deviceCode: flow.deviceCode,
|
|
58
|
+
userCode: flow.userCode,
|
|
59
|
+
verificationUri: flow.verificationUri,
|
|
60
|
+
verificationUriComplete: flow.urlWithCode,
|
|
61
|
+
expiresIn: deviceFlow.expires_in ?? deviceFlow.expiresIn,
|
|
62
62
|
interval: deviceFlow.interval
|
|
63
|
-
};
|
|
64
|
-
sendSuccess(res, response);
|
|
63
|
+
});
|
|
65
64
|
return true;
|
|
66
65
|
} catch (error) {
|
|
67
66
|
output.error('Error initiating device flow:', error);
|
|
68
|
-
|
|
67
|
+
sendAuthError(res, error);
|
|
69
68
|
return true;
|
|
70
69
|
}
|
|
71
70
|
}
|
|
@@ -81,45 +80,25 @@ export function createAuthRouter({
|
|
|
81
80
|
sendError(res, 400, 'deviceCode is required');
|
|
82
81
|
return true;
|
|
83
82
|
}
|
|
84
|
-
let result;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} catch (error) {
|
|
88
|
-
// Handle "Authorization pending" as a valid response
|
|
89
|
-
if (error.message?.includes('Authorization pending')) {
|
|
90
|
-
sendSuccess(res, {
|
|
91
|
-
status: 'pending'
|
|
92
|
-
});
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Check if authorization is complete by looking for tokens
|
|
99
|
-
if (result.tokens?.accessToken) {
|
|
100
|
-
const tokensData = result.tokens;
|
|
101
|
-
const tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
|
|
102
|
-
const tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : result.expires_at || result.expiresAt;
|
|
103
|
-
const tokens = {
|
|
104
|
-
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
105
|
-
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
106
|
-
expiresAt: tokenExpiresAt,
|
|
107
|
-
user: result.user
|
|
108
|
-
};
|
|
83
|
+
let result = await authService.pollDeviceAuthorization(deviceCode);
|
|
84
|
+
if (result.tokens?.accessToken || result.tokens?.access_token) {
|
|
85
|
+
let tokens = buildTokens(result);
|
|
109
86
|
await authService.completeDeviceFlow(tokens);
|
|
110
87
|
sendSuccess(res, {
|
|
111
88
|
status: 'complete',
|
|
112
89
|
user: result.user
|
|
113
90
|
});
|
|
114
|
-
} else {
|
|
91
|
+
} else if (['pending', 'expired', 'denied', 'invalid'].includes(result.status)) {
|
|
115
92
|
sendSuccess(res, {
|
|
116
|
-
status:
|
|
93
|
+
status: result.status
|
|
117
94
|
});
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error('Unexpected response from authorization server');
|
|
118
97
|
}
|
|
119
98
|
return true;
|
|
120
99
|
} catch (error) {
|
|
121
100
|
output.error('Error polling device authorization:', error);
|
|
122
|
-
|
|
101
|
+
sendAuthError(res, error);
|
|
123
102
|
return true;
|
|
124
103
|
}
|
|
125
104
|
}
|
|
@@ -135,10 +114,21 @@ export function createAuthRouter({
|
|
|
135
114
|
return true;
|
|
136
115
|
} catch (error) {
|
|
137
116
|
output.error('Error logging out:', error);
|
|
138
|
-
|
|
117
|
+
sendAuthError(res, error);
|
|
139
118
|
return true;
|
|
140
119
|
}
|
|
141
120
|
}
|
|
142
121
|
return false;
|
|
143
122
|
};
|
|
123
|
+
}
|
|
124
|
+
function sendAuthError(res, error) {
|
|
125
|
+
let retryAfter = error.context?.retryAfter;
|
|
126
|
+
if (retryAfter) res.setHeader('Retry-After', String(retryAfter));
|
|
127
|
+
sendJson(res, error.context?.status || 500, {
|
|
128
|
+
error: error.message,
|
|
129
|
+
code: error.context?.serverCode || error.code,
|
|
130
|
+
...(retryAfter ? {
|
|
131
|
+
retryAfter
|
|
132
|
+
} : {})
|
|
133
|
+
});
|
|
144
134
|
}
|
package/dist/test-runner/core.js
CHANGED
|
@@ -64,6 +64,8 @@ export function buildApiBuildPayload(options, comparisonConfig = null) {
|
|
|
64
64
|
environment: options.environment || 'test',
|
|
65
65
|
commit_sha: options.commit,
|
|
66
66
|
commit_message: options.message,
|
|
67
|
+
commit_author_name: options.commitAuthorName,
|
|
68
|
+
commit_author_email: options.commitAuthorEmail,
|
|
67
69
|
github_pull_request_number: options.pullRequestNumber,
|
|
68
70
|
parallel_id: options.parallelId
|
|
69
71
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -165,6 +165,8 @@ export interface UploadOptions {
|
|
|
165
165
|
branch?: string;
|
|
166
166
|
commit?: string;
|
|
167
167
|
message?: string;
|
|
168
|
+
commitAuthorName?: string;
|
|
169
|
+
commitAuthorEmail?: string;
|
|
168
170
|
environment?: string;
|
|
169
171
|
threshold?: number;
|
|
170
172
|
minClusterSize?: number;
|
|
@@ -517,6 +519,8 @@ export interface BuildOptions {
|
|
|
517
519
|
commit_sha?: string;
|
|
518
520
|
message?: string;
|
|
519
521
|
commit_message?: string;
|
|
522
|
+
commitAuthorName?: string;
|
|
523
|
+
commitAuthorEmail?: string;
|
|
520
524
|
environment?: string;
|
|
521
525
|
threshold?: number;
|
|
522
526
|
eager?: boolean;
|
package/dist/uploader/core.js
CHANGED
|
@@ -154,6 +154,8 @@ export function buildBuildInfo(options, defaultBranch = 'main') {
|
|
|
154
154
|
branch: options.branch || defaultBranch || 'main',
|
|
155
155
|
commit_sha: options.commit,
|
|
156
156
|
commit_message: options.message,
|
|
157
|
+
commit_author_name: options.commitAuthorName,
|
|
158
|
+
commit_author_email: options.commitAuthorEmail,
|
|
157
159
|
environment: options.environment || 'production',
|
|
158
160
|
threshold: options.threshold,
|
|
159
161
|
github_pull_request_number: options.pullRequestNumber,
|
package/dist/utils/browser.js
CHANGED
|
@@ -52,7 +52,13 @@ export async function openBrowser(url) {
|
|
|
52
52
|
args = [url];
|
|
53
53
|
break;
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
// Some launchers stay in the foreground. Bound the attempt so login and
|
|
57
|
+
// other CLI workflows can continue using the displayed URL.
|
|
58
|
+
execFile(command, args, {
|
|
59
|
+
timeout: 5000,
|
|
60
|
+
killSignal: 'SIGKILL'
|
|
61
|
+
}, error => {
|
|
56
62
|
if (error) {
|
|
57
63
|
// Browser opening failed, but don't throw - user can manually open
|
|
58
64
|
resolve(false);
|
package/dist/utils/ci-env.js
CHANGED
|
@@ -156,13 +156,23 @@ export function getCommit() {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/**
|
|
159
|
-
* Get the commit message from CI environment variables
|
|
159
|
+
* Get the commit message from CI environment variables.
|
|
160
|
+
*
|
|
161
|
+
* For GitHub Actions pull_request events, the checkout is a synthetic merge
|
|
162
|
+
* commit whose message is "Merge <sha> into <sha>", so the PR title from the
|
|
163
|
+
* event payload is used instead.
|
|
164
|
+
*
|
|
160
165
|
* @returns {string|null} Commit message or null if not available
|
|
161
166
|
*/
|
|
162
167
|
export function getCommitMessage() {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
if (process.env.VIZZLY_COMMIT_MESSAGE) {
|
|
169
|
+
return process.env.VIZZLY_COMMIT_MESSAGE;
|
|
170
|
+
}
|
|
171
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
172
|
+
let title = getGitHubEvent().pull_request?.title;
|
|
173
|
+
if (title) return title;
|
|
174
|
+
}
|
|
175
|
+
return process.env.CI_COMMIT_MESSAGE ||
|
|
166
176
|
// GitLab CI
|
|
167
177
|
process.env.TRAVIS_COMMIT_MESSAGE ||
|
|
168
178
|
// Travis CI
|
|
@@ -176,6 +186,35 @@ export function getCommitMessage() {
|
|
|
176
186
|
// Generic
|
|
177
187
|
null;
|
|
178
188
|
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parse a "Name <email>" author string (GitLab's CI_COMMIT_AUTHOR format)
|
|
192
|
+
* @param {string|undefined} value - Author string
|
|
193
|
+
* @returns {{ name: string|null, email: string|null }}
|
|
194
|
+
*/
|
|
195
|
+
function parseAuthorString(value) {
|
|
196
|
+
let match = value?.match(/^(.*?)\s*<([^>]*)>\s*$/);
|
|
197
|
+
if (!match) return {
|
|
198
|
+
name: value?.trim() || null,
|
|
199
|
+
email: null
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
name: match[1] || null,
|
|
203
|
+
email: match[2] || null
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the commit author from CI environment variables
|
|
209
|
+
* @returns {{ name: string|null, email: string|null }} Author name and email
|
|
210
|
+
*/
|
|
211
|
+
export function getCommitAuthor() {
|
|
212
|
+
let gitlab = parseAuthorString(process.env.CI_COMMIT_AUTHOR);
|
|
213
|
+
return {
|
|
214
|
+
name: process.env.VIZZLY_COMMIT_AUTHOR_NAME || gitlab.name || null,
|
|
215
|
+
email: process.env.VIZZLY_COMMIT_AUTHOR_EMAIL || gitlab.email || null
|
|
216
|
+
};
|
|
217
|
+
}
|
|
179
218
|
function parsePullRequestNumber(value) {
|
|
180
219
|
if (!/^\d+$/.test(value)) {
|
|
181
220
|
return null;
|
package/dist/utils/git.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
-
import { getBranch as getCIBranch, getCommit as getCICommit, getCommitMessage as getCICommitMessage, getPullRequestNumber } from './ci-env.js';
|
|
3
|
+
import { getBranch as getCIBranch, getCommit as getCICommit, getCommitAuthor as getCICommitAuthor, getCommitMessage as getCICommitMessage, getPullRequestNumber } from './ci-env.js';
|
|
4
4
|
let execFileAsync = promisify(execFile);
|
|
5
5
|
async function runGit(args, cwd = process.cwd()) {
|
|
6
6
|
let {
|
|
@@ -124,8 +124,48 @@ export async function detectCommitMessage(override = null, cwd = process.cwd())
|
|
|
124
124
|
let ciCommitMessage = getCICommitMessage();
|
|
125
125
|
if (ciCommitMessage) return ciCommitMessage;
|
|
126
126
|
|
|
127
|
-
// Fallback to
|
|
128
|
-
return await
|
|
127
|
+
// Fallback to git, reading the same commit reported as commit_sha
|
|
128
|
+
return await readCommit('%B', cwd);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read a formatted field from the detected build commit.
|
|
133
|
+
*
|
|
134
|
+
* Uses detectCommit() so CI checkouts (e.g. GitHub's synthetic PR merge
|
|
135
|
+
* commit) describe the PR head rather than HEAD. Falls back to HEAD when that
|
|
136
|
+
* commit is not in local history (shallow clones).
|
|
137
|
+
*
|
|
138
|
+
* @param {string} format - git log --format string
|
|
139
|
+
* @param {string} cwd - Working directory
|
|
140
|
+
* @returns {Promise<string|null>} Formatted output or null
|
|
141
|
+
*/
|
|
142
|
+
async function readCommit(format, cwd = process.cwd()) {
|
|
143
|
+
let sha = await detectCommit(null, cwd);
|
|
144
|
+
let refs = /^[0-9a-f]{4,64}$/i.test(sha || '') ? [sha, 'HEAD'] : ['HEAD'];
|
|
145
|
+
for (let ref of refs) {
|
|
146
|
+
try {
|
|
147
|
+
return await runGit(['log', '-1', `--format=${format}`, ref], cwd);
|
|
148
|
+
} catch {
|
|
149
|
+
// Commit not available locally, try the next ref
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect commit author with environment variable support, falling back to git
|
|
157
|
+
* @param {string} cwd - Working directory
|
|
158
|
+
* @returns {Promise<{ name: string|null, email: string|null }>}
|
|
159
|
+
*/
|
|
160
|
+
export async function detectCommitAuthor(cwd = process.cwd()) {
|
|
161
|
+
let author = getCICommitAuthor();
|
|
162
|
+
if (author.name && author.email) return author;
|
|
163
|
+
let stdout = await readCommit('%an%x00%ae', cwd);
|
|
164
|
+
let [gitName, gitEmail] = stdout ? stdout.split('\0') : [];
|
|
165
|
+
return {
|
|
166
|
+
name: author.name || gitName || null,
|
|
167
|
+
email: author.email || gitEmail || null
|
|
168
|
+
};
|
|
129
169
|
}
|
|
130
170
|
|
|
131
171
|
/**
|
|
@@ -206,9 +246,9 @@ export async function detectCommit(override = null, cwd = process.cwd()) {
|
|
|
206
246
|
*/
|
|
207
247
|
export async function generateBuildNameWithGit(override = null, cwd = process.cwd()) {
|
|
208
248
|
if (override) return override;
|
|
209
|
-
let branch = await
|
|
210
|
-
let shortSha = await
|
|
211
|
-
if (branch && shortSha) {
|
|
249
|
+
let branch = await detectBranch(null, cwd);
|
|
250
|
+
let shortSha = await detectCommit(null, cwd);
|
|
251
|
+
if (branch && branch !== 'unknown' && shortSha) {
|
|
212
252
|
let shortCommit = shortSha.substring(0, 7);
|
|
213
253
|
return `${branch}-${shortCommit}`;
|
|
214
254
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
|
|
3
|
+
/** Wait for the next check, with optional terminal input and a live countdown. */
|
|
4
|
+
export async function waitForLoginPoll({
|
|
5
|
+
milliseconds,
|
|
6
|
+
manualAllowed = true,
|
|
7
|
+
earliestCheckAt = 0,
|
|
8
|
+
stdin = process.stdin,
|
|
9
|
+
now = Date.now,
|
|
10
|
+
sleep = delay,
|
|
11
|
+
onTick
|
|
12
|
+
}) {
|
|
13
|
+
if (!stdin.isTTY) {
|
|
14
|
+
onTick(milliseconds, false);
|
|
15
|
+
await sleep(milliseconds);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let deadline = now() + milliseconds;
|
|
19
|
+
let requested = false;
|
|
20
|
+
let timer;
|
|
21
|
+
let wasFlowing = stdin.readableFlowing === true;
|
|
22
|
+
let onData = data => {
|
|
23
|
+
if (manualAllowed && /[\r\n]/.test(data.toString())) {
|
|
24
|
+
requested = true;
|
|
25
|
+
timer?.abort();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
if (manualAllowed) {
|
|
29
|
+
stdin.on('data', onData);
|
|
30
|
+
stdin.resume();
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
while (true) {
|
|
34
|
+
let remaining = Math.min(deadline, requested ? earliestCheckAt : deadline) - now();
|
|
35
|
+
if (remaining <= 0) return;
|
|
36
|
+
onTick(remaining, manualAllowed && !requested);
|
|
37
|
+
timer = new AbortController();
|
|
38
|
+
try {
|
|
39
|
+
await sleep(Math.min(remaining, 1000), undefined, {
|
|
40
|
+
signal: timer.signal
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!timer.signal.aborted) throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
stdin.removeListener('data', onData);
|
|
48
|
+
if (manualAllowed && !wasFlowing) stdin.pause();
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.1",
|
|
4
4
|
"description": "Visual regression testing from your terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm-context",
|
|
@@ -87,24 +87,24 @@
|
|
|
87
87
|
"fix": "pnpm run format && pnpm run lint:fix"
|
|
88
88
|
},
|
|
89
89
|
"engines": {
|
|
90
|
-
"node": "
|
|
90
|
+
"node": "^22.18.0 || >=24.0.0"
|
|
91
91
|
},
|
|
92
92
|
"publishConfig": {
|
|
93
93
|
"access": "public",
|
|
94
94
|
"registry": "https://registry.npmjs.org/"
|
|
95
95
|
},
|
|
96
96
|
"dependencies": {
|
|
97
|
-
"@vizzly-testing/bear-den": "0.1.
|
|
97
|
+
"@vizzly-testing/bear-den": "0.1.6",
|
|
98
98
|
"@vizzly-testing/honeydiff": "^0.17.0",
|
|
99
|
-
"ansis": "^4.
|
|
99
|
+
"ansis": "^4.4.0",
|
|
100
100
|
"commander": "^15.0.0",
|
|
101
|
-
"cosmiconfig": "^
|
|
102
|
-
"dotenv": "^17.2
|
|
103
|
-
"form-data": "^4.0.
|
|
104
|
-
"glob": "^13.0.
|
|
105
|
-
"react": "^19.
|
|
106
|
-
"react-dom": "^19.
|
|
107
|
-
"zod": "^4.
|
|
101
|
+
"cosmiconfig": "^10.0.1",
|
|
102
|
+
"dotenv": "^17.4.2",
|
|
103
|
+
"form-data": "^4.0.6",
|
|
104
|
+
"glob": "^13.0.6",
|
|
105
|
+
"react": "^19.3.0",
|
|
106
|
+
"react-dom": "^19.3.0",
|
|
107
|
+
"zod": "^4.6.5"
|
|
108
108
|
},
|
|
109
109
|
"tsd": {
|
|
110
110
|
"directory": "test-d",
|
|
@@ -117,29 +117,29 @@
|
|
|
117
117
|
}
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
|
-
"@babel/cli": "^8.0.
|
|
121
|
-
"@babel/core": "^8.0.
|
|
122
|
-
"@babel/preset-env": "^8.0.
|
|
120
|
+
"@babel/cli": "^8.0.6",
|
|
121
|
+
"@babel/core": "^8.0.6",
|
|
122
|
+
"@babel/preset-env": "^8.0.6",
|
|
123
123
|
"@babel/preset-react": "^8.0.1",
|
|
124
124
|
"@babel/preset-typescript": "^8.0.1",
|
|
125
|
-
"@biomejs/biome": "^2.5.
|
|
125
|
+
"@biomejs/biome": "^2.5.15",
|
|
126
126
|
"@heroicons/react": "^2.2.0",
|
|
127
|
-
"@modelcontextprotocol/sdk": "^1.0
|
|
128
|
-
"@playwright/test": "^1.
|
|
129
|
-
"@tailwindcss/postcss": "^4.
|
|
130
|
-
"@tanstack/react-query": "^5.
|
|
131
|
-
"@types/node": "^26.
|
|
132
|
-
"@vitejs/plugin-react": "^6.
|
|
133
|
-
"autoprefixer": "^10.
|
|
127
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
128
|
+
"@playwright/test": "^1.63.0",
|
|
129
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
130
|
+
"@tanstack/react-query": "^5.104.0",
|
|
131
|
+
"@types/node": "^26.6.3",
|
|
132
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
133
|
+
"autoprefixer": "^10.6.1",
|
|
134
134
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
135
135
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
|
136
|
-
"postcss": "^8.5.
|
|
137
|
-
"rimraf": "^6.
|
|
138
|
-
"tailwindcss": "^4.
|
|
136
|
+
"postcss": "^8.5.29",
|
|
137
|
+
"rimraf": "^6.1.3",
|
|
138
|
+
"tailwindcss": "^4.3.3",
|
|
139
139
|
"tsd": "^0.33.0",
|
|
140
140
|
"tui-driver": "github:vizzly-testing/tui-driver",
|
|
141
141
|
"typescript": "^7.0.2",
|
|
142
|
-
"vite": "^8.
|
|
143
|
-
"wouter": "^3.
|
|
142
|
+
"vite": "^8.3.1",
|
|
143
|
+
"wouter": "^3.11.1"
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -86,6 +86,31 @@ vizzly tdd stop --json
|
|
|
86
86
|
`tdd run` and `tdd start` are alternatives. Stop only a daemon started for the
|
|
87
87
|
current task.
|
|
88
88
|
|
|
89
|
+
## Review A Local TDD Run
|
|
90
|
+
|
|
91
|
+
After `tdd run` exits, inspect captures directly from `.vizzly`. These
|
|
92
|
+
commands don't need a running TDD daemon or cloud credentials:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
vizzly tdd screenshots list --page 1 --page-size 20
|
|
96
|
+
vizzly tdd screenshots latest
|
|
97
|
+
vizzly tdd screenshots show <comparison-id>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`latest` prints the current screenshot path for the most recent capture. Use
|
|
101
|
+
`list` to find a comparison ID when you need a specific screenshot or browser
|
|
102
|
+
variant. During UI iteration, inspect the current image first; request
|
|
103
|
+
`--image diff` or `--image all` when comparison diagnostics help answer the
|
|
104
|
+
question. Open the printed image paths with the available image viewer.
|
|
105
|
+
|
|
106
|
+
`accept` replaces one local baseline and updates that report entry. Use it only
|
|
107
|
+
when the task explicitly authorizes accepting a baseline; don't accept a change
|
|
108
|
+
just to make a diff disappear:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
vizzly tdd screenshots accept <comparison-id>
|
|
112
|
+
```
|
|
113
|
+
|
|
89
114
|
When a cloud build is in scope:
|
|
90
115
|
|
|
91
116
|
```bash
|