@vizzly-testing/cli 0.10.3 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -8
- package/claude-plugin/.claude-plugin/.mcp.json +8 -0
- package/claude-plugin/.claude-plugin/README.md +114 -0
- package/claude-plugin/.claude-plugin/marketplace.json +28 -0
- package/claude-plugin/.claude-plugin/plugin.json +14 -0
- package/claude-plugin/commands/debug-diff.md +153 -0
- package/claude-plugin/commands/setup.md +137 -0
- package/claude-plugin/commands/suggest-screenshots.md +111 -0
- package/claude-plugin/commands/tdd-status.md +43 -0
- package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +354 -0
- package/claude-plugin/mcp/vizzly-server/index.js +861 -0
- package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +422 -0
- package/claude-plugin/mcp/vizzly-server/token-resolver.js +185 -0
- package/dist/cli.js +64 -0
- package/dist/client/index.js +13 -3
- package/dist/commands/login.js +195 -0
- package/dist/commands/logout.js +71 -0
- package/dist/commands/project.js +351 -0
- package/dist/commands/run.js +30 -0
- package/dist/commands/whoami.js +162 -0
- package/dist/plugin-loader.js +9 -15
- package/dist/sdk/index.js +16 -4
- package/dist/services/api-service.js +50 -7
- package/dist/services/auth-service.js +226 -0
- package/dist/types/client/index.d.ts +9 -3
- package/dist/types/commands/login.d.ts +11 -0
- package/dist/types/commands/logout.d.ts +11 -0
- package/dist/types/commands/project.d.ts +28 -0
- package/dist/types/commands/whoami.d.ts +11 -0
- package/dist/types/sdk/index.d.ts +9 -4
- package/dist/types/services/api-service.d.ts +2 -1
- package/dist/types/services/auth-service.d.ts +59 -0
- package/dist/types/utils/browser.d.ts +6 -0
- package/dist/types/utils/config-loader.d.ts +1 -1
- package/dist/types/utils/config-schema.d.ts +8 -174
- package/dist/types/utils/file-helpers.d.ts +18 -0
- package/dist/types/utils/global-config.d.ts +84 -0
- package/dist/utils/browser.js +44 -0
- package/dist/utils/config-loader.js +69 -3
- package/dist/utils/file-helpers.js +64 -0
- package/dist/utils/global-config.js +259 -0
- package/docs/api-reference.md +177 -6
- package/docs/authentication.md +334 -0
- package/docs/getting-started.md +21 -2
- package/docs/plugins.md +27 -0
- package/docs/test-integration.md +60 -10
- package/package.json +5 -3
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Analyze test files and suggest where visual screenshots would be valuable
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Suggest Screenshot Opportunities
|
|
6
|
+
|
|
7
|
+
Analyze existing test files to identify ideal locations for visual regression testing.
|
|
8
|
+
|
|
9
|
+
## Process
|
|
10
|
+
|
|
11
|
+
1. **Detect SDK type** (same logic as setup command)
|
|
12
|
+
|
|
13
|
+
**If Storybook detected** (`@storybook/*` in package.json or `.storybook/` directory):
|
|
14
|
+
- Inform user that Storybook SDK auto-discovers all stories
|
|
15
|
+
- No manual screenshot calls needed
|
|
16
|
+
- Exit early
|
|
17
|
+
|
|
18
|
+
**If Static Site detected** (build directories like `dist/`, `build/`, `.next/out/` or static site generators):
|
|
19
|
+
- Inform user that Static Site SDK auto-discovers all pages
|
|
20
|
+
- No manual screenshot calls needed
|
|
21
|
+
- Exit early
|
|
22
|
+
|
|
23
|
+
**Otherwise continue** with test file analysis below.
|
|
24
|
+
|
|
25
|
+
2. **Ask user for test directory** if not obvious (e.g., `tests/`, `e2e/`, `__tests__/`, `spec/`)
|
|
26
|
+
|
|
27
|
+
3. **Find test files** using glob patterns:
|
|
28
|
+
- `**/*.test.{js,ts,jsx,tsx}`
|
|
29
|
+
- `**/*.spec.{js,ts,jsx,tsx}`
|
|
30
|
+
- `**/e2e/**/*.{js,ts}`
|
|
31
|
+
|
|
32
|
+
**IMPORTANT: Exclude these directories:**
|
|
33
|
+
- `node_modules/`
|
|
34
|
+
- `dist/`, `build/`, `out/`
|
|
35
|
+
- `.next/`, `.nuxt/`, `.vite/`
|
|
36
|
+
- `coverage/`, `.nyc_output/`
|
|
37
|
+
- `vendor/`
|
|
38
|
+
- Any hidden directories (`.*/`)
|
|
39
|
+
|
|
40
|
+
Use the Glob tool with explicit exclusion or filter results to avoid these directories.
|
|
41
|
+
|
|
42
|
+
4. **Analyze test files** looking for:
|
|
43
|
+
- Page navigations (`.goto()`, `.visit()`, `navigate()`)
|
|
44
|
+
- User interactions before assertions (`.click()`, `.type()`, `.fill()`)
|
|
45
|
+
- Component rendering (React Testing Library, etc.)
|
|
46
|
+
- Visual assertions or wait conditions
|
|
47
|
+
|
|
48
|
+
5. **Suggest screenshot points** where:
|
|
49
|
+
- After page loads or navigations
|
|
50
|
+
- After key user interactions
|
|
51
|
+
- Before visual assertions
|
|
52
|
+
- At different viewport sizes
|
|
53
|
+
- For different user states (logged in/out, etc.)
|
|
54
|
+
|
|
55
|
+
6. **Provide code examples** specific to their test framework:
|
|
56
|
+
|
|
57
|
+
**Playwright:**
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
|
|
61
|
+
|
|
62
|
+
// After page navigation
|
|
63
|
+
await page.goto('/dashboard');
|
|
64
|
+
await vizzlyScreenshot('dashboard-logged-in', await page.screenshot(), {
|
|
65
|
+
browser: 'chrome',
|
|
66
|
+
viewport: '1920x1080'
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Cypress:**
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
cy.visit('/login');
|
|
74
|
+
cy.screenshot('login-page');
|
|
75
|
+
// Then add vizzlyScreenshot in custom command
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Output Format
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Found 8 potential screenshot opportunities in your tests:
|
|
82
|
+
|
|
83
|
+
tests/e2e/auth.spec.js:
|
|
84
|
+
Line 15: After login page navigation
|
|
85
|
+
Suggested screenshot: 'login-page'
|
|
86
|
+
Reason: Page load after navigation
|
|
87
|
+
|
|
88
|
+
Line 28: After successful login
|
|
89
|
+
Suggested screenshot: 'dashboard-authenticated'
|
|
90
|
+
Reason: User state change (logged in)
|
|
91
|
+
|
|
92
|
+
tests/e2e/checkout.spec.js:
|
|
93
|
+
Line 42: Shopping cart with items
|
|
94
|
+
Suggested screenshot: 'cart-with-items'
|
|
95
|
+
Reason: Visual state after user interaction
|
|
96
|
+
|
|
97
|
+
Line 67: Checkout confirmation page
|
|
98
|
+
Suggested screenshot: 'order-confirmation'
|
|
99
|
+
Reason: Final state of user flow
|
|
100
|
+
|
|
101
|
+
Example integration for Playwright:
|
|
102
|
+
[Provide code example specific to their test]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Important Notes
|
|
106
|
+
|
|
107
|
+
- **Do NOT modify test files**
|
|
108
|
+
- **Do NOT create new test files**
|
|
109
|
+
- Only provide suggestions and examples
|
|
110
|
+
- Let the user decide where to add screenshots
|
|
111
|
+
- Respect their test framework and structure
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Check TDD dashboard status and view visual regression test results
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Check Vizzly TDD Status
|
|
6
|
+
|
|
7
|
+
Use the Vizzly MCP server to check the current TDD status:
|
|
8
|
+
|
|
9
|
+
1. Call the `get_tdd_status` tool from the vizzly MCP server
|
|
10
|
+
2. Analyze the comparison results
|
|
11
|
+
3. Show a summary of:
|
|
12
|
+
- Total screenshots tested
|
|
13
|
+
- Passed, failed, and new screenshot counts
|
|
14
|
+
- List of failed comparisons with diff percentages
|
|
15
|
+
- Available diff images to inspect
|
|
16
|
+
4. If TDD server is running, provide the dashboard URL
|
|
17
|
+
5. For failed comparisons, provide guidance on next steps
|
|
18
|
+
|
|
19
|
+
## Example Output Format
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Vizzly TDD Status:
|
|
23
|
+
✅ Total: 15 screenshots
|
|
24
|
+
✅ Passed: 12
|
|
25
|
+
❌ Failed: 2 (exceeded threshold)
|
|
26
|
+
🆕 New: 1 (no baseline)
|
|
27
|
+
|
|
28
|
+
Failed Comparisons:
|
|
29
|
+
- homepage (2.3% diff) - Check .vizzly/diffs/homepage.png
|
|
30
|
+
- login-form (1.8% diff) - Check .vizzly/diffs/login-form.png
|
|
31
|
+
|
|
32
|
+
New Screenshots:
|
|
33
|
+
- dashboard (no baseline for comparison)
|
|
34
|
+
|
|
35
|
+
Dashboard: http://localhost:47392
|
|
36
|
+
|
|
37
|
+
Next Steps:
|
|
38
|
+
- Review diff images to understand what changed
|
|
39
|
+
- Accept baselines from dashboard if changes are intentional
|
|
40
|
+
- Fix visual issues if changes are unintentional
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Focus on providing actionable information to help the developer understand what's failing and why.
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider for Vizzly Cloud API integration
|
|
3
|
+
*/
|
|
4
|
+
export class CloudAPIProvider {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.defaultApiUrl = process.env.VIZZLY_API_URL || 'https://app.vizzly.dev';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Make API request to Vizzly
|
|
11
|
+
*/
|
|
12
|
+
async makeRequest(path, apiToken, apiUrl = this.defaultApiUrl) {
|
|
13
|
+
if (!apiToken) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'API token required. Set VIZZLY_TOKEN environment variable or provide via apiToken parameter.'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let url = `${apiUrl}${path}`;
|
|
20
|
+
let response = await fetch(url, {
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${apiToken}`,
|
|
23
|
+
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0'
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
let error = await response.text();
|
|
29
|
+
throw new Error(`API request failed (${response.status}): ${error}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return response.json();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get build status and details
|
|
37
|
+
*/
|
|
38
|
+
async getBuildStatus(buildId, apiToken, apiUrl) {
|
|
39
|
+
if (!buildId || typeof buildId !== 'string') {
|
|
40
|
+
throw new Error('buildId is required and must be a non-empty string');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let data = await this.makeRequest(
|
|
44
|
+
`/api/sdk/builds/${buildId}?include=comparisons`,
|
|
45
|
+
apiToken,
|
|
46
|
+
apiUrl
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
let { build } = data;
|
|
50
|
+
|
|
51
|
+
// Calculate comparison summary
|
|
52
|
+
let comparisons = build.comparisons || [];
|
|
53
|
+
let summary = {
|
|
54
|
+
total: comparisons.length,
|
|
55
|
+
new: comparisons.filter((c) => c.status === 'new').length,
|
|
56
|
+
changed: comparisons.filter((c) => c.has_diff).length,
|
|
57
|
+
identical: comparisons.filter((c) => !c.has_diff && c.status !== 'new').length
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Group comparisons by status
|
|
61
|
+
let failedComparisons = comparisons
|
|
62
|
+
.filter((c) => c.has_diff)
|
|
63
|
+
.map((c) => ({
|
|
64
|
+
name: c.name,
|
|
65
|
+
diffPercentage: c.diff_percentage,
|
|
66
|
+
currentUrl: c.current_screenshot?.original_url,
|
|
67
|
+
baselineUrl: c.baseline_screenshot?.original_url,
|
|
68
|
+
diffUrl: c.diff_image?.url
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
let newComparisons = comparisons
|
|
72
|
+
.filter((c) => c.status === 'new')
|
|
73
|
+
.map((c) => ({
|
|
74
|
+
name: c.name,
|
|
75
|
+
currentUrl: c.current_screenshot?.original_url
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
build: {
|
|
80
|
+
id: build.id,
|
|
81
|
+
name: build.name,
|
|
82
|
+
branch: build.branch,
|
|
83
|
+
status: build.status,
|
|
84
|
+
url: build.url,
|
|
85
|
+
organizationSlug: build.organizationSlug,
|
|
86
|
+
projectSlug: build.projectSlug,
|
|
87
|
+
createdAt: build.created_at,
|
|
88
|
+
// Include commit details for debugging
|
|
89
|
+
commitSha: build.commit_sha,
|
|
90
|
+
commitMessage: build.commit_message,
|
|
91
|
+
commonAncestorSha: build.common_ancestor_sha
|
|
92
|
+
},
|
|
93
|
+
summary,
|
|
94
|
+
failedComparisons,
|
|
95
|
+
newComparisons
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List recent builds
|
|
101
|
+
*/
|
|
102
|
+
async listRecentBuilds(apiToken, options = {}) {
|
|
103
|
+
let { limit = 10, branch, apiUrl } = options;
|
|
104
|
+
|
|
105
|
+
let queryParams = new URLSearchParams({
|
|
106
|
+
limit: limit.toString()
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (branch) {
|
|
110
|
+
queryParams.append('branch', branch);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let data = await this.makeRequest(`/api/sdk/builds?${queryParams}`, apiToken, apiUrl);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
builds: data.builds.map((b) => ({
|
|
117
|
+
id: b.id,
|
|
118
|
+
name: b.name,
|
|
119
|
+
branch: b.branch,
|
|
120
|
+
status: b.status,
|
|
121
|
+
environment: b.environment,
|
|
122
|
+
createdAt: b.created_at
|
|
123
|
+
})),
|
|
124
|
+
pagination: data.pagination
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get token context (organization and project info)
|
|
130
|
+
*/
|
|
131
|
+
async getTokenContext(apiToken, apiUrl) {
|
|
132
|
+
return await this.makeRequest('/api/sdk/token/context', apiToken, apiUrl);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get comparison details
|
|
137
|
+
*/
|
|
138
|
+
async getComparison(comparisonId, apiToken, apiUrl) {
|
|
139
|
+
if (!comparisonId || typeof comparisonId !== 'string') {
|
|
140
|
+
throw new Error('comparisonId is required and must be a non-empty string');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let data = await this.makeRequest(`/api/sdk/comparisons/${comparisonId}`, apiToken, apiUrl);
|
|
144
|
+
|
|
145
|
+
return data.comparison;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ==================================================================
|
|
149
|
+
// BUILD COMMENTS
|
|
150
|
+
// ==================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create a comment on a build
|
|
154
|
+
*/
|
|
155
|
+
async createBuildComment(buildId, content, type, apiToken, apiUrl) {
|
|
156
|
+
if (!buildId || typeof buildId !== 'string') {
|
|
157
|
+
throw new Error('buildId is required and must be a non-empty string');
|
|
158
|
+
}
|
|
159
|
+
if (!content || typeof content !== 'string') {
|
|
160
|
+
throw new Error('content is required and must be a non-empty string');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let url = `${apiUrl || this.defaultApiUrl}/api/sdk/builds/${buildId}/comments`;
|
|
164
|
+
let response = await fetch(url, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: {
|
|
167
|
+
Authorization: `Bearer ${apiToken}`,
|
|
168
|
+
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
|
|
169
|
+
'Content-Type': 'application/json'
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({ content, type })
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
let error = await response.text();
|
|
176
|
+
throw new Error(`Failed to create comment (${response.status}): ${error}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return response.json();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* List comments for a build
|
|
184
|
+
*/
|
|
185
|
+
async listBuildComments(buildId, apiToken, apiUrl) {
|
|
186
|
+
if (!buildId || typeof buildId !== 'string') {
|
|
187
|
+
throw new Error('buildId is required and must be a non-empty string');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let data = await this.makeRequest(`/api/sdk/builds/${buildId}/comments`, apiToken, apiUrl);
|
|
191
|
+
|
|
192
|
+
// Filter out unnecessary fields from comments for MCP
|
|
193
|
+
let filterComment = (comment) => {
|
|
194
|
+
// eslint-disable-next-line no-unused-vars
|
|
195
|
+
let { profile_photo_url, email, ...filtered } = comment;
|
|
196
|
+
// Recursively filter replies if they exist
|
|
197
|
+
if (filtered.replies && Array.isArray(filtered.replies)) {
|
|
198
|
+
filtered.replies = filtered.replies.map(filterComment);
|
|
199
|
+
}
|
|
200
|
+
return filtered;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
...data,
|
|
205
|
+
comments: data.comments ? data.comments.map(filterComment) : []
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ==================================================================
|
|
210
|
+
// COMPARISON APPROVALS
|
|
211
|
+
// ==================================================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Approve a comparison
|
|
215
|
+
*/
|
|
216
|
+
async approveComparison(comparisonId, comment, apiToken, apiUrl) {
|
|
217
|
+
if (!comparisonId || typeof comparisonId !== 'string') {
|
|
218
|
+
throw new Error('comparisonId is required and must be a non-empty string');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/approve`;
|
|
222
|
+
let response = await fetch(url, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {
|
|
225
|
+
Authorization: `Bearer ${apiToken}`,
|
|
226
|
+
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
|
|
227
|
+
'Content-Type': 'application/json'
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify({ comment })
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
let error = await response.text();
|
|
234
|
+
throw new Error(`Failed to approve comparison (${response.status}): ${error}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return response.json();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Reject a comparison
|
|
242
|
+
*/
|
|
243
|
+
async rejectComparison(comparisonId, reason, apiToken, apiUrl) {
|
|
244
|
+
if (!comparisonId || typeof comparisonId !== 'string') {
|
|
245
|
+
throw new Error('comparisonId is required and must be a non-empty string');
|
|
246
|
+
}
|
|
247
|
+
if (!reason || typeof reason !== 'string') {
|
|
248
|
+
throw new Error('reason is required and must be a non-empty string');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/reject`;
|
|
252
|
+
let response = await fetch(url, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: `Bearer ${apiToken}`,
|
|
256
|
+
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
|
|
257
|
+
'Content-Type': 'application/json'
|
|
258
|
+
},
|
|
259
|
+
body: JSON.stringify({ reason })
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
let error = await response.text();
|
|
264
|
+
throw new Error(`Failed to reject comparison (${response.status}): ${error}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return response.json();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Update comparison approval status
|
|
272
|
+
*/
|
|
273
|
+
async updateComparisonApproval(comparisonId, approvalStatus, comment, apiToken, apiUrl) {
|
|
274
|
+
let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/approval`;
|
|
275
|
+
let response = await fetch(url, {
|
|
276
|
+
method: 'PUT',
|
|
277
|
+
headers: {
|
|
278
|
+
Authorization: `Bearer ${apiToken}`,
|
|
279
|
+
'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
|
|
280
|
+
'Content-Type': 'application/json'
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({ approval_status: approvalStatus, comment })
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
let error = await response.text();
|
|
287
|
+
throw new Error(`Failed to update comparison approval (${response.status}): ${error}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return response.json();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ==================================================================
|
|
294
|
+
// REVIEW STATUS
|
|
295
|
+
// ==================================================================
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get review summary for a build
|
|
299
|
+
*/
|
|
300
|
+
async getReviewSummary(buildId, apiToken, apiUrl) {
|
|
301
|
+
if (!buildId || typeof buildId !== 'string') {
|
|
302
|
+
throw new Error('buildId is required and must be a non-empty string');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let data = await this.makeRequest(
|
|
306
|
+
`/api/sdk/builds/${buildId}/review-summary`,
|
|
307
|
+
apiToken,
|
|
308
|
+
apiUrl
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
return data;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ==================================================================
|
|
315
|
+
// TDD WORKFLOW
|
|
316
|
+
// ==================================================================
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Download baseline screenshots from a cloud build
|
|
320
|
+
* Returns screenshot data that can be saved locally
|
|
321
|
+
*/
|
|
322
|
+
async downloadBaselines(buildId, screenshotNames, apiToken, apiUrl) {
|
|
323
|
+
if (!buildId || typeof buildId !== 'string') {
|
|
324
|
+
throw new Error('buildId is required and must be a non-empty string');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let data = await this.makeRequest(
|
|
328
|
+
`/api/sdk/builds/${buildId}?include=screenshots`,
|
|
329
|
+
apiToken,
|
|
330
|
+
apiUrl
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
let { build } = data;
|
|
334
|
+
let screenshots = build.screenshots || [];
|
|
335
|
+
|
|
336
|
+
// Filter by screenshot names if provided
|
|
337
|
+
if (screenshotNames && screenshotNames.length > 0) {
|
|
338
|
+
screenshots = screenshots.filter((s) => screenshotNames.includes(s.name));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
buildId: build.id,
|
|
343
|
+
buildName: build.name,
|
|
344
|
+
screenshots: screenshots.map((s) => ({
|
|
345
|
+
name: s.name,
|
|
346
|
+
url: s.original_url,
|
|
347
|
+
sha256: s.sha256,
|
|
348
|
+
width: s.viewport_width,
|
|
349
|
+
height: s.viewport_height,
|
|
350
|
+
browser: s.browser
|
|
351
|
+
}))
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|