@vizzly-testing/cli 0.39.0 → 0.39.2
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 +12 -3
- package/bin/vizzly.js +1 -1
- package/dist/{cli.js → adapters/cli/cli.js} +50 -35
- package/dist/adapters/cli/setup.js +34 -0
- package/dist/{client → adapters/client}/index.js +6 -7
- package/dist/adapters/http/cloud-proxy.js +29 -0
- package/dist/{server → adapters/http}/http-server.js +12 -13
- package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
- package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
- package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
- package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
- package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
- package/dist/adapters/sdk/index.d.ts +89 -0
- package/dist/{sdk → adapters/sdk}/index.js +22 -19
- package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
- package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
- package/dist/domains/auth/auth-errors.js +50 -0
- package/dist/domains/auth/auth-headers.js +41 -0
- package/dist/domains/auth/auth.normalizers.js +50 -0
- package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
- package/dist/domains/auth/auth.rules.js +19 -0
- package/dist/domains/auth/auth.serializers.js +32 -0
- package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
- package/dist/domains/auth/auth.validations.js +21 -0
- package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
- package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
- package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
- package/dist/domains/auth/token-store.js +81 -0
- package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
- package/dist/domains/baselines/baseline-download.js +192 -0
- package/dist/domains/baselines/baseline-write.js +22 -0
- package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
- package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
- package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
- package/dist/domains/baselines/cloud-baselines.js +9 -0
- package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
- package/dist/domains/builds/build.normalizers.js +35 -0
- package/dist/domains/builds/build.serializers.js +164 -0
- package/dist/domains/builds/build.service.js +119 -0
- package/dist/domains/builds/build.validations.js +39 -0
- package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
- package/dist/domains/builds/cloud-builds.routes.js +31 -0
- package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
- package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
- package/dist/domains/comparisons/cloud-comparisons.js +43 -0
- package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
- package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
- package/dist/domains/config/config-defaults.js +27 -0
- package/dist/{utils → domains/config}/config-discovery.js +1 -9
- package/dist/{utils → domains/config}/config-loader.js +10 -9
- package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
- package/dist/{types → domains/config}/config.d.ts +3 -3
- package/dist/domains/config/config.normalizers.js +126 -0
- package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
- package/dist/domains/config/config.rules.js +47 -0
- package/dist/domains/config/config.serializers.js +202 -0
- package/dist/domains/config/config.service.js +625 -0
- package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
- package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
- package/dist/domains/context/compact-context.validations.js +60 -0
- package/dist/domains/context/context-presentation.js +368 -0
- package/dist/domains/context/context.commands.js +385 -0
- package/dist/domains/context/context.serializers.js +157 -0
- package/dist/domains/context/context.service.js +76 -0
- package/dist/domains/context/context.validations.js +100 -0
- package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
- package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
- package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
- package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
- package/dist/domains/previews/preview.service.js +37 -0
- package/dist/domains/projects/cloud-projects.routes.js +28 -0
- package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
- package/dist/{utils → domains/projects}/project-binding.js +3 -3
- package/dist/domains/projects/project-errors.js +61 -0
- package/dist/{utils → domains/projects}/project-link-store.js +2 -2
- package/dist/domains/projects/project-requests.js +61 -0
- package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
- package/dist/domains/projects/project.normalizers.js +69 -0
- package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
- package/dist/domains/projects/project.validations.js +31 -0
- package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
- package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
- package/dist/domains/reports/dashboard.routes.js +96 -0
- package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
- package/dist/domains/reports/report-assets.routes.js +81 -0
- package/dist/domains/reports/report-state.js +51 -0
- package/dist/domains/reports/report-store.js +85 -0
- package/dist/domains/reports/report.serializers.js +99 -0
- package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
- package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
- package/dist/domains/runs/run-environment.js +69 -0
- package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
- package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
- package/dist/domains/runs/run.normalizers.js +8 -0
- package/dist/domains/runs/run.rules.js +56 -0
- package/dist/domains/runs/run.serializers.js +101 -0
- package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
- package/dist/domains/runs/run.validations.js +17 -0
- package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
- package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
- package/dist/domains/screenshots/cloud-screenshots.js +109 -0
- package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
- package/dist/domains/screenshots/screenshot-hash.js +53 -0
- package/dist/domains/screenshots/screenshot-server-options.js +28 -0
- package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
- package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
- package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
- package/dist/domains/screenshots/screenshot.serializers.js +146 -0
- package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
- package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
- package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
- package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
- package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
- package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
- package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
- package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
- package/dist/domains/updates/cli-update.js +103 -0
- package/dist/domains/updates/update-check.js +2 -0
- package/dist/domains/updates/update-notifications.js +175 -0
- package/dist/domains/uploads/upload-defaults.js +4 -0
- package/dist/domains/uploads/upload-files.js +32 -0
- package/dist/domains/uploads/upload-settings.js +21 -0
- package/dist/domains/uploads/upload-timing.js +18 -0
- package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
- package/dist/domains/uploads/upload.normalizers.js +60 -0
- package/dist/domains/uploads/upload.serializers.js +179 -0
- package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
- package/dist/domains/uploads/upload.validations.js +77 -0
- package/dist/index.js +12 -12
- package/dist/reporter/reporter-bundle.iife.js +86 -86
- package/dist/reporter-ssr/ssr-entry.js +3 -6
- package/dist/shared/cloud-protocol.js +171 -0
- package/dist/{utils → shared}/file-changes.js +1 -1
- package/dist/shared/files.js +9 -0
- package/dist/{utils → shared}/global-config.js +7 -78
- package/dist/shared/log-level.js +7 -0
- package/dist/{utils → shared}/output.js +1 -1
- package/dist/{utils → shared}/package-info.js +5 -4
- package/dist/shared/paths.js +57 -0
- package/package.json +37 -36
- package/dist/api/core.js +0 -382
- package/dist/api/endpoints.js +0 -421
- package/dist/api/index.js +0 -19
- package/dist/auth/core.js +0 -237
- package/dist/auth/index.js +0 -31
- package/dist/commands/context.js +0 -1066
- package/dist/config/core.js +0 -439
- package/dist/config/operations.js +0 -327
- package/dist/project/core.js +0 -211
- package/dist/screenshot-server/core.js +0 -161
- package/dist/screenshot-server/index.js +0 -11
- package/dist/server/routers/assets.js +0 -95
- package/dist/server/routers/cloud-proxy.js +0 -146
- package/dist/server/routers/dashboard.js +0 -172
- package/dist/services/auth-service.js +0 -117
- package/dist/services/config-service.js +0 -313
- package/dist/services/project-service.js +0 -88
- package/dist/test-runner/core.js +0 -293
- package/dist/test-runner/index.js +0 -13
- package/dist/types/sdk.d.ts +0 -186
- package/dist/uploader/core.js +0 -432
- package/dist/uploader/index.js +0 -88
- /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
- /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
- /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
- /package/dist/{server → adapters/http}/middleware/response.js +0 -0
- /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
- /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
- /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
- /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
- /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
- /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
- /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
- /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
- /package/dist/{context → domains/context}/provider-resolver.js +0 -0
- /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
- /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
- /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
- /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
- /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
- /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
- /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
- /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
- /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
- /package/dist/{types/index.d.ts → index.d.ts} +0 -0
- /package/dist/{utils → shared}/api-url.js +0 -0
- /package/dist/{utils → shared}/async-utils.js +0 -0
- /package/dist/{utils → shared}/browser.js +0 -0
- /package/dist/{utils → shared}/ci-env.js +0 -0
- /package/dist/{utils → shared}/colors.js +0 -0
- /package/dist/{utils → shared}/fetch-utils.js +0 -0
- /package/dist/{utils → shared}/git.js +0 -0
- /package/dist/{utils → shared}/patterns.js +0 -0
- /package/dist/{utils → shared}/secret-store.js +0 -0
- /package/dist/{errors → shared}/vizzly-error.js +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { VizzlyError } from '../../shared/vizzly-error.js';
|
|
2
|
+
export let COMPACT_CONTEXT_LIMIT = 10;
|
|
3
|
+
let COMPACT_SUMMARY_MAX_BYTES = 64 * 1024;
|
|
4
|
+
let COMPACT_DIFFS_MAX_BYTES = 512 * 1024;
|
|
5
|
+
function getCompactPage(collection, label) {
|
|
6
|
+
if (!collection || !Array.isArray(collection.items) || !collection.page) {
|
|
7
|
+
throw new VizzlyError(`Vizzly returned an invalid compact ${label} collection`, 'COMPACT_CONTEXT_INVALID');
|
|
8
|
+
}
|
|
9
|
+
if (collection.items.length > COMPACT_CONTEXT_LIMIT) {
|
|
10
|
+
throw new VizzlyError(`Vizzly returned more than ${COMPACT_CONTEXT_LIMIT} compact ${label} items`, 'COMPACT_CONTEXT_INVALID');
|
|
11
|
+
}
|
|
12
|
+
if (collection.items.some(item => !item || typeof item !== 'object')) {
|
|
13
|
+
throw new VizzlyError(`Vizzly returned an invalid item in compact ${label}`, 'COMPACT_CONTEXT_INVALID');
|
|
14
|
+
}
|
|
15
|
+
let {
|
|
16
|
+
page
|
|
17
|
+
} = collection;
|
|
18
|
+
let validLimit = Number.isInteger(page.limit) && page.limit >= 1 && page.limit <= COMPACT_CONTEXT_LIMIT;
|
|
19
|
+
let validReturned = Number.isInteger(page.returned) && page.returned === collection.items.length;
|
|
20
|
+
let validTotal = page.total == null || Number.isInteger(page.total) && page.total >= 0 && page.total >= page.returned;
|
|
21
|
+
let validHasMore = typeof page.has_more === 'boolean';
|
|
22
|
+
let validCursor = page.next_cursor == null || typeof page.next_cursor === 'string' && page.next_cursor.length > 0;
|
|
23
|
+
if (!validLimit || !validReturned || !validTotal || !validHasMore || !validCursor) {
|
|
24
|
+
throw new VizzlyError(`Vizzly returned invalid pagination facts for compact ${label}`, 'COMPACT_CONTEXT_INVALID');
|
|
25
|
+
}
|
|
26
|
+
if (page.included === false && collection.items.length > 0) {
|
|
27
|
+
throw new VizzlyError(`Vizzly returned items for an omitted compact ${label} collection`, 'COMPACT_CONTEXT_INVALID');
|
|
28
|
+
}
|
|
29
|
+
if (page.has_more !== Boolean(page.next_cursor)) {
|
|
30
|
+
throw new VizzlyError(`Vizzly returned contradictory pagination facts for compact ${label}`, 'COMPACT_CONTEXT_INVALID');
|
|
31
|
+
}
|
|
32
|
+
return collection;
|
|
33
|
+
}
|
|
34
|
+
export function validateCompactContext(context, resource) {
|
|
35
|
+
if (!context || typeof context !== 'object') {
|
|
36
|
+
throw new VizzlyError(`Vizzly returned an invalid compact ${resource} context`, 'COMPACT_CONTEXT_INVALID');
|
|
37
|
+
}
|
|
38
|
+
if (context.resource !== `${resource}_context` || !context[resource]?.id) {
|
|
39
|
+
throw new VizzlyError(`Vizzly returned an invalid compact ${resource} context`, 'COMPACT_CONTEXT_INVALID');
|
|
40
|
+
}
|
|
41
|
+
if (resource === 'build') {
|
|
42
|
+
getCompactPage(context.evidence, 'build evidence');
|
|
43
|
+
} else {
|
|
44
|
+
let history = context.history || {};
|
|
45
|
+
getCompactPage(history.similar_by_fingerprint, 'similar history');
|
|
46
|
+
getCompactPage(history.recent_by_name, 'recent history');
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
export function validateCompactOutputSize(payload, resource, details) {
|
|
51
|
+
let bytes = Buffer.byteLength(JSON.stringify(payload));
|
|
52
|
+
let maxBytes = details === 'diffs' ? COMPACT_DIFFS_MAX_BYTES : COMPACT_SUMMARY_MAX_BYTES;
|
|
53
|
+
if (bytes > maxBytes) {
|
|
54
|
+
throw new VizzlyError(`Vizzly produced an oversized compact ${resource} context`, 'COMPACT_CONTEXT_OVERSIZED', {
|
|
55
|
+
bytes,
|
|
56
|
+
max_bytes: maxBytes
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { normalizeBuildContext } from './context.normalizers.js';
|
|
2
|
+
function getStatusTone(colors, status) {
|
|
3
|
+
if (status === 'changed' || status === 'pending' || status === 'failed') {
|
|
4
|
+
return colors.brand.warning;
|
|
5
|
+
}
|
|
6
|
+
if (status === 'approved' || status === 'completed' || status === 'identical') {
|
|
7
|
+
return colors.brand.success;
|
|
8
|
+
}
|
|
9
|
+
if (status === 'rejected' || status === 'error') {
|
|
10
|
+
return colors.brand.error;
|
|
11
|
+
}
|
|
12
|
+
return colors.brand.info;
|
|
13
|
+
}
|
|
14
|
+
function getComparisonDisplayState(comparison = {}) {
|
|
15
|
+
return comparison.result || comparison.status || 'unknown';
|
|
16
|
+
}
|
|
17
|
+
function getComparisonName(comparison = {}) {
|
|
18
|
+
return comparison.screenshot_name || comparison.screenshot?.name || comparison.name || comparison.id || 'unknown screenshot';
|
|
19
|
+
}
|
|
20
|
+
function getComparisonDiffPercentage(comparison = {}) {
|
|
21
|
+
return comparison.diff?.percentage ?? comparison.diff_percentage ?? null;
|
|
22
|
+
}
|
|
23
|
+
function getComparisonFingerprint(comparison = {}) {
|
|
24
|
+
return comparison.diff?.fingerprint_hash || comparison.analysis?.fingerprint_hash || null;
|
|
25
|
+
}
|
|
26
|
+
function formatKnownBoolean(value) {
|
|
27
|
+
if (value === true) return 'yes';
|
|
28
|
+
if (value === false) return 'no';
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
function formatCompactPage(collection) {
|
|
32
|
+
let page = collection?.page || {};
|
|
33
|
+
let returned = page.returned ?? collection?.items?.length;
|
|
34
|
+
let total = page.total;
|
|
35
|
+
if (returned == null) return 'unknown';
|
|
36
|
+
if (total == null) return String(returned);
|
|
37
|
+
return `${returned} of ${total}${page.has_more ? ' · more available' : ''}`;
|
|
38
|
+
}
|
|
39
|
+
function displayCompactHeading(output, context, title, status) {
|
|
40
|
+
let colors = output.getColors();
|
|
41
|
+
let tone = getStatusTone(colors, status);
|
|
42
|
+
let organization = context.scope?.organization?.slug || 'unknown';
|
|
43
|
+
let project = context.scope?.project?.slug || 'unknown';
|
|
44
|
+
output.print(` ${colors.bold(title)} ${tone(status.toUpperCase())}`);
|
|
45
|
+
output.print(` ${colors.dim(`@${organization}/${project}`)}`);
|
|
46
|
+
output.blank();
|
|
47
|
+
}
|
|
48
|
+
export function displayCompactBuildContext(output, context) {
|
|
49
|
+
output.header('context', 'build');
|
|
50
|
+
let colors = output.getColors();
|
|
51
|
+
let build = context.build || {};
|
|
52
|
+
let displayStatus = build.status || context.status?.state || 'unknown';
|
|
53
|
+
displayCompactHeading(output, context, build.name || build.id || 'unknown build', displayStatus);
|
|
54
|
+
output.labelValue('Attention', formatKnownBoolean(context.status?.needs_review));
|
|
55
|
+
output.labelValue('Evidence', formatCompactPage(context.evidence));
|
|
56
|
+
let items = context.evidence?.items || [];
|
|
57
|
+
if (items.length > 0) {
|
|
58
|
+
output.blank();
|
|
59
|
+
output.print(' Evidence');
|
|
60
|
+
for (let item of items) {
|
|
61
|
+
let result = item.result || item.status || 'unknown';
|
|
62
|
+
let percentage = getComparisonDiffPercentage(item);
|
|
63
|
+
let detail = percentage == null ? '' : ` · ${percentage}% diff`;
|
|
64
|
+
output.print(` ${colors.dim('•')} ${getComparisonName(item)}: ${result}${detail}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (context.evidence?.page?.has_more) {
|
|
68
|
+
let next = context.suggested_commands?.find(command => command.label === 'Load next evidence page');
|
|
69
|
+
if (next) {
|
|
70
|
+
output.blank();
|
|
71
|
+
output.labelValue('Next', next.command);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (context.links?.build_url) {
|
|
75
|
+
output.labelValue('Build URL', context.links.build_url);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function displayCompactComparisonContext(output, context) {
|
|
79
|
+
output.header('context', 'comparison');
|
|
80
|
+
let comparison = context.comparison || {};
|
|
81
|
+
let displayState = getComparisonDisplayState(comparison);
|
|
82
|
+
displayCompactHeading(output, context, getComparisonName(comparison), displayState);
|
|
83
|
+
output.labelValue('Images', comparison.diff?.image_url || comparison.analysis?.diff_image_url ? 'baseline/current/diff available' : 'unavailable');
|
|
84
|
+
output.labelValue('Similar history', formatCompactPage(context.history?.similar_by_fingerprint));
|
|
85
|
+
output.labelValue('Recent history', formatCompactPage(context.history?.recent_by_name));
|
|
86
|
+
let similar = context.history?.similar_by_fingerprint?.items || [];
|
|
87
|
+
if (similar.length > 0) {
|
|
88
|
+
output.blank();
|
|
89
|
+
output.print(' Similar Diffs');
|
|
90
|
+
printComparisonList(output, similar);
|
|
91
|
+
}
|
|
92
|
+
let recent = context.history?.recent_by_name?.items || [];
|
|
93
|
+
if (recent.length > 0) {
|
|
94
|
+
output.blank();
|
|
95
|
+
output.print(' Recent Diffs');
|
|
96
|
+
printComparisonList(output, recent);
|
|
97
|
+
}
|
|
98
|
+
if (context.links?.comparison_url) {
|
|
99
|
+
output.labelValue('Comparison URL', context.links.comparison_url);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function getBuildCommentsCount(context = {}) {
|
|
103
|
+
if (Array.isArray(context.comments?.build)) {
|
|
104
|
+
return context.comments.build.length;
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(context.review?.comments)) {
|
|
107
|
+
return context.review.comments.length;
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
function getScreenshotCommentsCount(context = {}) {
|
|
112
|
+
if (Number.isInteger(context.comments?.screenshot_count)) {
|
|
113
|
+
return context.comments.screenshot_count;
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
function getReviewAssignmentsCount(context = {}) {
|
|
118
|
+
if (Array.isArray(context.review?.assignments)) {
|
|
119
|
+
return context.review.assignments.length;
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
function formatNeedsReview(status = {}) {
|
|
124
|
+
if (!status || status.needs_review == null) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (!status.needs_review) {
|
|
128
|
+
return 'no';
|
|
129
|
+
}
|
|
130
|
+
let details = [];
|
|
131
|
+
if (status.pending_comparisons != null) {
|
|
132
|
+
details.push(`${status.pending_comparisons} comparisons`);
|
|
133
|
+
}
|
|
134
|
+
if (status.unresolved_comments != null) {
|
|
135
|
+
details.push(`${status.unresolved_comments} unresolved comments`);
|
|
136
|
+
}
|
|
137
|
+
return details.length > 0 ? `yes · ${details.join(' · ')}` : 'yes';
|
|
138
|
+
}
|
|
139
|
+
function printComparisonList(output, comparisons = [], {
|
|
140
|
+
limit = 5
|
|
141
|
+
} = {}) {
|
|
142
|
+
let colors = output.getColors();
|
|
143
|
+
for (let comparison of comparisons.slice(0, limit)) {
|
|
144
|
+
let displayState = getComparisonDisplayState(comparison);
|
|
145
|
+
let statusTone = getStatusTone(colors, displayState);
|
|
146
|
+
let screenshotName = getComparisonName(comparison);
|
|
147
|
+
let rawDiffPercentage = getComparisonDiffPercentage(comparison);
|
|
148
|
+
let diffPercentage = rawDiffPercentage == null ? null : `${rawDiffPercentage}%`;
|
|
149
|
+
let fingerprint = getComparisonFingerprint(comparison);
|
|
150
|
+
let details = [];
|
|
151
|
+
if (diffPercentage) {
|
|
152
|
+
details.push(diffPercentage);
|
|
153
|
+
}
|
|
154
|
+
if (comparison.needs_review) {
|
|
155
|
+
details.push('needs review');
|
|
156
|
+
}
|
|
157
|
+
if (fingerprint) {
|
|
158
|
+
details.push(`fp:${fingerprint}`);
|
|
159
|
+
}
|
|
160
|
+
if (comparison.build_branch) {
|
|
161
|
+
details.push(comparison.build_branch);
|
|
162
|
+
}
|
|
163
|
+
output.print(` ${colors.bold(screenshotName)} ${statusTone(displayState.toUpperCase())}`);
|
|
164
|
+
if (details.length > 0) {
|
|
165
|
+
output.print(` ${colors.dim(details.join(' · '))}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function getGroupDisplayState(group = {}) {
|
|
170
|
+
let aggregate = group.aggregate_status || {};
|
|
171
|
+
if ((aggregate.failed_count ?? 0) > 0) return 'failed';
|
|
172
|
+
if (aggregate.has_rejected === true) return 'rejected';
|
|
173
|
+
if (aggregate.has_changes === true) return 'changed';
|
|
174
|
+
if (aggregate.has_new === true) return 'new';
|
|
175
|
+
if (aggregate.needs_review === true) return 'needs review';
|
|
176
|
+
if (aggregate.all_approved === true) return 'approved';
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
function formatViewport(viewport) {
|
|
180
|
+
if (!viewport) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return `${viewport.width ?? '?'}×${viewport.height ?? '?'}`;
|
|
184
|
+
}
|
|
185
|
+
function printScreenshotGroups(output, groups = [], {
|
|
186
|
+
limit = 8
|
|
187
|
+
} = {}) {
|
|
188
|
+
let colors = output.getColors();
|
|
189
|
+
let visibleGroups = groups.slice(0, limit);
|
|
190
|
+
output.print(' Screenshot groups');
|
|
191
|
+
for (let group of visibleGroups) {
|
|
192
|
+
let displayState = getGroupDisplayState(group);
|
|
193
|
+
let tone = getStatusTone(colors, displayState);
|
|
194
|
+
let aggregate = group.aggregate_status || {};
|
|
195
|
+
let details = [];
|
|
196
|
+
if (group.variant_count != null) {
|
|
197
|
+
details.push(`${group.variant_count} variants`);
|
|
198
|
+
}
|
|
199
|
+
if (aggregate.needs_review_count != null) {
|
|
200
|
+
details.push(`${aggregate.needs_review_count} needs review`);
|
|
201
|
+
}
|
|
202
|
+
if (aggregate.failed_count != null && aggregate.failed_count > 0) {
|
|
203
|
+
details.push(`${aggregate.failed_count} failed`);
|
|
204
|
+
}
|
|
205
|
+
if (aggregate.max_diff_percentage != null) {
|
|
206
|
+
details.push(`${aggregate.max_diff_percentage}% max diff`);
|
|
207
|
+
}
|
|
208
|
+
output.print(` ${colors.bold(group.name)}${displayState ? ` ${tone(displayState.toUpperCase())}` : ''}`);
|
|
209
|
+
if (details.length > 0) {
|
|
210
|
+
output.print(` ${colors.dim(details.join(' · '))}`);
|
|
211
|
+
}
|
|
212
|
+
for (let variant of group.variants.slice(0, 3)) {
|
|
213
|
+
let variantDetails = [variant.review_state, variant.browser, formatViewport(variant.viewport)].filter(Boolean);
|
|
214
|
+
output.print(` ${getStatusTone(colors, variant.result)((variant.result || 'unknown').toUpperCase())}${variantDetails.length > 0 ? ` · ${colors.dim(variantDetails.join(' · '))}` : ''}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (groups.length > visibleGroups.length) {
|
|
218
|
+
output.print(` ${colors.dim(`...${groups.length - visibleGroups.length} more groups`)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function printFailedCaptures(output, captures = []) {
|
|
222
|
+
let colors = output.getColors();
|
|
223
|
+
output.print(' Failed captures');
|
|
224
|
+
for (let capture of captures.slice(0, 8)) {
|
|
225
|
+
let details = [capture.error_message, capture.browser, formatViewport(capture.viewport), capture.screenshot.url].filter(Boolean);
|
|
226
|
+
output.print(` ${colors.bold(capture.name)} ${colors.brand.error('FAILED')}`);
|
|
227
|
+
if (details.length > 0) {
|
|
228
|
+
output.print(` ${colors.dim(details.join(' · '))}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function formatReviewSummary(review = {}) {
|
|
233
|
+
let details = [review.pending != null ? `${review.pending} pending` : null, review.approved != null ? `${review.approved} approved` : null, review.rejected != null ? `${review.rejected} rejected` : null].filter(Boolean);
|
|
234
|
+
return details.length > 0 ? details.join(' · ') : null;
|
|
235
|
+
}
|
|
236
|
+
export function displayBuildContext(output, context) {
|
|
237
|
+
output.header('context', 'build');
|
|
238
|
+
let normalizedContext = normalizeBuildContext(context);
|
|
239
|
+
let colors = output.getColors();
|
|
240
|
+
let build = normalizedContext.build || {};
|
|
241
|
+
let buildTone = getStatusTone(colors, build.status);
|
|
242
|
+
let comparisons = normalizedContext.comparisons;
|
|
243
|
+
let groups = normalizedContext.groups;
|
|
244
|
+
let screenshots = normalizedContext.screenshots || [];
|
|
245
|
+
let failedCaptures = normalizedContext.failed_captures;
|
|
246
|
+
let reviewSummary = formatReviewSummary(normalizedContext.summary?.review);
|
|
247
|
+
let commentsSummary = normalizedContext.summary?.comments || {};
|
|
248
|
+
let needsReview = formatNeedsReview(normalizedContext.status);
|
|
249
|
+
let baseline = normalizedContext.baseline?.selected || null;
|
|
250
|
+
let comparisonCount = normalizedContext.summary?.comparisons?.total ?? normalizedContext.total_comparisons ?? (Array.isArray(context.comparisons) ? comparisons.length : null);
|
|
251
|
+
let screenshotCount = normalizedContext.summary?.screenshots?.total ?? normalizedContext.screenshot_count ?? (Array.isArray(context.screenshots) ? screenshots.length : null);
|
|
252
|
+
output.print(` ${colors.bold(build.name || build.id || 'unknown build')} ${buildTone((build.status || 'unknown').toUpperCase())}`);
|
|
253
|
+
output.print(` ${colors.dim(`@${normalizedContext.scope?.organization?.slug || 'unknown'}/${normalizedContext.scope?.project?.slug || 'unknown'}`)}`);
|
|
254
|
+
output.blank();
|
|
255
|
+
if (comparisonCount != null) {
|
|
256
|
+
output.labelValue('Comparisons', String(comparisonCount));
|
|
257
|
+
}
|
|
258
|
+
if (groups.length > 0) {
|
|
259
|
+
output.labelValue('Screenshot Groups', String(groups.length));
|
|
260
|
+
}
|
|
261
|
+
if (screenshotCount != null) {
|
|
262
|
+
output.labelValue('Screenshots', String(screenshotCount));
|
|
263
|
+
}
|
|
264
|
+
if (baseline) {
|
|
265
|
+
output.labelValue('Baseline', `${baseline.name || baseline.id || 'selected'}${normalizedContext.baseline.selection_reason ? ` · ${normalizedContext.baseline.selection_reason}` : ''}`);
|
|
266
|
+
}
|
|
267
|
+
if (needsReview) {
|
|
268
|
+
output.labelValue('Needs Review', needsReview);
|
|
269
|
+
}
|
|
270
|
+
if (reviewSummary) {
|
|
271
|
+
output.labelValue('Review', reviewSummary);
|
|
272
|
+
}
|
|
273
|
+
let buildComments = commentsSummary.build ?? (Array.isArray(normalizedContext.comments?.build) ? getBuildCommentsCount(normalizedContext) : null);
|
|
274
|
+
let screenshotComments = commentsSummary.screenshot ?? (Number.isInteger(normalizedContext.comments?.screenshot_count) ? getScreenshotCommentsCount(normalizedContext) : null);
|
|
275
|
+
let assignments = Array.isArray(normalizedContext.review?.assignments) ? getReviewAssignmentsCount(normalizedContext) : null;
|
|
276
|
+
let memory = [buildComments != null ? `${buildComments} build comments` : null, screenshotComments != null ? `${screenshotComments} screenshot comments` : null, assignments != null ? `${assignments} assignments` : null].filter(Boolean);
|
|
277
|
+
if (memory.length > 0) {
|
|
278
|
+
output.labelValue('Memory', memory.join(' · '));
|
|
279
|
+
}
|
|
280
|
+
if (normalizedContext.preview) {
|
|
281
|
+
let previewUrl = normalizedContext.preview.preview_url || normalizedContext.preview.url;
|
|
282
|
+
output.labelValue('Preview', `${normalizedContext.preview.status || 'unknown'}${previewUrl ? ' · available' : ''}`);
|
|
283
|
+
}
|
|
284
|
+
if (normalizedContext.links?.build_url) {
|
|
285
|
+
output.labelValue('Build URL', normalizedContext.links.build_url);
|
|
286
|
+
}
|
|
287
|
+
if (groups.length > 0) {
|
|
288
|
+
output.blank();
|
|
289
|
+
printScreenshotGroups(output, groups);
|
|
290
|
+
}
|
|
291
|
+
if (failedCaptures.length > 0) {
|
|
292
|
+
output.blank();
|
|
293
|
+
printFailedCaptures(output, failedCaptures);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function countScreenshotCommentEntries(groups = []) {
|
|
297
|
+
return groups.reduce((total, group) => total + (group.comments?.length || 0), 0);
|
|
298
|
+
}
|
|
299
|
+
export function displayComparisonContext(output, context) {
|
|
300
|
+
output.header('context', 'comparison');
|
|
301
|
+
let colors = output.getColors();
|
|
302
|
+
let displayState = getComparisonDisplayState(context.comparison);
|
|
303
|
+
let statusTone = getStatusTone(colors, displayState);
|
|
304
|
+
let screenshotName = context.comparison.screenshot?.name || context.comparison.id;
|
|
305
|
+
let analysis = context.comparison.analysis || {};
|
|
306
|
+
output.print(` ${colors.bold(screenshotName)} ${statusTone(displayState.toUpperCase())}`);
|
|
307
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
308
|
+
output.blank();
|
|
309
|
+
output.labelValue('Eyes', `${analysis.diff_image_url ? 'baseline/current/diff' : 'comparison metadata only'}`);
|
|
310
|
+
output.labelValue('Memory', `${context.history.similar_by_fingerprint.length} similar · ${context.history.recent_by_name.length} recent`);
|
|
311
|
+
output.labelValue('Review', `${context.review.build_comments.length} build comments · ${countScreenshotCommentEntries(context.review.screenshot_comments)} screenshot comments`);
|
|
312
|
+
if (analysis.fingerprint_hash) {
|
|
313
|
+
output.labelValue('Fingerprint', analysis.fingerprint_hash);
|
|
314
|
+
}
|
|
315
|
+
if (context.links?.comparison_url) {
|
|
316
|
+
output.labelValue('Comparison URL', context.links.comparison_url);
|
|
317
|
+
}
|
|
318
|
+
if (context.history.similar_by_fingerprint.length > 0) {
|
|
319
|
+
output.blank();
|
|
320
|
+
output.print(' Similar Diffs');
|
|
321
|
+
printComparisonList(output, context.history.similar_by_fingerprint);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
export function displayScreenshotContext(output, context) {
|
|
325
|
+
output.header('context', 'screenshot');
|
|
326
|
+
let colors = output.getColors();
|
|
327
|
+
output.print(` ${colors.bold(context.screenshot.name)}`);
|
|
328
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
329
|
+
output.blank();
|
|
330
|
+
output.labelValue('Memory', `${context.history.recent_comparisons.length} recent comparisons`);
|
|
331
|
+
if (context.history.recent_comparisons.length > 0) {
|
|
332
|
+
output.blank();
|
|
333
|
+
output.print(' Recent Comparisons');
|
|
334
|
+
printComparisonList(output, context.history.recent_comparisons);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
export function displayFingerprintContext(output, context) {
|
|
338
|
+
output.header('context', 'similar');
|
|
339
|
+
let colors = output.getColors();
|
|
340
|
+
let fingerprintHash = context.fingerprint?.hash || context.fingerprint_hash || 'unknown';
|
|
341
|
+
let comparisons = context.comparisons || context.matches || [];
|
|
342
|
+
output.print(` ${colors.bold(fingerprintHash)}`);
|
|
343
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
344
|
+
output.blank();
|
|
345
|
+
output.labelValue('Matches', String(comparisons.length));
|
|
346
|
+
if (comparisons.length > 0) {
|
|
347
|
+
output.blank();
|
|
348
|
+
output.print(' Similar Diffs');
|
|
349
|
+
printComparisonList(output, comparisons, {
|
|
350
|
+
limit: 10
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
export function displayReviewQueueContext(output, context) {
|
|
355
|
+
output.header('context', 'review');
|
|
356
|
+
let colors = output.getColors();
|
|
357
|
+
output.print(` ${colors.bold(`${context.summary.total} pending comparisons`)}`);
|
|
358
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
359
|
+
output.blank();
|
|
360
|
+
output.labelValue('Queue', `${context.summary.changed} changed · ${context.summary.new} new · ${context.summary.builds} builds`);
|
|
361
|
+
if (context.comparisons.length > 0) {
|
|
362
|
+
output.blank();
|
|
363
|
+
output.print(' Needs Review');
|
|
364
|
+
printComparisonList(output, context.comparisons, {
|
|
365
|
+
limit: 10
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|