@vizzly-testing/cli 0.16.4 → 0.18.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 +4 -4
- package/claude-plugin/skills/debug-visual-regression/SKILL.md +2 -2
- package/dist/cli.js +84 -58
- package/dist/client/index.js +6 -6
- package/dist/commands/doctor.js +18 -17
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/init.js +30 -30
- package/dist/commands/login.js +23 -23
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +36 -36
- package/dist/commands/run.js +33 -33
- package/dist/commands/status.js +14 -14
- package/dist/commands/tdd-daemon.js +43 -43
- package/dist/commands/tdd.js +27 -27
- package/dist/commands/upload.js +33 -33
- package/dist/commands/whoami.js +12 -12
- package/dist/index.js +9 -14
- package/dist/plugin-loader.js +28 -28
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +19 -19
- package/dist/sdk/index.js +33 -35
- package/dist/server/handlers/api-handler.js +4 -4
- package/dist/server/handlers/tdd-handler.js +12 -12
- package/dist/server/http-server.js +21 -22
- package/dist/server/middleware/json-parser.js +1 -1
- package/dist/server/routers/assets.js +14 -14
- package/dist/server/routers/auth.js +14 -14
- package/dist/server/routers/baseline.js +8 -8
- package/dist/server/routers/cloud-proxy.js +15 -15
- package/dist/server/routers/config.js +11 -11
- package/dist/server/routers/dashboard.js +11 -11
- package/dist/server/routers/health.js +4 -4
- package/dist/server/routers/projects.js +19 -19
- package/dist/server/routers/screenshot.js +9 -9
- package/dist/services/api-service.js +16 -16
- package/dist/services/auth-service.js +17 -17
- package/dist/services/build-manager.js +3 -3
- package/dist/services/config-service.js +33 -33
- package/dist/services/html-report-generator.js +8 -8
- package/dist/services/index.js +11 -11
- package/dist/services/project-service.js +19 -19
- package/dist/services/report-generator/report.css +3 -3
- package/dist/services/report-generator/viewer.js +25 -23
- package/dist/services/screenshot-server.js +1 -1
- package/dist/services/server-manager.js +5 -5
- package/dist/services/static-report-generator.js +14 -14
- package/dist/services/tdd-service.js +101 -95
- package/dist/services/test-runner.js +14 -4
- package/dist/services/uploader.js +10 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/utils/browser.js +3 -3
- package/dist/utils/build-history.js +12 -12
- package/dist/utils/config-loader.js +19 -19
- package/dist/utils/config-schema.js +10 -9
- package/dist/utils/environment-config.js +11 -0
- package/dist/utils/fetch-utils.js +2 -2
- package/dist/utils/file-helpers.js +2 -2
- package/dist/utils/git.js +3 -6
- package/dist/utils/global-config.js +28 -25
- package/dist/utils/output.js +136 -28
- package/dist/utils/package-info.js +3 -3
- package/dist/utils/security.js +12 -12
- package/docs/api-reference.md +56 -27
- package/docs/doctor-command.md +1 -1
- package/docs/tdd-mode.md +3 -3
- package/package.json +9 -13
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Manages reading and writing Vizzly configuration files
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
6
8
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
7
|
-
import { writeFile, readFile } from 'fs/promises';
|
|
8
|
-
import { join } from 'path';
|
|
9
9
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
10
10
|
import { validateVizzlyConfigWithDefaults } from '../utils/config-schema.js';
|
|
11
|
-
import { loadGlobalConfig, saveGlobalConfig
|
|
11
|
+
import { getGlobalConfigPath, loadGlobalConfig, saveGlobalConfig } from '../utils/global-config.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* ConfigService for reading and writing configuration
|
|
@@ -44,7 +44,7 @@ export class ConfigService {
|
|
|
44
44
|
* @returns {Promise<Object>}
|
|
45
45
|
*/
|
|
46
46
|
async _getProjectConfig() {
|
|
47
|
-
|
|
47
|
+
const result = this.explorer.search(this.projectRoot);
|
|
48
48
|
if (!result || !result.config) {
|
|
49
49
|
return {
|
|
50
50
|
config: {},
|
|
@@ -52,7 +52,7 @@ export class ConfigService {
|
|
|
52
52
|
isEmpty: true
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
const config = result.config.default || result.config;
|
|
56
56
|
return {
|
|
57
57
|
config,
|
|
58
58
|
filepath: result.filepath,
|
|
@@ -66,7 +66,7 @@ export class ConfigService {
|
|
|
66
66
|
* @returns {Promise<Object>}
|
|
67
67
|
*/
|
|
68
68
|
async _getGlobalConfig() {
|
|
69
|
-
|
|
69
|
+
const globalConfig = await loadGlobalConfig();
|
|
70
70
|
return {
|
|
71
71
|
config: globalConfig,
|
|
72
72
|
filepath: getGlobalConfigPath(),
|
|
@@ -80,15 +80,15 @@ export class ConfigService {
|
|
|
80
80
|
* @returns {Promise<Object>}
|
|
81
81
|
*/
|
|
82
82
|
async _getMergedConfig() {
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const projectConfigData = await this._getProjectConfig();
|
|
84
|
+
const globalConfigData = await this._getGlobalConfig();
|
|
85
85
|
|
|
86
86
|
// Build config with source tracking
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const mergedConfig = {};
|
|
88
|
+
const sources = {};
|
|
89
89
|
|
|
90
90
|
// Layer 1: Defaults
|
|
91
|
-
|
|
91
|
+
const defaults = {
|
|
92
92
|
apiUrl: 'https://app.vizzly.dev',
|
|
93
93
|
server: {
|
|
94
94
|
port: 47392,
|
|
@@ -104,7 +104,7 @@ export class ConfigService {
|
|
|
104
104
|
timeout: 30000
|
|
105
105
|
},
|
|
106
106
|
comparison: {
|
|
107
|
-
threshold: 0
|
|
107
|
+
threshold: 2.0
|
|
108
108
|
},
|
|
109
109
|
tdd: {
|
|
110
110
|
openReport: false
|
|
@@ -133,7 +133,7 @@ export class ConfigService {
|
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
// Layer 4: Environment variables (tracked separately)
|
|
136
|
-
|
|
136
|
+
const envOverrides = {};
|
|
137
137
|
if (process.env.VIZZLY_TOKEN) {
|
|
138
138
|
envOverrides.apiKey = process.env.VIZZLY_TOKEN;
|
|
139
139
|
sources.apiKey = 'env';
|
|
@@ -176,12 +176,12 @@ export class ConfigService {
|
|
|
176
176
|
* @returns {Promise<Object>} Updated config
|
|
177
177
|
*/
|
|
178
178
|
async _updateProjectConfig(updates) {
|
|
179
|
-
|
|
179
|
+
const result = this.explorer.search(this.projectRoot);
|
|
180
180
|
|
|
181
181
|
// Determine config file path
|
|
182
182
|
let configPath;
|
|
183
183
|
let currentConfig = {};
|
|
184
|
-
if (result
|
|
184
|
+
if (result?.filepath) {
|
|
185
185
|
configPath = result.filepath;
|
|
186
186
|
currentConfig = result.config.default || result.config;
|
|
187
187
|
} else {
|
|
@@ -190,7 +190,7 @@ export class ConfigService {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// Merge updates with current config
|
|
193
|
-
|
|
193
|
+
const newConfig = this._deepMerge(currentConfig, updates);
|
|
194
194
|
|
|
195
195
|
// Validate before writing
|
|
196
196
|
try {
|
|
@@ -219,8 +219,8 @@ export class ConfigService {
|
|
|
219
219
|
* @returns {Promise<Object>} Updated config
|
|
220
220
|
*/
|
|
221
221
|
async _updateGlobalConfig(updates) {
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
const currentConfig = await loadGlobalConfig();
|
|
223
|
+
const newConfig = this._deepMerge(currentConfig, updates);
|
|
224
224
|
await saveGlobalConfig(newConfig);
|
|
225
225
|
return {
|
|
226
226
|
config: newConfig,
|
|
@@ -238,22 +238,22 @@ export class ConfigService {
|
|
|
238
238
|
async _writeProjectConfigFile(filepath, config) {
|
|
239
239
|
// For .js files, export as ES module
|
|
240
240
|
if (filepath.endsWith('.js') || filepath.endsWith('.mjs')) {
|
|
241
|
-
|
|
241
|
+
const content = this._serializeToJavaScript(config);
|
|
242
242
|
await writeFile(filepath, content, 'utf-8');
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
// For .json files
|
|
247
247
|
if (filepath.endsWith('.json')) {
|
|
248
|
-
|
|
248
|
+
const content = JSON.stringify(config, null, 2);
|
|
249
249
|
await writeFile(filepath, content, 'utf-8');
|
|
250
250
|
return;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// For package.json, merge into existing
|
|
254
254
|
if (filepath.endsWith('package.json')) {
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
const pkgContent = await readFile(filepath, 'utf-8');
|
|
256
|
+
const pkg = JSON.parse(pkgContent);
|
|
257
257
|
pkg.vizzly = config;
|
|
258
258
|
await writeFile(filepath, JSON.stringify(pkg, null, 2), 'utf-8');
|
|
259
259
|
return;
|
|
@@ -268,7 +268,7 @@ export class ConfigService {
|
|
|
268
268
|
* @returns {string} JavaScript source code
|
|
269
269
|
*/
|
|
270
270
|
_serializeToJavaScript(config) {
|
|
271
|
-
|
|
271
|
+
const lines = ['/**', ' * Vizzly Configuration', ' * @see https://docs.vizzly.dev/cli/configuration', ' */', '', "import { defineConfig } from '@vizzly-testing/cli/config';", '', 'export default defineConfig(', this._stringifyWithIndent(config, 1), ');', ''];
|
|
272
272
|
return lines.join('\n');
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -280,8 +280,8 @@ export class ConfigService {
|
|
|
280
280
|
* @returns {string}
|
|
281
281
|
*/
|
|
282
282
|
_stringifyWithIndent(value, depth = 0) {
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
const indent = ' '.repeat(depth);
|
|
284
|
+
const prevIndent = ' '.repeat(depth - 1);
|
|
285
285
|
if (value === null || value === undefined) {
|
|
286
286
|
return String(value);
|
|
287
287
|
}
|
|
@@ -293,14 +293,14 @@ export class ConfigService {
|
|
|
293
293
|
}
|
|
294
294
|
if (Array.isArray(value)) {
|
|
295
295
|
if (value.length === 0) return '[]';
|
|
296
|
-
|
|
296
|
+
const items = value.map(item => `${indent}${this._stringifyWithIndent(item, depth + 1)}`);
|
|
297
297
|
return `[\n${items.join(',\n')}\n${prevIndent}]`;
|
|
298
298
|
}
|
|
299
299
|
if (typeof value === 'object') {
|
|
300
|
-
|
|
300
|
+
const keys = Object.keys(value);
|
|
301
301
|
if (keys.length === 0) return '{}';
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
const items = keys.map(key => {
|
|
303
|
+
const val = this._stringifyWithIndent(value[key], depth + 1);
|
|
304
304
|
return `${indent}${key}: ${val}`;
|
|
305
305
|
});
|
|
306
306
|
return `{\n${items.join(',\n')}\n${prevIndent}}`;
|
|
@@ -315,7 +315,7 @@ export class ConfigService {
|
|
|
315
315
|
*/
|
|
316
316
|
async validateConfig(config) {
|
|
317
317
|
try {
|
|
318
|
-
|
|
318
|
+
const validated = validateVizzlyConfigWithDefaults(config);
|
|
319
319
|
return {
|
|
320
320
|
valid: true,
|
|
321
321
|
config: validated,
|
|
@@ -338,7 +338,7 @@ export class ConfigService {
|
|
|
338
338
|
* @returns {Promise<string>} Source ('default', 'global', 'project', 'env', 'cli')
|
|
339
339
|
*/
|
|
340
340
|
async getConfigSource(key) {
|
|
341
|
-
|
|
341
|
+
const merged = await this._getMergedConfig();
|
|
342
342
|
return merged.sources[key] || 'unknown';
|
|
343
343
|
}
|
|
344
344
|
|
|
@@ -350,10 +350,10 @@ export class ConfigService {
|
|
|
350
350
|
* @returns {Object} Merged object
|
|
351
351
|
*/
|
|
352
352
|
_deepMerge(target, source) {
|
|
353
|
-
|
|
353
|
+
const output = {
|
|
354
354
|
...target
|
|
355
355
|
};
|
|
356
|
-
for (
|
|
356
|
+
for (const key in source) {
|
|
357
357
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
358
358
|
if (target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
|
|
359
359
|
output[key] = this._deepMerge(target[key], source[key]);
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Creates an interactive report with overlay, toggle, and onion skin modes
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { join, relative
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { dirname, join, relative } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import * as output from '../utils/output.js';
|
|
11
11
|
export class HtmlReportGenerator {
|
|
12
12
|
constructor(workingDir, config) {
|
|
@@ -16,8 +16,8 @@ export class HtmlReportGenerator {
|
|
|
16
16
|
this.reportPath = join(this.reportDir, 'index.html');
|
|
17
17
|
|
|
18
18
|
// Get path to the CSS file that ships with the package
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
21
|
this.cssPath = join(__dirname, 'report-generator', 'report.css');
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -37,7 +37,7 @@ export class HtmlReportGenerator {
|
|
|
37
37
|
* @returns {Object} Sanitized build info
|
|
38
38
|
*/
|
|
39
39
|
sanitizeBuildInfo(buildInfo = {}) {
|
|
40
|
-
|
|
40
|
+
const sanitized = {};
|
|
41
41
|
if (buildInfo.baseline && typeof buildInfo.baseline === 'object') {
|
|
42
42
|
sanitized.baseline = {
|
|
43
43
|
buildId: this.sanitizeHtml(buildInfo.baseline.buildId || ''),
|
|
@@ -383,7 +383,7 @@ function rejectChanges(screenshotName) {
|
|
|
383
383
|
<p>Missing comparison images</p>
|
|
384
384
|
</div>`;
|
|
385
385
|
}
|
|
386
|
-
|
|
386
|
+
const safeName = this.sanitizeHtml(comparison.name);
|
|
387
387
|
return `
|
|
388
388
|
<div class="comparison" data-comparison="${safeName}">
|
|
389
389
|
<div class="comparison-header">
|
package/dist/services/index.js
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { ApiService } from './api-service.js';
|
|
7
7
|
import { AuthService } from './auth-service.js';
|
|
8
|
+
import { BuildManager } from './build-manager.js';
|
|
8
9
|
import { ConfigService } from './config-service.js';
|
|
9
10
|
import { ProjectService } from './project-service.js';
|
|
10
|
-
import { createUploader } from './uploader.js';
|
|
11
|
-
import { BuildManager } from './build-manager.js';
|
|
12
11
|
import { ServerManager } from './server-manager.js';
|
|
13
12
|
import { createTDDService } from './tdd-service.js';
|
|
14
13
|
import { TestRunner } from './test-runner.js';
|
|
14
|
+
import { createUploader } from './uploader.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Create all services with their dependencies
|
|
@@ -20,36 +20,36 @@ import { TestRunner } from './test-runner.js';
|
|
|
20
20
|
* @returns {Object} Services object
|
|
21
21
|
*/
|
|
22
22
|
export function createServices(config, command = 'run') {
|
|
23
|
-
|
|
23
|
+
const apiService = new ApiService({
|
|
24
24
|
...config,
|
|
25
25
|
allowNoToken: true
|
|
26
26
|
});
|
|
27
|
-
|
|
27
|
+
const authService = new AuthService({
|
|
28
28
|
baseUrl: config.apiUrl
|
|
29
29
|
});
|
|
30
|
-
|
|
30
|
+
const configService = new ConfigService(config, {
|
|
31
31
|
projectRoot: process.cwd()
|
|
32
32
|
});
|
|
33
|
-
|
|
33
|
+
const projectService = new ProjectService(config, {
|
|
34
34
|
apiService,
|
|
35
35
|
authService
|
|
36
36
|
});
|
|
37
|
-
|
|
37
|
+
const uploader = createUploader({
|
|
38
38
|
...config,
|
|
39
39
|
command
|
|
40
40
|
});
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const buildManager = new BuildManager(config);
|
|
42
|
+
const tddService = createTDDService(config, {
|
|
43
43
|
authService
|
|
44
44
|
});
|
|
45
|
-
|
|
45
|
+
const serverManager = new ServerManager(config, {
|
|
46
46
|
services: {
|
|
47
47
|
configService,
|
|
48
48
|
authService,
|
|
49
49
|
projectService
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
-
|
|
52
|
+
const testRunner = new TestRunner(config, buildManager, serverManager, tddService);
|
|
53
53
|
return {
|
|
54
54
|
apiService,
|
|
55
55
|
authService,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
7
|
-
import {
|
|
7
|
+
import { deleteProjectMapping, getProjectMapping, getProjectMappings, saveProjectMapping } from '../utils/global-config.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* ProjectService for managing project mappings and operations
|
|
@@ -21,7 +21,7 @@ export class ProjectService {
|
|
|
21
21
|
* @returns {Promise<Array>} Array of project mappings
|
|
22
22
|
*/
|
|
23
23
|
async listMappings() {
|
|
24
|
-
|
|
24
|
+
const mappings = await getProjectMappings();
|
|
25
25
|
|
|
26
26
|
// Convert object to array with directory path included
|
|
27
27
|
return Object.entries(mappings).map(([directory, data]) => ({
|
|
@@ -89,7 +89,7 @@ export class ProjectService {
|
|
|
89
89
|
* @returns {Promise<Object>} Updated mapping
|
|
90
90
|
*/
|
|
91
91
|
async switchProject(projectSlug, organizationSlug, token) {
|
|
92
|
-
|
|
92
|
+
const currentDir = process.cwd();
|
|
93
93
|
return this.createMapping(currentDir, {
|
|
94
94
|
projectSlug,
|
|
95
95
|
organizationSlug,
|
|
@@ -107,19 +107,19 @@ export class ProjectService {
|
|
|
107
107
|
if (this.authService) {
|
|
108
108
|
try {
|
|
109
109
|
// First get the user's organizations via whoami
|
|
110
|
-
|
|
110
|
+
const whoami = await this.authService.authenticatedRequest('/api/auth/cli/whoami', {
|
|
111
111
|
method: 'GET'
|
|
112
112
|
});
|
|
113
|
-
|
|
113
|
+
const organizations = whoami.organizations || [];
|
|
114
114
|
if (organizations.length === 0) {
|
|
115
115
|
return [];
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Fetch projects for each organization
|
|
119
|
-
|
|
120
|
-
for (
|
|
119
|
+
const allProjects = [];
|
|
120
|
+
for (const org of organizations) {
|
|
121
121
|
try {
|
|
122
|
-
|
|
122
|
+
const response = await this.authService.authenticatedRequest('/api/project', {
|
|
123
123
|
method: 'GET',
|
|
124
124
|
headers: {
|
|
125
125
|
'X-Organization': org.slug
|
|
@@ -127,7 +127,7 @@ export class ProjectService {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
// Add organization info to each project
|
|
130
|
-
|
|
130
|
+
const projects = (response.projects || []).map(project => ({
|
|
131
131
|
...project,
|
|
132
132
|
organizationSlug: org.slug,
|
|
133
133
|
organizationName: org.name
|
|
@@ -146,7 +146,7 @@ export class ProjectService {
|
|
|
146
146
|
// Fall back to API token-based request (tokens are org-scoped, so no org header needed)
|
|
147
147
|
if (this.apiService) {
|
|
148
148
|
try {
|
|
149
|
-
|
|
149
|
+
const response = await this.apiService.request('/api/project', {
|
|
150
150
|
method: 'GET'
|
|
151
151
|
});
|
|
152
152
|
return response.projects || [];
|
|
@@ -169,7 +169,7 @@ export class ProjectService {
|
|
|
169
169
|
// Try OAuth-based request first
|
|
170
170
|
if (this.authService) {
|
|
171
171
|
try {
|
|
172
|
-
|
|
172
|
+
const response = await this.authService.authenticatedRequest(`/api/project/${projectSlug}`, {
|
|
173
173
|
method: 'GET',
|
|
174
174
|
headers: {
|
|
175
175
|
'X-Organization': organizationSlug
|
|
@@ -184,7 +184,7 @@ export class ProjectService {
|
|
|
184
184
|
// Fall back to API token
|
|
185
185
|
if (this.apiService) {
|
|
186
186
|
try {
|
|
187
|
-
|
|
187
|
+
const response = await this.apiService.request(`/api/project/${projectSlug}`, {
|
|
188
188
|
method: 'GET',
|
|
189
189
|
headers: {
|
|
190
190
|
'X-Organization': organizationSlug
|
|
@@ -211,16 +211,16 @@ export class ProjectService {
|
|
|
211
211
|
* @returns {Promise<Array>} Array of builds
|
|
212
212
|
*/
|
|
213
213
|
async getRecentBuilds(projectSlug, organizationSlug, options = {}) {
|
|
214
|
-
|
|
214
|
+
const queryParams = new globalThis.URLSearchParams();
|
|
215
215
|
if (options.limit) queryParams.append('limit', String(options.limit));
|
|
216
216
|
if (options.branch) queryParams.append('branch', options.branch);
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
const query = queryParams.toString();
|
|
218
|
+
const url = `/api/build/${projectSlug}${query ? `?${query}` : ''}`;
|
|
219
219
|
|
|
220
220
|
// Try OAuth-based request first (user login via device flow)
|
|
221
221
|
if (this.authService) {
|
|
222
222
|
try {
|
|
223
|
-
|
|
223
|
+
const response = await this.authService.authenticatedRequest(url, {
|
|
224
224
|
method: 'GET',
|
|
225
225
|
headers: {
|
|
226
226
|
'X-Organization': organizationSlug
|
|
@@ -235,7 +235,7 @@ export class ProjectService {
|
|
|
235
235
|
// Fall back to API token-based request
|
|
236
236
|
if (this.apiService) {
|
|
237
237
|
try {
|
|
238
|
-
|
|
238
|
+
const response = await this.apiService.request(url, {
|
|
239
239
|
method: 'GET',
|
|
240
240
|
headers: {
|
|
241
241
|
'X-Organization': organizationSlug
|
|
@@ -265,7 +265,7 @@ export class ProjectService {
|
|
|
265
265
|
throw new VizzlyError('API service not available', 'NO_API_SERVICE');
|
|
266
266
|
}
|
|
267
267
|
try {
|
|
268
|
-
|
|
268
|
+
const response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`, {
|
|
269
269
|
method: 'POST',
|
|
270
270
|
headers: {
|
|
271
271
|
'Content-Type': 'application/json'
|
|
@@ -291,7 +291,7 @@ export class ProjectService {
|
|
|
291
291
|
throw new VizzlyError('API service not available', 'NO_API_SERVICE');
|
|
292
292
|
}
|
|
293
293
|
try {
|
|
294
|
-
|
|
294
|
+
const response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`, {
|
|
295
295
|
method: 'GET'
|
|
296
296
|
});
|
|
297
297
|
return response.tokens || [];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
body {
|
|
8
8
|
font-family:
|
|
9
|
-
-apple-system, BlinkMacSystemFont,
|
|
9
|
+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
10
10
|
line-height: 1.6;
|
|
11
11
|
color: #e2e8f0;
|
|
12
12
|
background: #0f172a;
|
|
@@ -296,7 +296,7 @@ body {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
.onion-divider::before {
|
|
299
|
-
content:
|
|
299
|
+
content: "";
|
|
300
300
|
position: absolute;
|
|
301
301
|
top: 50%;
|
|
302
302
|
left: 50%;
|
|
@@ -309,7 +309,7 @@ body {
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
.onion-divider::after {
|
|
312
|
-
content:
|
|
312
|
+
content: "⟷";
|
|
313
313
|
position: absolute;
|
|
314
314
|
top: 50%;
|
|
315
315
|
left: 50%;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
document.addEventListener('DOMContentLoaded',
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
2
|
// Handle view mode switching
|
|
3
3
|
document.querySelectorAll('.view-mode-btn').forEach(btn => {
|
|
4
4
|
btn.addEventListener('click', function () {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const comparison = this.closest('.comparison');
|
|
6
|
+
const mode = this.dataset.mode;
|
|
7
7
|
|
|
8
8
|
// Update active button
|
|
9
|
-
comparison.querySelectorAll('.view-mode-btn')
|
|
9
|
+
for (let b of comparison.querySelectorAll('.view-mode-btn')) {
|
|
10
|
+
b.classList.remove('active');
|
|
11
|
+
}
|
|
10
12
|
this.classList.add('active');
|
|
11
13
|
|
|
12
14
|
// Update viewer mode
|
|
13
|
-
|
|
15
|
+
const viewer = comparison.querySelector('.comparison-viewer');
|
|
14
16
|
viewer.dataset.mode = mode;
|
|
15
17
|
|
|
16
18
|
// Hide all mode containers
|
|
@@ -19,7 +21,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
19
21
|
});
|
|
20
22
|
|
|
21
23
|
// Show appropriate mode container
|
|
22
|
-
|
|
24
|
+
const activeContainer = viewer.querySelector(`.${mode}-mode`);
|
|
23
25
|
if (activeContainer) {
|
|
24
26
|
activeContainer.style.display = 'block';
|
|
25
27
|
}
|
|
@@ -30,42 +32,42 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
30
32
|
document.querySelectorAll('.onion-container').forEach(container => {
|
|
31
33
|
let isDragging = false;
|
|
32
34
|
function updateOnionSkin(x) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const rect = container.getBoundingClientRect();
|
|
36
|
+
const percentage = Math.max(0, Math.min(100, (x - rect.left) / rect.width * 100));
|
|
37
|
+
const currentImg = container.querySelector('.onion-current');
|
|
38
|
+
const divider = container.querySelector('.onion-divider');
|
|
37
39
|
if (currentImg && divider) {
|
|
38
|
-
currentImg.style.clipPath =
|
|
39
|
-
divider.style.left = percentage
|
|
40
|
+
currentImg.style.clipPath = `inset(0 ${100 - percentage}% 0 0)`;
|
|
41
|
+
divider.style.left = `${percentage}%`;
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
|
-
container.addEventListener('mousedown',
|
|
44
|
+
container.addEventListener('mousedown', e => {
|
|
43
45
|
isDragging = true;
|
|
44
46
|
updateOnionSkin(e.clientX);
|
|
45
47
|
e.preventDefault();
|
|
46
48
|
});
|
|
47
|
-
container.addEventListener('mousemove',
|
|
49
|
+
container.addEventListener('mousemove', e => {
|
|
48
50
|
if (isDragging) {
|
|
49
51
|
updateOnionSkin(e.clientX);
|
|
50
52
|
}
|
|
51
53
|
});
|
|
52
|
-
document.addEventListener('mouseup',
|
|
54
|
+
document.addEventListener('mouseup', () => {
|
|
53
55
|
isDragging = false;
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
// Touch events for mobile
|
|
57
|
-
container.addEventListener('touchstart',
|
|
59
|
+
container.addEventListener('touchstart', e => {
|
|
58
60
|
isDragging = true;
|
|
59
61
|
updateOnionSkin(e.touches[0].clientX);
|
|
60
62
|
e.preventDefault();
|
|
61
63
|
});
|
|
62
|
-
container.addEventListener('touchmove',
|
|
64
|
+
container.addEventListener('touchmove', e => {
|
|
63
65
|
if (isDragging) {
|
|
64
66
|
updateOnionSkin(e.touches[0].clientX);
|
|
65
67
|
e.preventDefault();
|
|
66
68
|
}
|
|
67
69
|
});
|
|
68
|
-
document.addEventListener('touchend',
|
|
70
|
+
document.addEventListener('touchend', () => {
|
|
69
71
|
isDragging = false;
|
|
70
72
|
});
|
|
71
73
|
});
|
|
@@ -73,10 +75,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
73
75
|
// Handle overlay mode clicking
|
|
74
76
|
document.querySelectorAll('.overlay-container').forEach(container => {
|
|
75
77
|
container.addEventListener('click', function () {
|
|
76
|
-
|
|
78
|
+
const diffImage = this.querySelector('.diff-image');
|
|
77
79
|
if (diffImage) {
|
|
78
80
|
// Toggle diff visibility
|
|
79
|
-
|
|
81
|
+
const isVisible = diffImage.style.opacity === '1';
|
|
80
82
|
diffImage.style.opacity = isVisible ? '0' : '1';
|
|
81
83
|
}
|
|
82
84
|
});
|
|
@@ -85,9 +87,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
85
87
|
// Handle toggle mode clicking
|
|
86
88
|
document.querySelectorAll('.toggle-container img').forEach(img => {
|
|
87
89
|
let isBaseline = true;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
const comparison = img.closest('.comparison');
|
|
91
|
+
const baselineSrc = comparison.querySelector('.baseline-image').src;
|
|
92
|
+
const currentSrc = comparison.querySelector('.current-image').src;
|
|
91
93
|
img.addEventListener('click', function () {
|
|
92
94
|
isBaseline = !isBaseline;
|
|
93
95
|
this.src = isBaseline ? baselineSrc : currentSrc;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Listens for and processes screenshots from the test runner
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createServer } from 'http';
|
|
6
|
+
import { createServer } from 'node:http';
|
|
7
7
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
8
8
|
import * as output from '../utils/output.js';
|
|
9
9
|
export class ScreenshotServer {
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Manages the HTTP server with functional handlers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
8
|
import { createApiHandler } from '../server/handlers/api-handler.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { createTddHandler } from '../server/handlers/tdd-handler.js';
|
|
10
|
+
import { createHttpServer } from '../server/http-server.js';
|
|
11
11
|
export class ServerManager {
|
|
12
12
|
constructor(config, options = {}) {
|
|
13
13
|
this.config = config;
|
|
@@ -88,7 +88,7 @@ export class ServerManager {
|
|
|
88
88
|
|
|
89
89
|
// Clean up server.json so the client SDK doesn't try to connect to a dead server
|
|
90
90
|
try {
|
|
91
|
-
|
|
91
|
+
const serverFile = join(process.cwd(), '.vizzly', 'server.json');
|
|
92
92
|
if (existsSync(serverFile)) {
|
|
93
93
|
unlinkSync(serverFile);
|
|
94
94
|
}
|