tlc-claude-code 1.4.9 → 1.5.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/CLAUDE.md +23 -0
- package/CODING-STANDARDS.md +408 -0
- package/bin/install.js +2 -0
- package/dashboard/dist/components/QualityGatePane.d.ts +38 -0
- package/dashboard/dist/components/QualityGatePane.js +31 -0
- package/dashboard/dist/components/QualityGatePane.test.d.ts +1 -0
- package/dashboard/dist/components/QualityGatePane.test.js +147 -0
- package/dashboard/dist/components/orchestration/AgentCard.d.ts +26 -0
- package/dashboard/dist/components/orchestration/AgentCard.js +60 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.js +63 -0
- package/dashboard/dist/components/orchestration/AgentControls.d.ts +11 -0
- package/dashboard/dist/components/orchestration/AgentControls.js +20 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.js +52 -0
- package/dashboard/dist/components/orchestration/AgentDetail.d.ts +35 -0
- package/dashboard/dist/components/orchestration/AgentDetail.js +37 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.js +79 -0
- package/dashboard/dist/components/orchestration/AgentList.d.ts +31 -0
- package/dashboard/dist/components/orchestration/AgentList.js +47 -0
- package/dashboard/dist/components/orchestration/AgentList.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentList.test.js +64 -0
- package/dashboard/dist/components/orchestration/CostMeter.d.ts +11 -0
- package/dashboard/dist/components/orchestration/CostMeter.js +28 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.js +50 -0
- package/dashboard/dist/components/orchestration/ModelSelector.d.ts +20 -0
- package/dashboard/dist/components/orchestration/ModelSelector.js +12 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.js +56 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.d.ts +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.js +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.js +56 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.d.ts +11 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.js +37 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.js +52 -0
- package/dashboard/dist/components/orchestration/index.d.ts +8 -0
- package/dashboard/dist/components/orchestration/index.js +8 -0
- package/package.json +1 -1
- package/server/lib/access-control.js +352 -0
- package/server/lib/access-control.test.js +322 -0
- package/server/lib/agents-cancel-command.js +139 -0
- package/server/lib/agents-cancel-command.test.js +180 -0
- package/server/lib/agents-get-command.js +159 -0
- package/server/lib/agents-get-command.test.js +167 -0
- package/server/lib/agents-list-command.js +150 -0
- package/server/lib/agents-list-command.test.js +149 -0
- package/server/lib/agents-logs-command.js +126 -0
- package/server/lib/agents-logs-command.test.js +198 -0
- package/server/lib/agents-retry-command.js +117 -0
- package/server/lib/agents-retry-command.test.js +192 -0
- package/server/lib/budget-limits.js +222 -0
- package/server/lib/budget-limits.test.js +214 -0
- package/server/lib/code-generator.js +291 -0
- package/server/lib/code-generator.test.js +307 -0
- package/server/lib/cost-command.js +290 -0
- package/server/lib/cost-command.test.js +202 -0
- package/server/lib/cost-optimizer.js +404 -0
- package/server/lib/cost-optimizer.test.js +232 -0
- package/server/lib/cost-projections.js +302 -0
- package/server/lib/cost-projections.test.js +217 -0
- package/server/lib/cost-reports.js +277 -0
- package/server/lib/cost-reports.test.js +254 -0
- package/server/lib/cost-tracker.js +216 -0
- package/server/lib/cost-tracker.test.js +302 -0
- package/server/lib/crypto-patterns.js +433 -0
- package/server/lib/crypto-patterns.test.js +346 -0
- package/server/lib/design-command.js +385 -0
- package/server/lib/design-command.test.js +249 -0
- package/server/lib/design-parser.js +237 -0
- package/server/lib/design-parser.test.js +290 -0
- package/server/lib/gemini-vision.js +377 -0
- package/server/lib/gemini-vision.test.js +282 -0
- package/server/lib/input-validator.js +360 -0
- package/server/lib/input-validator.test.js +295 -0
- package/server/lib/litellm-client.js +232 -0
- package/server/lib/litellm-client.test.js +267 -0
- package/server/lib/litellm-command.js +291 -0
- package/server/lib/litellm-command.test.js +260 -0
- package/server/lib/litellm-config.js +273 -0
- package/server/lib/litellm-config.test.js +212 -0
- package/server/lib/model-pricing.js +189 -0
- package/server/lib/model-pricing.test.js +178 -0
- package/server/lib/models-command.js +223 -0
- package/server/lib/models-command.test.js +193 -0
- package/server/lib/optimize-command.js +197 -0
- package/server/lib/optimize-command.test.js +193 -0
- package/server/lib/orchestration-integration.js +206 -0
- package/server/lib/orchestration-integration.test.js +235 -0
- package/server/lib/output-encoder.js +308 -0
- package/server/lib/output-encoder.test.js +312 -0
- package/server/lib/quality-evaluator.js +396 -0
- package/server/lib/quality-evaluator.test.js +337 -0
- package/server/lib/quality-gate-command.js +340 -0
- package/server/lib/quality-gate-command.test.js +321 -0
- package/server/lib/quality-gate-scorer.js +378 -0
- package/server/lib/quality-gate-scorer.test.js +376 -0
- package/server/lib/quality-history.js +265 -0
- package/server/lib/quality-history.test.js +359 -0
- package/server/lib/quality-presets.js +288 -0
- package/server/lib/quality-presets.test.js +269 -0
- package/server/lib/quality-retry.js +323 -0
- package/server/lib/quality-retry.test.js +325 -0
- package/server/lib/quality-thresholds.js +255 -0
- package/server/lib/quality-thresholds.test.js +237 -0
- package/server/lib/secure-auth.js +333 -0
- package/server/lib/secure-auth.test.js +288 -0
- package/server/lib/secure-code-command.js +540 -0
- package/server/lib/secure-code-command.test.js +309 -0
- package/server/lib/secure-errors.js +521 -0
- package/server/lib/secure-errors.test.js +298 -0
- package/server/lib/vision-command.js +372 -0
- package/server/lib/vision-command.test.js +255 -0
- package/server/lib/visual-command.js +350 -0
- package/server/lib/visual-command.test.js +256 -0
- package/server/lib/visual-testing.js +315 -0
- package/server/lib/visual-testing.test.js +357 -0
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vision Command Tests
|
|
3
|
+
*
|
|
4
|
+
* CLI commands for vision operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it, beforeEach } = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
VisionCommand,
|
|
12
|
+
parseArgs,
|
|
13
|
+
formatAnalysis,
|
|
14
|
+
formatComparison,
|
|
15
|
+
formatAccessibilityReport,
|
|
16
|
+
} = require('./vision-command.js');
|
|
17
|
+
|
|
18
|
+
describe('Vision Command', () => {
|
|
19
|
+
let command;
|
|
20
|
+
let mockClient;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockClient = {
|
|
24
|
+
_call: async () => ({ text: 'Mock response' }),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
command = new VisionCommand({
|
|
28
|
+
client: mockClient,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('execute analyze', () => {
|
|
33
|
+
it('analyzes screenshot', async () => {
|
|
34
|
+
mockClient._call = async () => ({
|
|
35
|
+
description: 'A login form with email and password inputs.',
|
|
36
|
+
elements: ['input', 'button'],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const result = await command.execute('analyze /path/to/screenshot.png');
|
|
40
|
+
|
|
41
|
+
assert.ok(result.success);
|
|
42
|
+
assert.ok(result.output);
|
|
43
|
+
assert.ok(result.analysis);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('accepts custom prompt', async () => {
|
|
47
|
+
let receivedPrompt;
|
|
48
|
+
mockClient._call = async (prompt) => {
|
|
49
|
+
receivedPrompt = prompt;
|
|
50
|
+
return { description: 'Custom analysis' };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await command.execute('analyze /path/to/image.png --prompt "Find all buttons"');
|
|
54
|
+
|
|
55
|
+
assert.ok(receivedPrompt.includes('buttons'));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('execute compare', () => {
|
|
60
|
+
it('compares before/after images', async () => {
|
|
61
|
+
mockClient._call = async () => ({
|
|
62
|
+
differences: [
|
|
63
|
+
{ type: 'changed', description: 'Header color changed' },
|
|
64
|
+
],
|
|
65
|
+
similarity: 0.92,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const result = await command.execute('compare /path/to/before.png /path/to/after.png');
|
|
69
|
+
|
|
70
|
+
assert.ok(result.success);
|
|
71
|
+
assert.ok(result.comparison);
|
|
72
|
+
assert.ok(result.comparison.differences);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('reports when identical', async () => {
|
|
76
|
+
mockClient._call = async () => ({
|
|
77
|
+
differences: [],
|
|
78
|
+
similarity: 1.0,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = await command.execute('compare /path/to/a.png /path/to/b.png');
|
|
82
|
+
|
|
83
|
+
assert.ok(result.output.includes('identical') || result.output.includes('no diff'));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('execute a11y', () => {
|
|
88
|
+
it('runs accessibility audit', async () => {
|
|
89
|
+
mockClient._call = async () => ({
|
|
90
|
+
issues: [
|
|
91
|
+
{ type: 'contrast', severity: 'high', description: 'Low contrast text' },
|
|
92
|
+
],
|
|
93
|
+
score: 75,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = await command.execute('a11y /path/to/ui.png');
|
|
97
|
+
|
|
98
|
+
assert.ok(result.success);
|
|
99
|
+
assert.ok(result.audit);
|
|
100
|
+
assert.ok(result.audit.issues);
|
|
101
|
+
assert.ok(result.audit.score !== undefined);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('formats issues by severity', async () => {
|
|
105
|
+
mockClient._call = async () => ({
|
|
106
|
+
issues: [
|
|
107
|
+
{ type: 'contrast', severity: 'high', description: 'Issue 1' },
|
|
108
|
+
{ type: 'touch', severity: 'low', description: 'Issue 2' },
|
|
109
|
+
],
|
|
110
|
+
score: 80,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result = await command.execute('a11y /path/to/ui.png');
|
|
114
|
+
|
|
115
|
+
assert.ok(result.output.includes('high') || result.output.includes('HIGH'));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('execute extract', () => {
|
|
120
|
+
it('extracts components from mockup', async () => {
|
|
121
|
+
mockClient._call = async () => ({
|
|
122
|
+
components: [
|
|
123
|
+
{ type: 'button', label: 'Submit' },
|
|
124
|
+
{ type: 'input', placeholder: 'Email' },
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = await command.execute('extract /path/to/mockup.png');
|
|
129
|
+
|
|
130
|
+
assert.ok(result.success);
|
|
131
|
+
assert.ok(result.components);
|
|
132
|
+
assert.ok(result.components.length > 0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('filters by component type', async () => {
|
|
136
|
+
mockClient._call = async () => ({
|
|
137
|
+
components: [
|
|
138
|
+
{ type: 'button', label: 'Submit' },
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = await command.execute('extract /path/to/mockup.png --type button');
|
|
143
|
+
|
|
144
|
+
result.components.forEach(c => {
|
|
145
|
+
assert.strictEqual(c.type, 'button');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('parseArgs', () => {
|
|
151
|
+
it('parses analyze command', () => {
|
|
152
|
+
const parsed = parseArgs('analyze /path/to/image.png');
|
|
153
|
+
|
|
154
|
+
assert.strictEqual(parsed.command, 'analyze');
|
|
155
|
+
assert.strictEqual(parsed.imagePath, '/path/to/image.png');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('parses compare command', () => {
|
|
159
|
+
const parsed = parseArgs('compare /path/to/before.png /path/to/after.png');
|
|
160
|
+
|
|
161
|
+
assert.strictEqual(parsed.command, 'compare');
|
|
162
|
+
assert.strictEqual(parsed.beforeImage, '/path/to/before.png');
|
|
163
|
+
assert.strictEqual(parsed.afterImage, '/path/to/after.png');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('parses a11y command', () => {
|
|
167
|
+
const parsed = parseArgs('a11y /path/to/ui.png');
|
|
168
|
+
|
|
169
|
+
assert.strictEqual(parsed.command, 'a11y');
|
|
170
|
+
assert.strictEqual(parsed.imagePath, '/path/to/ui.png');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('parses prompt flag', () => {
|
|
174
|
+
const parsed = parseArgs('analyze /image.png --prompt "Find buttons"');
|
|
175
|
+
|
|
176
|
+
assert.strictEqual(parsed.prompt, 'Find buttons');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('parses type filter', () => {
|
|
180
|
+
const parsed = parseArgs('extract /mockup.png --type button');
|
|
181
|
+
|
|
182
|
+
assert.strictEqual(parsed.type, 'button');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('formatAnalysis', () => {
|
|
187
|
+
it('creates readable output', () => {
|
|
188
|
+
const analysis = {
|
|
189
|
+
description: 'A login form with two input fields and a button.',
|
|
190
|
+
elements: ['input:email', 'input:password', 'button:submit'],
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const formatted = formatAnalysis(analysis);
|
|
194
|
+
|
|
195
|
+
assert.ok(typeof formatted === 'string');
|
|
196
|
+
assert.ok(formatted.includes('login') || formatted.includes('Login'));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('formatComparison', () => {
|
|
201
|
+
it('formats differences', () => {
|
|
202
|
+
const comparison = {
|
|
203
|
+
differences: [
|
|
204
|
+
{ type: 'changed', description: 'Button color changed' },
|
|
205
|
+
{ type: 'added', description: 'New icon added' },
|
|
206
|
+
],
|
|
207
|
+
similarity: 0.85,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const formatted = formatComparison(comparison);
|
|
211
|
+
|
|
212
|
+
assert.ok(formatted.includes('changed') || formatted.includes('Changed'));
|
|
213
|
+
assert.ok(formatted.includes('85%') || formatted.includes('0.85'));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('handles no differences', () => {
|
|
217
|
+
const comparison = {
|
|
218
|
+
differences: [],
|
|
219
|
+
similarity: 1.0,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const formatted = formatComparison(comparison);
|
|
223
|
+
|
|
224
|
+
assert.ok(formatted.includes('identical') || formatted.includes('100%'));
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('formatAccessibilityReport', () => {
|
|
229
|
+
it('formats issues with severity', () => {
|
|
230
|
+
const audit = {
|
|
231
|
+
issues: [
|
|
232
|
+
{ type: 'contrast', severity: 'high', description: 'Low contrast' },
|
|
233
|
+
{ type: 'touch', severity: 'medium', description: 'Small button' },
|
|
234
|
+
],
|
|
235
|
+
score: 70,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const formatted = formatAccessibilityReport(audit);
|
|
239
|
+
|
|
240
|
+
assert.ok(formatted.includes('70') || formatted.includes('Score'));
|
|
241
|
+
assert.ok(formatted.includes('contrast') || formatted.includes('Contrast'));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('shows passing score', () => {
|
|
245
|
+
const audit = {
|
|
246
|
+
issues: [],
|
|
247
|
+
score: 100,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const formatted = formatAccessibilityReport(audit);
|
|
251
|
+
|
|
252
|
+
assert.ok(formatted.includes('100') || formatted.includes('pass'));
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Command Module
|
|
3
|
+
*
|
|
4
|
+
* CLI for visual regression testing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const {
|
|
9
|
+
createBaseline,
|
|
10
|
+
updateBaseline,
|
|
11
|
+
runVisualTest,
|
|
12
|
+
formatVisualReport,
|
|
13
|
+
} = require('./visual-testing.js');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse command line arguments
|
|
17
|
+
* @param {string} input - Command input
|
|
18
|
+
* @returns {Object} Parsed arguments
|
|
19
|
+
*/
|
|
20
|
+
function parseArgs(input) {
|
|
21
|
+
const parts = input.split(/\s+/);
|
|
22
|
+
const result = {
|
|
23
|
+
command: parts[0] || 'test',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let argIndex = 1;
|
|
27
|
+
|
|
28
|
+
// Handle positional name argument
|
|
29
|
+
if (parts[1] && !parts[1].startsWith('--')) {
|
|
30
|
+
result.name = parts[1];
|
|
31
|
+
argIndex = 2;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Parse flags
|
|
35
|
+
for (let i = argIndex; i < parts.length; i++) {
|
|
36
|
+
const part = parts[i];
|
|
37
|
+
|
|
38
|
+
if (part === '--url' && parts[i + 1]) {
|
|
39
|
+
result.url = parts[i + 1];
|
|
40
|
+
i++;
|
|
41
|
+
} else if (part === '--viewport' && parts[i + 1]) {
|
|
42
|
+
result.viewport = parts[i + 1];
|
|
43
|
+
i++;
|
|
44
|
+
} else if (part === '--selector' && parts[i + 1]) {
|
|
45
|
+
result.selector = parts[i + 1];
|
|
46
|
+
i++;
|
|
47
|
+
} else if (part === '--threshold' && parts[i + 1]) {
|
|
48
|
+
result.threshold = parts[i + 1];
|
|
49
|
+
i++;
|
|
50
|
+
} else if (part === '--pattern' && parts[i + 1]) {
|
|
51
|
+
result.pattern = parts[i + 1];
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse viewport string (e.g., "375x812")
|
|
61
|
+
* @param {string} viewport - Viewport string
|
|
62
|
+
* @returns {Object} Viewport object
|
|
63
|
+
*/
|
|
64
|
+
function parseViewport(viewport) {
|
|
65
|
+
if (!viewport) return null;
|
|
66
|
+
|
|
67
|
+
const [width, height] = viewport.split('x').map(Number);
|
|
68
|
+
return { width, height };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format test summary
|
|
73
|
+
* @param {Array} results - Test results
|
|
74
|
+
* @param {Object} [options] - Format options
|
|
75
|
+
* @returns {string} Formatted summary
|
|
76
|
+
*/
|
|
77
|
+
function formatTestSummary(results, options = {}) {
|
|
78
|
+
const { showTiming = false } = options;
|
|
79
|
+
|
|
80
|
+
const passed = results.filter(r => r.pass).length;
|
|
81
|
+
const failed = results.filter(r => !r.pass).length;
|
|
82
|
+
const total = results.length;
|
|
83
|
+
|
|
84
|
+
const lines = [
|
|
85
|
+
'Visual Test Summary',
|
|
86
|
+
'═'.repeat(40),
|
|
87
|
+
'',
|
|
88
|
+
`Total: ${total} | Pass: ${passed} | Fail: ${failed}`,
|
|
89
|
+
'',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const result of results) {
|
|
93
|
+
const status = result.pass ? '✓ PASS' : '✗ FAIL';
|
|
94
|
+
let line = `${status} ${result.name}`;
|
|
95
|
+
|
|
96
|
+
if (result.similarity !== undefined && !result.pass) {
|
|
97
|
+
line += ` (${Math.round(result.similarity * 100)}% similar)`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (showTiming && result.duration) {
|
|
101
|
+
line += ` [${result.duration}ms]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
lines.push(line);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Visual Command class
|
|
112
|
+
*/
|
|
113
|
+
class VisualCommand {
|
|
114
|
+
/**
|
|
115
|
+
* Create a visual command handler
|
|
116
|
+
* @param {Object} options - Dependencies
|
|
117
|
+
* @param {Object} options.tester - Visual tester instance
|
|
118
|
+
*/
|
|
119
|
+
constructor(options) {
|
|
120
|
+
this.tester = options.tester;
|
|
121
|
+
this.pendingApprovals = new Map();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Execute a command
|
|
126
|
+
* @param {string} input - Command input
|
|
127
|
+
* @returns {Promise<Object>} Execution result
|
|
128
|
+
*/
|
|
129
|
+
async execute(input) {
|
|
130
|
+
const args = parseArgs(input);
|
|
131
|
+
|
|
132
|
+
switch (args.command) {
|
|
133
|
+
case 'baseline':
|
|
134
|
+
return this.executeBaseline(args);
|
|
135
|
+
|
|
136
|
+
case 'test':
|
|
137
|
+
return this.executeTest(args);
|
|
138
|
+
|
|
139
|
+
case 'approve':
|
|
140
|
+
return this.executeApprove(args);
|
|
141
|
+
|
|
142
|
+
case 'list':
|
|
143
|
+
return this.executeList(args);
|
|
144
|
+
|
|
145
|
+
case 'run':
|
|
146
|
+
return this.executeRun(args);
|
|
147
|
+
|
|
148
|
+
default:
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
output: `Unknown command: ${args.command}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Execute baseline command
|
|
158
|
+
* @param {Object} args - Parsed arguments
|
|
159
|
+
* @returns {Promise<Object>} Baseline result
|
|
160
|
+
*/
|
|
161
|
+
async executeBaseline(args) {
|
|
162
|
+
try {
|
|
163
|
+
const viewport = parseViewport(args.viewport);
|
|
164
|
+
|
|
165
|
+
const baseline = await createBaseline(this.tester, {
|
|
166
|
+
name: args.name,
|
|
167
|
+
url: args.url,
|
|
168
|
+
viewport,
|
|
169
|
+
selector: args.selector,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
output: `Baseline created: ${args.name}`,
|
|
175
|
+
baseline,
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
output: `Failed to create baseline: ${error.message}`,
|
|
181
|
+
error: error.message,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Execute test command
|
|
188
|
+
* @param {Object} args - Parsed arguments
|
|
189
|
+
* @returns {Promise<Object>} Test result
|
|
190
|
+
*/
|
|
191
|
+
async executeTest(args) {
|
|
192
|
+
try {
|
|
193
|
+
if (args.threshold) {
|
|
194
|
+
this.tester.threshold = parseFloat(args.threshold);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const result = await runVisualTest(this.tester, {
|
|
198
|
+
name: args.name,
|
|
199
|
+
url: args.url,
|
|
200
|
+
createIfMissing: false,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!result.pass) {
|
|
204
|
+
// Store for potential approval
|
|
205
|
+
this.pendingApprovals.set(args.name, {
|
|
206
|
+
url: args.url,
|
|
207
|
+
timestamp: new Date(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
pass: result.pass,
|
|
214
|
+
output: result.pass
|
|
215
|
+
? `✓ Visual test passed: ${args.name}`
|
|
216
|
+
: `✗ Visual test failed: ${args.name} (${Math.round(result.similarity * 100)}% similar)`,
|
|
217
|
+
similarity: result.similarity,
|
|
218
|
+
differences: result.differences,
|
|
219
|
+
duration: result.duration,
|
|
220
|
+
};
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
pass: false,
|
|
225
|
+
output: `Test failed: ${error.message}`,
|
|
226
|
+
error: error.message,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Execute approve command
|
|
233
|
+
* @param {Object} args - Parsed arguments
|
|
234
|
+
* @returns {Promise<Object>} Approval result
|
|
235
|
+
*/
|
|
236
|
+
async executeApprove(args) {
|
|
237
|
+
try {
|
|
238
|
+
await updateBaseline(this.tester, {
|
|
239
|
+
name: args.name,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
this.pendingApprovals.delete(args.name);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
success: true,
|
|
246
|
+
output: `Baseline updated: ${args.name}`,
|
|
247
|
+
};
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
output: `Failed to approve: ${error.message}`,
|
|
252
|
+
error: error.message,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Execute list command
|
|
259
|
+
* @param {Object} args - Parsed arguments
|
|
260
|
+
* @returns {Promise<Object>} List result
|
|
261
|
+
*/
|
|
262
|
+
async executeList(args) {
|
|
263
|
+
try {
|
|
264
|
+
const files = await this.tester._listFiles(this.tester.baselineDir);
|
|
265
|
+
const baselines = files.filter(f => f.endsWith('.png')).map(f => path.basename(f, '.png'));
|
|
266
|
+
|
|
267
|
+
const lines = [
|
|
268
|
+
'Visual Baselines',
|
|
269
|
+
'═'.repeat(40),
|
|
270
|
+
'',
|
|
271
|
+
...baselines.map(b => ` - ${b}`),
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
output: lines.join('\n'),
|
|
277
|
+
baselines,
|
|
278
|
+
};
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
output: `Failed to list baselines: ${error.message}`,
|
|
283
|
+
baselines: [],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Execute run command (all tests)
|
|
290
|
+
* @param {Object} args - Parsed arguments
|
|
291
|
+
* @returns {Promise<Object>} Run result
|
|
292
|
+
*/
|
|
293
|
+
async executeRun(args) {
|
|
294
|
+
try {
|
|
295
|
+
const files = await this.tester._listFiles(this.tester.baselineDir);
|
|
296
|
+
let baselines = files.filter(f => f.endsWith('.png')).map(f => path.basename(f, '.png'));
|
|
297
|
+
|
|
298
|
+
// Filter by pattern if specified
|
|
299
|
+
if (args.pattern) {
|
|
300
|
+
baselines = baselines.filter(b => b.includes(args.pattern));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const results = [];
|
|
304
|
+
|
|
305
|
+
for (const name of baselines) {
|
|
306
|
+
// Load metadata for URL
|
|
307
|
+
const metadataPath = path.join(this.tester.baselineDir, `${name}.json`);
|
|
308
|
+
let url;
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const metadata = JSON.parse(await this.tester._readFile(metadataPath));
|
|
312
|
+
url = metadata.url;
|
|
313
|
+
} catch {
|
|
314
|
+
url = 'http://localhost:3000'; // Default
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const result = await runVisualTest(this.tester, {
|
|
318
|
+
name,
|
|
319
|
+
url,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
results.push(result);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
success: true,
|
|
327
|
+
output: formatVisualReport(results),
|
|
328
|
+
results,
|
|
329
|
+
summary: {
|
|
330
|
+
total: results.length,
|
|
331
|
+
passed: results.filter(r => r.pass).length,
|
|
332
|
+
failed: results.filter(r => !r.pass).length,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
} catch (error) {
|
|
336
|
+
return {
|
|
337
|
+
success: false,
|
|
338
|
+
output: `Failed to run tests: ${error.message}`,
|
|
339
|
+
results: [],
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
VisualCommand,
|
|
347
|
+
parseArgs,
|
|
348
|
+
parseViewport,
|
|
349
|
+
formatTestSummary,
|
|
350
|
+
};
|