@vizzly-testing/cli 0.33.0 → 0.35.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.
- package/README.md +53 -3
- package/dist/api/core.js +2 -1
- package/dist/cli.js +21 -8
- package/dist/client/index.js +62 -21
- package/dist/commands/api.js +9 -3
- package/dist/commands/init.js +214 -2
- package/dist/commands/preview.js +13 -11
- package/dist/commands/project.js +1 -1
- package/dist/commands/run.js +18 -6
- package/dist/commands/tdd-daemon.js +8 -3
- package/dist/commands/tdd.js +24 -4
- package/dist/commands/upload.js +31 -13
- package/dist/reporter/reporter-bundle.iife.js +1 -1
- package/dist/sdk/index.js +23 -9
- package/dist/server/handlers/api-handler.js +3 -2
- package/dist/server/handlers/tdd-handler.js +5 -1
- package/dist/server/routers/screenshot.js +11 -4
- package/dist/services/build-manager.js +6 -1
- package/dist/test-runner/core.js +4 -1
- package/dist/test-runner/operations.js +2 -1
- package/dist/types/client.d.ts +32 -5
- package/dist/types/index.d.ts +33 -5
- package/dist/uploader/core.js +41 -9
- package/dist/uploader/operations.js +19 -17
- package/dist/utils/config-loader.js +12 -6
- package/dist/utils/context.js +46 -76
- package/dist/utils/environment-config.js +20 -0
- package/dist/utils/output.js +5 -5
- package/dist/utils/screenshot-options.js +60 -10
- package/package.json +2 -1
- package/skills/vizzly/SKILL.md +99 -0
- package/skills/vizzly/agents/openai.yaml +4 -0
- package/skills/vizzly/references/cli-context.md +42 -0
- package/skills/vizzly/references/dynamic-content.md +44 -0
- package/skills/vizzly/references/public-screenshots.md +44 -0
- package/skills/vizzly/references/sdks.md +62 -0
- package/skills/vizzly/references/setup-ci.md +42 -0
|
@@ -386,23 +386,25 @@ export async function upload({
|
|
|
386
386
|
let buildInfo = buildBuildInfo(uploadOptions, defaultBranch);
|
|
387
387
|
let build = await createBuild(client, buildInfo);
|
|
388
388
|
let buildId = build.id;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
let
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
389
|
+
let toUpload = fileMetadata;
|
|
390
|
+
let existing = [];
|
|
391
|
+
let screenshots = [];
|
|
392
|
+
if (!uploadOptions.uploadAll) {
|
|
393
|
+
let existingResult = await checkExistingFiles({
|
|
394
|
+
fileMetadata,
|
|
395
|
+
client,
|
|
396
|
+
signal,
|
|
397
|
+
buildId,
|
|
398
|
+
deps: {
|
|
399
|
+
checkShas,
|
|
400
|
+
createError,
|
|
401
|
+
output
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
toUpload = existingResult.toUpload;
|
|
405
|
+
existing = existingResult.existing;
|
|
406
|
+
screenshots = existingResult.screenshots;
|
|
407
|
+
}
|
|
406
408
|
onProgress(buildDeduplicationProgress(toUpload.length, existing.length, files.length));
|
|
407
409
|
|
|
408
410
|
// Upload files
|
|
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
3
3
|
import { CONFIG_DEFAULTS, deepMerge } from '../config/core.js';
|
|
4
4
|
import { validateVizzlyConfigWithDefaults } from './config-schema.js';
|
|
5
|
-
import { getApiToken, getApiUrl, getBuildName, getParallelId } from './environment-config.js';
|
|
5
|
+
import { getApiToken, getApiUrl, getBuildName, getMinClusterSize, getParallelId, getThreshold } from './environment-config.js';
|
|
6
6
|
import { getAccessToken } from './global-config.js';
|
|
7
7
|
import * as output from './output.js';
|
|
8
8
|
import { getActiveProjectLink } from './project-link-store.js';
|
|
@@ -26,10 +26,12 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
26
26
|
mergeConfig(config, validatedFileConfig);
|
|
27
27
|
|
|
28
28
|
// 3. Override with environment variables (higher priority than fallbacks)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
let envApiKey = getApiToken();
|
|
30
|
+
let envApiUrl = getApiUrl();
|
|
31
|
+
let envBuildName = getBuildName();
|
|
32
|
+
let envParallelId = getParallelId();
|
|
33
|
+
let envThreshold = getThreshold();
|
|
34
|
+
let envMinClusterSize = getMinClusterSize();
|
|
33
35
|
if (envApiKey) {
|
|
34
36
|
config.apiKey = envApiKey;
|
|
35
37
|
output.debug('config', 'using token from environment');
|
|
@@ -40,6 +42,10 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
40
42
|
output.debug('config', 'using build name from environment');
|
|
41
43
|
}
|
|
42
44
|
if (envParallelId) config.parallelId = envParallelId;
|
|
45
|
+
if (envThreshold !== undefined) config.comparison.threshold = envThreshold;
|
|
46
|
+
if (envMinClusterSize !== undefined) {
|
|
47
|
+
config.comparison.minClusterSize = envMinClusterSize;
|
|
48
|
+
}
|
|
43
49
|
|
|
44
50
|
// 4. Apply CLI overrides (highest priority)
|
|
45
51
|
if (cliOverrides.token) {
|
|
@@ -109,7 +115,7 @@ function applyCLIOverrides(config, cliOverrides = {}) {
|
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
// Comparison overrides
|
|
112
|
-
if (cliOverrides.threshold !== undefined) config.comparison.threshold = cliOverrides.threshold;
|
|
118
|
+
if (cliOverrides.threshold !== undefined) config.comparison.threshold = Number(cliOverrides.threshold);
|
|
113
119
|
if (cliOverrides.minClusterSize !== undefined) {
|
|
114
120
|
config.comparison.minClusterSize = Number(cliOverrides.minClusterSize);
|
|
115
121
|
}
|
package/dist/utils/context.js
CHANGED
|
@@ -11,6 +11,37 @@
|
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
function getGlobalConfigPath() {
|
|
15
|
+
return join(process.env.VIZZLY_HOME || join(homedir(), '.vizzly'), 'config.json');
|
|
16
|
+
}
|
|
17
|
+
function readJsonFile(path) {
|
|
18
|
+
try {
|
|
19
|
+
if (existsSync(path)) {
|
|
20
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// Optional context should never block the command that is rendering it.
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function readGlobalConfig() {
|
|
28
|
+
return readJsonFile(getGlobalConfigPath()) || {};
|
|
29
|
+
}
|
|
30
|
+
function readBaselineInfo(cwd) {
|
|
31
|
+
let path = join(cwd, '.vizzly', 'baselines');
|
|
32
|
+
let metadata = readJsonFile(join(path, 'metadata.json'));
|
|
33
|
+
return {
|
|
34
|
+
count: metadata?.screenshots?.length || 0,
|
|
35
|
+
path: metadata ? path : null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function readServerInfo(cwd) {
|
|
39
|
+
let serverInfo = readJsonFile(join(cwd, '.vizzly', 'server.json'));
|
|
40
|
+
return {
|
|
41
|
+
running: !!serverInfo,
|
|
42
|
+
port: serverInfo ? serverInfo.port : null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
14
45
|
|
|
15
46
|
/**
|
|
16
47
|
* Get dynamic context about the current Vizzly state
|
|
@@ -22,46 +53,12 @@ export function getContext() {
|
|
|
22
53
|
let items = [];
|
|
23
54
|
try {
|
|
24
55
|
let cwd = process.cwd();
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
// Load global config once
|
|
28
|
-
let globalConfig = {};
|
|
29
|
-
try {
|
|
30
|
-
if (existsSync(globalConfigPath)) {
|
|
31
|
-
globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf8'));
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
// Ignore
|
|
35
|
-
}
|
|
56
|
+
let globalConfig = readGlobalConfig();
|
|
36
57
|
|
|
37
58
|
// Check for vizzly.config.js (project config)
|
|
38
59
|
let hasProjectConfig = existsSync(join(cwd, 'vizzly.config.js'));
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let baselineCount = 0;
|
|
42
|
-
try {
|
|
43
|
-
let metaPath = join(cwd, '.vizzly', 'baselines', 'metadata.json');
|
|
44
|
-
if (existsSync(metaPath)) {
|
|
45
|
-
let meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
46
|
-
baselineCount = meta.screenshots?.length || 0;
|
|
47
|
-
}
|
|
48
|
-
} catch {
|
|
49
|
-
// Ignore
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Check for TDD server running
|
|
53
|
-
let serverRunning = false;
|
|
54
|
-
let serverPort = null;
|
|
55
|
-
try {
|
|
56
|
-
let serverFile = join(cwd, '.vizzly', 'server.json');
|
|
57
|
-
if (existsSync(serverFile)) {
|
|
58
|
-
let serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
|
|
59
|
-
serverPort = serverInfo.port;
|
|
60
|
-
serverRunning = true;
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
// Ignore
|
|
64
|
-
}
|
|
60
|
+
let baselineInfo = readBaselineInfo(cwd);
|
|
61
|
+
let serverInfo = readServerInfo(cwd);
|
|
65
62
|
|
|
66
63
|
// Check for OAuth login (from vizzly login)
|
|
67
64
|
let isLoggedIn = !!globalConfig.auth?.accessToken;
|
|
@@ -71,11 +68,11 @@ export function getContext() {
|
|
|
71
68
|
let hasEnvToken = !!process.env.VIZZLY_TOKEN;
|
|
72
69
|
|
|
73
70
|
// Build context items - prioritize most useful info
|
|
74
|
-
if (
|
|
71
|
+
if (serverInfo.running) {
|
|
75
72
|
items.push({
|
|
76
73
|
type: 'success',
|
|
77
74
|
label: 'TDD Server',
|
|
78
|
-
value: `running on :${
|
|
75
|
+
value: `running on :${serverInfo.port}`
|
|
79
76
|
});
|
|
80
77
|
}
|
|
81
78
|
if (isLoggedIn && userName) {
|
|
@@ -97,14 +94,14 @@ export function getContext() {
|
|
|
97
94
|
value: 'run vizzly login'
|
|
98
95
|
});
|
|
99
96
|
}
|
|
100
|
-
if (
|
|
97
|
+
if (baselineInfo.count > 0) {
|
|
101
98
|
items.push({
|
|
102
99
|
type: 'success',
|
|
103
100
|
label: 'Baselines',
|
|
104
|
-
value: `${
|
|
101
|
+
value: `${baselineInfo.count} screenshots`
|
|
105
102
|
});
|
|
106
103
|
}
|
|
107
|
-
if (!hasProjectConfig && !
|
|
104
|
+
if (!hasProjectConfig && !serverInfo.running && baselineInfo.count === 0) {
|
|
108
105
|
// Only show "no config" hint if there's nothing else useful
|
|
109
106
|
items.push({
|
|
110
107
|
type: 'info',
|
|
@@ -126,7 +123,6 @@ export function getContext() {
|
|
|
126
123
|
*/
|
|
127
124
|
export function getDetailedContext() {
|
|
128
125
|
let cwd = process.cwd();
|
|
129
|
-
let globalConfigPath = join(process.env.VIZZLY_HOME || join(homedir(), '.vizzly'), 'config.json');
|
|
130
126
|
let context = {
|
|
131
127
|
tddServer: {
|
|
132
128
|
running: false,
|
|
@@ -146,42 +142,16 @@ export function getDetailedContext() {
|
|
|
146
142
|
}
|
|
147
143
|
};
|
|
148
144
|
try {
|
|
149
|
-
|
|
150
|
-
let globalConfig = {};
|
|
151
|
-
try {
|
|
152
|
-
if (existsSync(globalConfigPath)) {
|
|
153
|
-
globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf8'));
|
|
154
|
-
}
|
|
155
|
-
} catch {
|
|
156
|
-
// Ignore
|
|
157
|
-
}
|
|
145
|
+
let globalConfig = readGlobalConfig();
|
|
158
146
|
|
|
159
147
|
// Check for vizzly.config.js
|
|
160
148
|
context.project.hasConfig = existsSync(join(cwd, 'vizzly.config.js'));
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
context.baselines.count = meta.screenshots?.length || 0;
|
|
168
|
-
context.baselines.path = join(cwd, '.vizzly', 'baselines');
|
|
169
|
-
}
|
|
170
|
-
} catch {
|
|
171
|
-
// Ignore
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Check for TDD server
|
|
175
|
-
try {
|
|
176
|
-
let serverFile = join(cwd, '.vizzly', 'server.json');
|
|
177
|
-
if (existsSync(serverFile)) {
|
|
178
|
-
let serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
|
|
179
|
-
context.tddServer.running = true;
|
|
180
|
-
context.tddServer.port = serverInfo.port;
|
|
181
|
-
}
|
|
182
|
-
} catch {
|
|
183
|
-
// Ignore
|
|
184
|
-
}
|
|
149
|
+
let baselineInfo = readBaselineInfo(cwd);
|
|
150
|
+
context.baselines.count = baselineInfo.count;
|
|
151
|
+
context.baselines.path = baselineInfo.path;
|
|
152
|
+
let serverInfo = readServerInfo(cwd);
|
|
153
|
+
context.tddServer.running = serverInfo.running;
|
|
154
|
+
context.tddServer.port = serverInfo.port;
|
|
185
155
|
|
|
186
156
|
// Check auth status
|
|
187
157
|
context.auth.loggedIn = !!globalConfig.auth?.accessToken;
|
|
@@ -85,6 +85,24 @@ export function getBuildName() {
|
|
|
85
85
|
return process.env.VIZZLY_BUILD_NAME;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Get comparison threshold from environment
|
|
90
|
+
* @returns {number|undefined} Threshold value when set
|
|
91
|
+
*/
|
|
92
|
+
export function getThreshold() {
|
|
93
|
+
if (process.env.VIZZLY_THRESHOLD === undefined) return undefined;
|
|
94
|
+
return Number(process.env.VIZZLY_THRESHOLD);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get minimum changed cluster size from environment
|
|
99
|
+
* @returns {number|undefined} Minimum cluster size when set
|
|
100
|
+
*/
|
|
101
|
+
export function getMinClusterSize() {
|
|
102
|
+
if (process.env.VIZZLY_MIN_CLUSTER_SIZE === undefined) return undefined;
|
|
103
|
+
return Number(process.env.VIZZLY_MIN_CLUSTER_SIZE);
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
/**
|
|
89
107
|
* Check if TDD mode is enabled
|
|
90
108
|
* @returns {boolean} Whether TDD mode is enabled
|
|
@@ -117,6 +135,8 @@ export function getAllEnvironmentConfig() {
|
|
|
117
135
|
buildId: getBuildId(),
|
|
118
136
|
buildName: getBuildName(),
|
|
119
137
|
parallelId: getParallelId(),
|
|
138
|
+
threshold: getThreshold(),
|
|
139
|
+
minClusterSize: getMinClusterSize(),
|
|
120
140
|
tddMode: isTddMode()
|
|
121
141
|
};
|
|
122
142
|
}
|
package/dist/utils/output.js
CHANGED
|
@@ -282,7 +282,7 @@ export function success(message, data = {}) {
|
|
|
282
282
|
});
|
|
283
283
|
if (config.silent) return;
|
|
284
284
|
if (config.json) {
|
|
285
|
-
console.
|
|
285
|
+
console.error(JSON.stringify({
|
|
286
286
|
status: 'success',
|
|
287
287
|
message,
|
|
288
288
|
...data
|
|
@@ -301,7 +301,7 @@ export function result(message) {
|
|
|
301
301
|
if (config.silent) return;
|
|
302
302
|
const elapsed = getElapsedTime();
|
|
303
303
|
if (config.json) {
|
|
304
|
-
console.
|
|
304
|
+
console.error(JSON.stringify({
|
|
305
305
|
status: 'complete',
|
|
306
306
|
message,
|
|
307
307
|
elapsed
|
|
@@ -319,7 +319,7 @@ export function info(message, data = {}) {
|
|
|
319
319
|
writeLog('info', message, data);
|
|
320
320
|
if (!shouldLog('info')) return;
|
|
321
321
|
if (config.json) {
|
|
322
|
-
console.
|
|
322
|
+
console.error(JSON.stringify({
|
|
323
323
|
status: 'info',
|
|
324
324
|
message,
|
|
325
325
|
...data
|
|
@@ -513,7 +513,7 @@ export function stopSpinner() {
|
|
|
513
513
|
export function progress(message, current = 0, total = 0) {
|
|
514
514
|
if (config.silent) return;
|
|
515
515
|
if (config.json) {
|
|
516
|
-
console.
|
|
516
|
+
console.error(JSON.stringify({
|
|
517
517
|
status: 'progress',
|
|
518
518
|
message,
|
|
519
519
|
progress: {
|
|
@@ -941,7 +941,7 @@ export function complete(message, options = {}) {
|
|
|
941
941
|
} = options;
|
|
942
942
|
let detailStr = detail ? ` ${colors.brand.textMuted(detail)}` : '';
|
|
943
943
|
if (config.json) {
|
|
944
|
-
console.
|
|
944
|
+
console.error(JSON.stringify({
|
|
945
945
|
status: 'complete',
|
|
946
946
|
message,
|
|
947
947
|
detail
|
|
@@ -1,21 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot option names that are part of the SDK/config contract, not the
|
|
3
|
+
* user's arbitrary metadata bag.
|
|
4
|
+
*/
|
|
5
|
+
export let RESERVED_PROPERTY_OPTIONS = Object.freeze({
|
|
6
|
+
threshold: {
|
|
7
|
+
message: 'Move "threshold" out of properties; properties is only for user metadata.'
|
|
8
|
+
},
|
|
9
|
+
minClusterSize: {
|
|
10
|
+
message: 'Move "minClusterSize" out of properties; properties is only for user metadata.'
|
|
11
|
+
},
|
|
12
|
+
fullPage: {
|
|
13
|
+
message: 'Move "fullPage" out of properties; properties is only for user metadata.'
|
|
14
|
+
},
|
|
15
|
+
buildId: {
|
|
16
|
+
message: 'Move "buildId" out of properties; properties is only for user metadata.'
|
|
17
|
+
},
|
|
18
|
+
requestTimeout: {
|
|
19
|
+
message: 'Move "requestTimeout" out of properties; properties is only for user metadata.'
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
function createReservedPropertyWarning(option) {
|
|
23
|
+
return {
|
|
24
|
+
code: 'reserved-property-option',
|
|
25
|
+
option,
|
|
26
|
+
message: RESERVED_PROPERTY_OPTIONS[option].message
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
1
30
|
/**
|
|
2
31
|
* Normalize screenshot SDK options into the properties payload consumed by the
|
|
3
32
|
* local TDD server and cloud-compatible comparison path.
|
|
4
33
|
*/
|
|
5
|
-
export function
|
|
34
|
+
export function normalizeScreenshotOptions(options = {}) {
|
|
6
35
|
let {
|
|
7
|
-
buildId
|
|
36
|
+
buildId,
|
|
8
37
|
properties = {},
|
|
9
|
-
requestTimeout
|
|
38
|
+
requestTimeout,
|
|
10
39
|
threshold,
|
|
11
40
|
minClusterSize,
|
|
12
|
-
fullPage
|
|
13
|
-
...topLevelProperties
|
|
41
|
+
fullPage
|
|
14
42
|
} = options;
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
43
|
+
let warnings = [];
|
|
44
|
+
let normalizedProperties = {};
|
|
45
|
+
for (let [key, value] of Object.entries(properties || {})) {
|
|
46
|
+
if (RESERVED_PROPERTY_OPTIONS[key]) {
|
|
47
|
+
warnings.push(createReservedPropertyWarning(key));
|
|
48
|
+
if (key === 'threshold' && threshold === undefined) threshold = value;
|
|
49
|
+
if (key === 'minClusterSize' && minClusterSize === undefined) {
|
|
50
|
+
minClusterSize = value;
|
|
51
|
+
}
|
|
52
|
+
if (key === 'fullPage' && fullPage === undefined) fullPage = value;
|
|
53
|
+
if (key === 'buildId' && buildId === undefined) buildId = value;
|
|
54
|
+
if (key === 'requestTimeout' && requestTimeout === undefined) {
|
|
55
|
+
requestTimeout = value;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
normalizedProperties[key] = value;
|
|
60
|
+
}
|
|
19
61
|
if (threshold !== undefined) {
|
|
20
62
|
normalizedProperties.threshold = threshold;
|
|
21
63
|
}
|
|
@@ -25,5 +67,13 @@ export function createScreenshotProperties(options = {}) {
|
|
|
25
67
|
if (fullPage !== undefined) {
|
|
26
68
|
normalizedProperties.fullPage = fullPage;
|
|
27
69
|
}
|
|
28
|
-
return
|
|
70
|
+
return {
|
|
71
|
+
buildId,
|
|
72
|
+
requestTimeout,
|
|
73
|
+
properties: normalizedProperties,
|
|
74
|
+
warnings
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function createScreenshotProperties(options = {}) {
|
|
78
|
+
return normalizeScreenshotOptions(options).properties;
|
|
29
79
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Visual regression testing from your terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm-context",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"bin",
|
|
56
56
|
"dist",
|
|
57
57
|
"docs/assets",
|
|
58
|
+
"skills",
|
|
58
59
|
"README.md",
|
|
59
60
|
"LICENSE"
|
|
60
61
|
],
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vizzly
|
|
3
|
+
description: "Use when a repo has Vizzly configured and you need screenshot memory or visual history: before/after UI changes, visual regression review, screenshot history, approved baselines, diffs, dynamic regions, docs/manual images, public screenshots, or Vizzly builds. Teaches agents to use Vizzly CLI/context for local TDD, cloud builds, review state, comments, hotspots, previews, and SDK capture patterns."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Vizzly
|
|
7
|
+
|
|
8
|
+
Vizzly is the project's screenshot memory database. It stores approved baselines, current screenshots, diffs, review state, comments, hotspots, previews, and public screenshot URLs.
|
|
9
|
+
|
|
10
|
+
Use Vizzly when you need to understand what the UI looked like before, what changed, what humans already reviewed, or which screenshots already exist. Browser automation is still useful for interacting with a live app; Vizzly is usually the faster first stop for visual history.
|
|
11
|
+
|
|
12
|
+
## First Instinct
|
|
13
|
+
|
|
14
|
+
Before changing UI, check whether Vizzly already has local screenshot context:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
vizzly context build current --source local --agent
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If a task names a screen, component, or screenshot, inspect that screenshot's history before changing thresholds or re-capturing blindly:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
vizzly context screenshot "<screenshot-name>" --source local --json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
When you make or verify UI changes, run the focused user workflow that owns the surface:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
vizzly tdd run "<test command>" --no-open
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then inspect what changed:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
vizzly context build current --source local --agent --json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For specific evidence, drill in:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
vizzly context comparison <comparison-id> --json
|
|
42
|
+
vizzly context screenshot "<screenshot-name>" --json
|
|
43
|
+
vizzly context review-queue --json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If local context is unavailable and the project uses cloud builds, use the build id from CI or CLI output:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
vizzly context build <build-id> --agent --json --include diffs,comments
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What Vizzly Knows
|
|
53
|
+
|
|
54
|
+
- **Approved baselines**: expected UI.
|
|
55
|
+
- **Current screenshots**: what the latest run rendered.
|
|
56
|
+
- **Diffs**: where pixels/layout/content changed.
|
|
57
|
+
- **Review state and comments**: human context attached to builds and screenshots.
|
|
58
|
+
- **Hotspots and confirmed regions**: known dynamic areas.
|
|
59
|
+
- **Preview links**: static or deployed UI context for a build.
|
|
60
|
+
- **Public screenshots**: stable URLs for documentation and manuals.
|
|
61
|
+
|
|
62
|
+
## Acting On Visual Context
|
|
63
|
+
|
|
64
|
+
- Treat approved baselines as visual truth.
|
|
65
|
+
- Treat diffs as evidence, not as approval instructions.
|
|
66
|
+
- Do not approve or reject visual changes unless the user explicitly asks.
|
|
67
|
+
- Prefer existing E2E or user journeys over narrow screenshot-only specs.
|
|
68
|
+
- For dynamic content, inspect screenshot context before changing thresholds.
|
|
69
|
+
- Prefer deterministic test data, per-screenshot `threshold`, per-screenshot `minClusterSize`, hotspots, or confirmed regions over global tolerance changes.
|
|
70
|
+
- Report visual findings with screenshot names, build/comparison links when available, and the command you ran.
|
|
71
|
+
|
|
72
|
+
## Capturing Screenshots
|
|
73
|
+
|
|
74
|
+
Use the existing integration when one is present. For direct JavaScript capture:
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
|
|
78
|
+
|
|
79
|
+
let screenshot = await page.screenshot();
|
|
80
|
+
await vizzlyScreenshot('checkout-form', screenshot, {
|
|
81
|
+
properties: {
|
|
82
|
+
browser: 'chromium',
|
|
83
|
+
viewport: 'desktop',
|
|
84
|
+
state: 'valid-card'
|
|
85
|
+
},
|
|
86
|
+
threshold: 2,
|
|
87
|
+
minClusterSize: 4
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Use `properties` to separate variants such as theme, locale, viewport, role, state, component, page, or docs/manual grouping.
|
|
92
|
+
|
|
93
|
+
## When You Need More Detail
|
|
94
|
+
|
|
95
|
+
- For SDK examples and capture patterns, read `references/sdks.md`.
|
|
96
|
+
- For CLI/context commands and JSON output, read `references/cli-context.md`.
|
|
97
|
+
- For dynamic content, thresholds, and hotspots, read `references/dynamic-content.md`.
|
|
98
|
+
- For public screenshot URLs and docs/manual images, read `references/public-screenshots.md`.
|
|
99
|
+
- For project setup and CI, read `references/setup-ci.md`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# CLI And Context
|
|
2
|
+
|
|
3
|
+
Use these commands to gather Vizzly evidence before making UI assumptions.
|
|
4
|
+
|
|
5
|
+
## Local TDD
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
vizzly tdd start
|
|
9
|
+
vizzly tdd run "<test command>" --no-open
|
|
10
|
+
vizzly context build current --source local --agent
|
|
11
|
+
vizzly context build current --source local --agent --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Local context reads `.vizzly` state and does not require cloud auth.
|
|
15
|
+
|
|
16
|
+
## Cloud Builds
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
export VIZZLY_TOKEN="project-token"
|
|
20
|
+
vizzly run "<test command>" --wait --json
|
|
21
|
+
vizzly context build <build-id> --agent --json --include diffs,comments
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`vizzly run --wait --json` returns a `contextCommand` when a cloud build is created. Prefer that command over constructing URLs by hand.
|
|
25
|
+
|
|
26
|
+
## Drilldowns
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
vizzly context comparison <comparison-id> --json
|
|
30
|
+
vizzly context screenshot "<screenshot-name>" --json
|
|
31
|
+
vizzly context similar <fingerprint-hash> --json
|
|
32
|
+
vizzly context review-queue --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use comparison context for one diff. Use screenshot context for history and recurring dynamic areas. Use review queue context when triaging unresolved visual work.
|
|
36
|
+
|
|
37
|
+
## Good Agent Behavior
|
|
38
|
+
|
|
39
|
+
- Use `--json` for automation and summaries.
|
|
40
|
+
- Use `--agent` when building prompt context or asking another agent to continue.
|
|
41
|
+
- Keep human-readable command output in final summaries only when it changes the user's next action.
|
|
42
|
+
- Do not approve/reject unless explicitly asked.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Dynamic Content
|
|
2
|
+
|
|
3
|
+
Dynamic content is common: dates, calendars, generated images, avatars, timers, randomized data, responsive text, and API-backed content.
|
|
4
|
+
|
|
5
|
+
## Order Of Operations
|
|
6
|
+
|
|
7
|
+
1. Make test data deterministic when the changing content is not what you are testing.
|
|
8
|
+
2. Inspect screenshot context before changing code:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
vizzly context screenshot "<screenshot-name>" --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
3. Use per-screenshot tolerances for local noise:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
await vizzlyScreenshot('plant-calendar', screenshot, {
|
|
18
|
+
properties: { surface: 'calendar' },
|
|
19
|
+
threshold: 4,
|
|
20
|
+
minClusterSize: 12
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
4. Use hotspots or confirmed regions for recurring valid dynamic regions.
|
|
25
|
+
|
|
26
|
+
## Avoid
|
|
27
|
+
|
|
28
|
+
- Raising global project thresholds for one dynamic area.
|
|
29
|
+
- Masking a whole page when a small region changes.
|
|
30
|
+
- Treating every daily diff as product breakage.
|
|
31
|
+
- Ignoring structural changes because a region is known to be dynamic.
|
|
32
|
+
|
|
33
|
+
## How To Explain Findings
|
|
34
|
+
|
|
35
|
+
Say whether the diff looks like:
|
|
36
|
+
|
|
37
|
+
- expected dynamic content
|
|
38
|
+
- deterministic fixture drift
|
|
39
|
+
- real content disappearance
|
|
40
|
+
- layout shift
|
|
41
|
+
- screenshot timing/capture instability
|
|
42
|
+
- baseline mismatch
|
|
43
|
+
|
|
44
|
+
Then name the screenshot and include the relevant context command or link.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Public Screenshots
|
|
2
|
+
|
|
3
|
+
Public screenshots are Vizzly's path for stable image URLs in docs and manuals.
|
|
4
|
+
|
|
5
|
+
Use this when a user asks for hotlinked UI screenshots, generated manual images, docs that stay current with UI, or CDN-like screenshot URLs.
|
|
6
|
+
|
|
7
|
+
## Mental Model
|
|
8
|
+
|
|
9
|
+
Do not use arbitrary build screenshot URLs as stable docs assets. Use Public Properties.
|
|
10
|
+
|
|
11
|
+
The flow:
|
|
12
|
+
|
|
13
|
+
1. Capture screenshots with stable metadata.
|
|
14
|
+
2. Configure project Public Properties in Vizzly.
|
|
15
|
+
3. Approve/publish the baseline build.
|
|
16
|
+
4. Use the Public URLs tab to copy the stable URL.
|
|
17
|
+
|
|
18
|
+
Example capture:
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
await vizzlyScreenshot('watering-widget', screenshot, {
|
|
22
|
+
properties: {
|
|
23
|
+
manual: 'plant-care',
|
|
24
|
+
component: 'watering-widget',
|
|
25
|
+
viewport: 'desktop'
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Configure a Public Property like `manual=plant-care`. Vizzly publishes matching approved baseline screenshots and keeps stable URLs for each screenshot/property identity.
|
|
31
|
+
|
|
32
|
+
## Good Uses
|
|
33
|
+
|
|
34
|
+
- Product manuals
|
|
35
|
+
- User docs
|
|
36
|
+
- Component catalogs
|
|
37
|
+
- In-app UI examples
|
|
38
|
+
- Screenshots that should update when approved UI changes
|
|
39
|
+
|
|
40
|
+
## Cautions
|
|
41
|
+
|
|
42
|
+
- Public screenshots are public. Do not publish private user data, secrets, or internal-only UI.
|
|
43
|
+
- A new unapproved build does not automatically become the public docs image.
|
|
44
|
+
- Use stable names and properties so URLs do not churn.
|