@vizzly-testing/cli 0.29.6 → 0.31.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 +34 -6
- package/dist/api/client.js +6 -3
- package/dist/api/endpoints.js +59 -0
- package/dist/api/index.js +1 -1
- package/dist/auth/core.js +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/cli.js +206 -51
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +870 -0
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +18 -7
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +64 -26
- package/dist/config/core.js +37 -36
- package/dist/context/local-workspace-provider.js +532 -0
- package/dist/context/provider-resolver.js +36 -0
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +2 -1
- package/dist/reporter/reporter-bundle.iife.js +119 -119
- package/dist/reporter-ssr/ssr-entry.js +605 -509
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/assets.js +4 -0
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +226 -148
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/core/hotspot-coverage.js +1 -1
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/hotspot-service.js +1 -14
- package/dist/tdd/tdd-service.js +82 -113
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +47 -7
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/colors.js +46 -44
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +22 -3
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/report-data.js +31 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/dist/utils/security.js +3 -2
- package/package.json +14 -11
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
|
@@ -12,30 +12,10 @@ import { existsSync } from 'node:fs';
|
|
|
12
12
|
import { writeFile } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
15
|
+
import { CONFIG_DEFAULTS } from '../config/core.js';
|
|
15
16
|
import { loadGlobalConfig, saveGlobalConfig } from '../utils/global-config.js';
|
|
16
17
|
import * as output from '../utils/output.js';
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
* Default configuration values
|
|
20
|
-
*/
|
|
21
|
-
let DEFAULT_CONFIG = {
|
|
22
|
-
comparison: {
|
|
23
|
-
threshold: 2.0,
|
|
24
|
-
minClusterSize: 2
|
|
25
|
-
},
|
|
26
|
-
server: {
|
|
27
|
-
port: 47392,
|
|
28
|
-
timeout: 30000
|
|
29
|
-
},
|
|
30
|
-
build: {
|
|
31
|
-
name: 'Build {timestamp}',
|
|
32
|
-
environment: 'test'
|
|
33
|
-
},
|
|
34
|
-
tdd: {
|
|
35
|
-
openReport: false
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
19
|
/**
|
|
40
20
|
* Create a config service instance
|
|
41
21
|
* @param {Object} options
|
|
@@ -76,9 +56,7 @@ export function createConfigService({
|
|
|
76
56
|
* Get merged configuration with source tracking
|
|
77
57
|
*/
|
|
78
58
|
async function getMergedConfig() {
|
|
79
|
-
let config =
|
|
80
|
-
...DEFAULT_CONFIG
|
|
81
|
-
};
|
|
59
|
+
let config = cloneConfigValue(CONFIG_DEFAULTS);
|
|
82
60
|
let sources = {};
|
|
83
61
|
|
|
84
62
|
// Layer 1: Global config
|
|
@@ -100,15 +78,15 @@ export function createConfigService({
|
|
|
100
78
|
|
|
101
79
|
// Layer 3: Environment variables
|
|
102
80
|
if (process.env.VIZZLY_THRESHOLD) {
|
|
103
|
-
config.comparison.threshold =
|
|
81
|
+
config.comparison.threshold = Number(process.env.VIZZLY_THRESHOLD);
|
|
104
82
|
sources.comparison = 'env';
|
|
105
83
|
}
|
|
106
84
|
if (process.env.VIZZLY_MIN_CLUSTER_SIZE) {
|
|
107
|
-
config.comparison.minClusterSize =
|
|
85
|
+
config.comparison.minClusterSize = Number(process.env.VIZZLY_MIN_CLUSTER_SIZE);
|
|
108
86
|
sources.comparison = 'env';
|
|
109
87
|
}
|
|
110
88
|
if (process.env.VIZZLY_PORT) {
|
|
111
|
-
config.server.port =
|
|
89
|
+
config.server.port = Number(process.env.VIZZLY_PORT);
|
|
112
90
|
sources.server = 'env';
|
|
113
91
|
}
|
|
114
92
|
return {
|
|
@@ -241,7 +219,7 @@ export default defineConfig(${JSON.stringify(newConfig, null, 2)});
|
|
|
241
219
|
// Validate threshold
|
|
242
220
|
if (config.comparison?.threshold !== undefined) {
|
|
243
221
|
let threshold = config.comparison.threshold;
|
|
244
|
-
if (typeof threshold !== 'number' || threshold < 0) {
|
|
222
|
+
if (typeof threshold !== 'number' || !Number.isFinite(threshold) || threshold < 0) {
|
|
245
223
|
errors.push('comparison.threshold must be a non-negative number');
|
|
246
224
|
} else if (threshold > 100) {
|
|
247
225
|
warnings.push('comparison.threshold above 100 may cause all comparisons to pass');
|
|
@@ -292,30 +270,44 @@ export default defineConfig(${JSON.stringify(newConfig, null, 2)});
|
|
|
292
270
|
* Deep merge two objects
|
|
293
271
|
*/
|
|
294
272
|
function mergeDeep(target, source) {
|
|
295
|
-
let result =
|
|
296
|
-
...target
|
|
297
|
-
};
|
|
273
|
+
let result = cloneConfigValue(target);
|
|
298
274
|
for (let key in source) {
|
|
299
275
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
300
276
|
result[key] = mergeDeep(result[key] || {}, source[key]);
|
|
277
|
+
} else if (Array.isArray(source[key])) {
|
|
278
|
+
result[key] = [...source[key]];
|
|
301
279
|
} else {
|
|
302
280
|
result[key] = source[key];
|
|
303
281
|
}
|
|
304
282
|
}
|
|
305
283
|
return result;
|
|
306
284
|
}
|
|
285
|
+
function cloneConfigValue(value) {
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
return value.map(cloneConfigValue);
|
|
288
|
+
}
|
|
289
|
+
if (value && typeof value === 'object') {
|
|
290
|
+
let clone = {};
|
|
291
|
+
for (let key in value) {
|
|
292
|
+
clone[key] = cloneConfigValue(value[key]);
|
|
293
|
+
}
|
|
294
|
+
return clone;
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
307
298
|
|
|
308
299
|
/**
|
|
309
300
|
* Merge config with source tracking
|
|
310
301
|
*/
|
|
311
|
-
function mergeWithTracking(target, source, sources, sourceName) {
|
|
302
|
+
function mergeWithTracking(target, source, sources, sourceName, sectionName) {
|
|
312
303
|
for (let key in source) {
|
|
304
|
+
let sourceSection = sectionName || key;
|
|
313
305
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
314
306
|
if (!target[key]) target[key] = {};
|
|
315
|
-
mergeWithTracking(target[key], source[key], sources, sourceName);
|
|
307
|
+
mergeWithTracking(target[key], source[key], sources, sourceName, sourceSection);
|
|
316
308
|
} else {
|
|
317
309
|
target[key] = source[key];
|
|
318
|
-
sources[
|
|
310
|
+
sources[sourceSection] = sourceName;
|
|
319
311
|
}
|
|
320
312
|
}
|
|
321
313
|
}
|
package/dist/services/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* This factory is only used by cli.js to provide services to plugins.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { createServerManager } from '../server-manager/index.js';
|
|
13
13
|
import { TestRunner } from './test-runner.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -26,9 +26,7 @@ import { TestRunner } from './test-runner.js';
|
|
|
26
26
|
* @returns {Object} Services object for plugins
|
|
27
27
|
*/
|
|
28
28
|
export function createServices(config) {
|
|
29
|
-
let serverManager =
|
|
30
|
-
services: {}
|
|
31
|
-
});
|
|
29
|
+
let serverManager = createServerManager(config, {});
|
|
32
30
|
let testRunner = new TestRunner(config, serverManager);
|
|
33
31
|
return {
|
|
34
32
|
serverManager,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
11
|
let __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -119,6 +119,13 @@ ${css}
|
|
|
119
119
|
</body>
|
|
120
120
|
</html>`;
|
|
121
121
|
}
|
|
122
|
+
export async function loadStaticReportRenderer(ssrModulePath = getSSRModulePath()) {
|
|
123
|
+
let moduleUrl = pathToFileURL(ssrModulePath).href;
|
|
124
|
+
let {
|
|
125
|
+
renderStaticReport
|
|
126
|
+
} = await import(moduleUrl);
|
|
127
|
+
return renderStaticReport;
|
|
128
|
+
}
|
|
122
129
|
|
|
123
130
|
/**
|
|
124
131
|
* Generate a static report from the current TDD results
|
|
@@ -155,17 +162,9 @@ export async function generateStaticReport(workingDir, options = {}) {
|
|
|
155
162
|
|
|
156
163
|
// Transform image URLs to relative paths
|
|
157
164
|
let transformedData = transformImageUrls(reportData);
|
|
158
|
-
|
|
159
|
-
// Load and use the SSR module
|
|
160
|
-
let ssrModulePath = getSSRModulePath();
|
|
161
|
-
let {
|
|
162
|
-
renderStaticReport
|
|
163
|
-
} = await import(ssrModulePath);
|
|
165
|
+
let renderStaticReport = options.renderStaticReport || (await loadStaticReportRenderer());
|
|
164
166
|
let renderedContent = renderStaticReport(transformedData);
|
|
165
|
-
|
|
166
|
-
// Get CSS
|
|
167
|
-
let reporterDistPath = getReporterDistPath();
|
|
168
|
-
let css = readFileSync(join(reporterDistPath, 'reporter-bundle.css'), 'utf8');
|
|
167
|
+
let css = options.css ?? readFileSync(join(getReporterDistPath(), 'reporter-bundle.css'), 'utf8');
|
|
169
168
|
|
|
170
169
|
// Create output directory
|
|
171
170
|
mkdirSync(outputDir, {
|
|
@@ -201,5 +200,5 @@ export async function generateStaticReport(workingDir, options = {}) {
|
|
|
201
200
|
* Get the file:// URL for a report path
|
|
202
201
|
*/
|
|
203
202
|
export function getReportFileUrl(reportPath) {
|
|
204
|
-
return
|
|
203
|
+
return pathToFileURL(reportPath).href;
|
|
205
204
|
}
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
* Test Runner Service
|
|
3
3
|
* Orchestrates the test execution flow
|
|
4
4
|
*
|
|
5
|
-
* This
|
|
6
|
-
* src/test-runner/.
|
|
7
|
-
* delegating to pure functions for testability.
|
|
5
|
+
* This EventEmitter adapter keeps the stable plugin-facing runner contract
|
|
6
|
+
* while delegating execution to the functional operations in src/test-runner/.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { spawn } from 'node:child_process';
|
|
11
10
|
import { EventEmitter } from 'node:events';
|
|
12
11
|
import { createBuild as createApiBuild, createApiClient, finalizeBuild as finalizeApiBuild, getBuild } from '../api/index.js';
|
|
13
12
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
14
|
-
import { cancelTests, createBuild, finalizeBuild, initializeDaemon, runTests } from '../test-runner/index.js';
|
|
13
|
+
import { cancelTests, createBuild, fetchBuildUrl, finalizeBuild, initializeDaemon, runTests } from '../test-runner/index.js';
|
|
15
14
|
import * as output from '../utils/output.js';
|
|
16
15
|
import { writeSession as defaultWriteSession } from '../utils/session.js';
|
|
17
16
|
import { createBuildObject } from './build-manager.js';
|
|
@@ -81,6 +80,7 @@ export class TestRunner extends EventEmitter {
|
|
|
81
80
|
return result;
|
|
82
81
|
}
|
|
83
82
|
async createBuild(options, tdd) {
|
|
83
|
+
let createdApiBuild = null;
|
|
84
84
|
let buildId = await createBuild({
|
|
85
85
|
runOptions: options,
|
|
86
86
|
tdd,
|
|
@@ -88,11 +88,23 @@ export class TestRunner extends EventEmitter {
|
|
|
88
88
|
deps: {
|
|
89
89
|
buildManager: this.buildManager,
|
|
90
90
|
createApiClient: this.deps.createApiClient,
|
|
91
|
-
createApiBuild:
|
|
91
|
+
createApiBuild: async (client, payload) => {
|
|
92
|
+
createdApiBuild = await this.deps.createApiBuild(client, payload);
|
|
93
|
+
return createdApiBuild;
|
|
94
|
+
},
|
|
92
95
|
output: this.deps.output
|
|
93
96
|
}
|
|
94
97
|
});
|
|
95
98
|
if (!tdd && buildId) {
|
|
99
|
+
let buildUrl = createdApiBuild?.url || (await fetchBuildUrl({
|
|
100
|
+
buildId,
|
|
101
|
+
config: this.config,
|
|
102
|
+
deps: {
|
|
103
|
+
createApiClient: this.deps.createApiClient,
|
|
104
|
+
getBuild: this.deps.getBuild,
|
|
105
|
+
output: this.deps.output
|
|
106
|
+
}
|
|
107
|
+
}));
|
|
96
108
|
let writeSession = this.deps.writeSession || defaultWriteSession;
|
|
97
109
|
writeSession({
|
|
98
110
|
buildId,
|
|
@@ -100,6 +112,10 @@ export class TestRunner extends EventEmitter {
|
|
|
100
112
|
commit: options?.commit,
|
|
101
113
|
parallelId: options?.parallelId
|
|
102
114
|
});
|
|
115
|
+
this.emit('build-created', {
|
|
116
|
+
buildId,
|
|
117
|
+
url: buildUrl
|
|
118
|
+
});
|
|
103
119
|
}
|
|
104
120
|
return buildId;
|
|
105
121
|
}
|
|
@@ -119,28 +135,6 @@ export class TestRunner extends EventEmitter {
|
|
|
119
135
|
}
|
|
120
136
|
});
|
|
121
137
|
}
|
|
122
|
-
async executeTestCommand(testCommand, env) {
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
let proc = this.deps.spawn(testCommand, {
|
|
125
|
-
env,
|
|
126
|
-
stdio: 'inherit',
|
|
127
|
-
shell: true
|
|
128
|
-
});
|
|
129
|
-
this.testProcess = proc;
|
|
130
|
-
proc.on('error', error => {
|
|
131
|
-
reject(this.deps.createError(`Failed to run test command: ${error.message}`, 'TEST_COMMAND_FAILED'));
|
|
132
|
-
});
|
|
133
|
-
proc.on('exit', (code, signal) => {
|
|
134
|
-
if (signal === 'SIGINT') {
|
|
135
|
-
reject(this.deps.createError('Test command was interrupted', 'TEST_COMMAND_INTERRUPTED'));
|
|
136
|
-
} else if (code !== 0) {
|
|
137
|
-
reject(this.deps.createError(`Test command exited with code ${code}`, 'TEST_COMMAND_FAILED'));
|
|
138
|
-
} else {
|
|
139
|
-
resolve();
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
138
|
async cancel() {
|
|
145
139
|
await cancelTests({
|
|
146
140
|
testProcess: this.testProcess,
|
|
@@ -23,7 +23,7 @@ export function calculateHotspotCoverage(diffClusters, hotspotAnalysis) {
|
|
|
23
23
|
totalLines: 0
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
if (!hotspotAnalysis
|
|
26
|
+
if (!hotspotAnalysis?.regions || hotspotAnalysis.regions.length === 0) {
|
|
27
27
|
return {
|
|
28
28
|
coverage: 0,
|
|
29
29
|
linesInHotspots: 0,
|
package/dist/tdd/index.js
CHANGED
|
@@ -15,5 +15,5 @@ export { baselineMatchesSha, buildBaselineMetadataEntry, downloadBaselineImage,
|
|
|
15
15
|
// Services
|
|
16
16
|
export { baselineExists, clearBaselineData, getBaselinePath, getCurrentPath, getDiffPath, initializeDirectories, promoteCurrentToBaseline, readBaseline, readCurrent, saveBaseline, saveCurrent } from './services/baseline-manager.js';
|
|
17
17
|
export { buildErrorComparison, buildFailedComparison, buildNewComparison, buildPassedComparison, compareImages, isDimensionMismatchError } from './services/comparison-service.js';
|
|
18
|
-
export { downloadHotspots
|
|
18
|
+
export { downloadHotspots } from './services/hotspot-service.js';
|
|
19
19
|
export { buildResults, calculateSummary, findComparison, findComparisonById, getErrorComparisons, getFailedComparisons, getNewComparisons, isSuccessful } from './services/result-service.js';
|
|
@@ -53,41 +53,4 @@ export function saveRegionMetadata(workingDir, regionData, summary = {}) {
|
|
|
53
53
|
regions: regionData
|
|
54
54
|
};
|
|
55
55
|
writeFileSync(regionsPath, JSON.stringify(content, null, 2));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Get regions for a specific screenshot with caching support
|
|
60
|
-
*
|
|
61
|
-
* This is a pure function that takes a cache object as parameter
|
|
62
|
-
* for stateless operation. The cache is mutated if data needs to be loaded.
|
|
63
|
-
*
|
|
64
|
-
* @param {Object} cache - Cache object { data: Object|null, loaded: boolean }
|
|
65
|
-
* @param {string} workingDir - Working directory
|
|
66
|
-
* @param {string} screenshotName - Name of the screenshot
|
|
67
|
-
* @returns {Object|null} Region data or null if not available
|
|
68
|
-
*/
|
|
69
|
-
export function getRegionsForScreenshot(cache, workingDir, screenshotName) {
|
|
70
|
-
// Check cache first
|
|
71
|
-
if (cache.data?.[screenshotName]) {
|
|
72
|
-
return cache.data[screenshotName];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Load from disk if not yet loaded
|
|
76
|
-
if (!cache.loaded) {
|
|
77
|
-
cache.data = loadRegionMetadata(workingDir);
|
|
78
|
-
cache.loaded = true;
|
|
79
|
-
}
|
|
80
|
-
return cache.data?.[screenshotName] || null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Create an empty region cache object
|
|
85
|
-
*
|
|
86
|
-
* @returns {{ data: null, loaded: boolean }}
|
|
87
|
-
*/
|
|
88
|
-
export function createRegionCache() {
|
|
89
|
-
return {
|
|
90
|
-
data: null,
|
|
91
|
-
loaded: false
|
|
92
|
-
};
|
|
93
56
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime context assembly for TddService constructor.
|
|
3
|
+
* Keeps dependency merging and path bootstrap logic isolated and testable.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function buildTddDependencyOps(deps = {}, defaults = {}) {
|
|
7
|
+
let {
|
|
8
|
+
output = defaults.output,
|
|
9
|
+
colors = defaults.colors,
|
|
10
|
+
validatePathSecurity = defaults.validatePathSecurity,
|
|
11
|
+
initializeDirectories = defaults.initializeDirectories,
|
|
12
|
+
calculateHotspotCoverage = defaults.calculateHotspotCoverage,
|
|
13
|
+
fs = {},
|
|
14
|
+
api = {},
|
|
15
|
+
metadata = {},
|
|
16
|
+
baseline = {},
|
|
17
|
+
comparison = {},
|
|
18
|
+
signature = {},
|
|
19
|
+
results = {}
|
|
20
|
+
} = deps;
|
|
21
|
+
let fsOps = {
|
|
22
|
+
...defaults.fs,
|
|
23
|
+
...fs
|
|
24
|
+
};
|
|
25
|
+
let apiOps = {
|
|
26
|
+
...defaults.api,
|
|
27
|
+
...api
|
|
28
|
+
};
|
|
29
|
+
let metadataOps = {
|
|
30
|
+
...defaults.metadata,
|
|
31
|
+
...metadata
|
|
32
|
+
};
|
|
33
|
+
let baselineOps = {
|
|
34
|
+
...defaults.baseline,
|
|
35
|
+
...baseline
|
|
36
|
+
};
|
|
37
|
+
let comparisonOps = {
|
|
38
|
+
...defaults.comparison,
|
|
39
|
+
...comparison
|
|
40
|
+
};
|
|
41
|
+
let signatureOps = {
|
|
42
|
+
...defaults.signature,
|
|
43
|
+
...signature
|
|
44
|
+
};
|
|
45
|
+
let resultsOps = {
|
|
46
|
+
...defaults.results,
|
|
47
|
+
...results
|
|
48
|
+
};
|
|
49
|
+
let runtimeDeps = {
|
|
50
|
+
output,
|
|
51
|
+
colors,
|
|
52
|
+
validatePathSecurity,
|
|
53
|
+
initializeDirectories,
|
|
54
|
+
calculateHotspotCoverage,
|
|
55
|
+
...fsOps,
|
|
56
|
+
...apiOps,
|
|
57
|
+
...metadataOps,
|
|
58
|
+
...baselineOps,
|
|
59
|
+
...comparisonOps,
|
|
60
|
+
...signatureOps,
|
|
61
|
+
...resultsOps
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
runtimeDeps,
|
|
65
|
+
apiOps
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function resolveTddWorkingDirectory(workingDir, validatePathSecurity, output) {
|
|
69
|
+
try {
|
|
70
|
+
return validatePathSecurity(workingDir, workingDir);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
output.error(`Invalid working directory: ${error.message}`);
|
|
73
|
+
throw new Error(`Working directory validation failed: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function resolveTddPaths(workingDir, initializeDirectories) {
|
|
77
|
+
return initializeDirectories(workingDir);
|
|
78
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { createServer } from 'node:net';
|
|
@@ -10,9 +10,13 @@ import { join } from 'node:path';
|
|
|
10
10
|
* Enables the menubar app to discover and manage multiple concurrent servers.
|
|
11
11
|
*/
|
|
12
12
|
export class ServerRegistry {
|
|
13
|
-
constructor(
|
|
14
|
-
|
|
13
|
+
constructor({
|
|
14
|
+
vizzlyHome,
|
|
15
|
+
logger = console
|
|
16
|
+
} = {}) {
|
|
17
|
+
this.vizzlyHome = vizzlyHome || process.env.VIZZLY_HOME || join(homedir(), '.vizzly');
|
|
15
18
|
this.registryPath = join(this.vizzlyHome, 'servers.json');
|
|
19
|
+
this.logger = logger;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -40,7 +44,7 @@ export class ServerRegistry {
|
|
|
40
44
|
}
|
|
41
45
|
} catch (_err) {
|
|
42
46
|
// Corrupted file, start fresh
|
|
43
|
-
|
|
47
|
+
this.logger.warn('Warning: Could not read server registry, starting fresh');
|
|
44
48
|
}
|
|
45
49
|
return {
|
|
46
50
|
version: 1,
|
|
@@ -64,10 +68,10 @@ export class ServerRegistry {
|
|
|
64
68
|
if (!serverInfo.pid || !serverInfo.port || !serverInfo.directory) {
|
|
65
69
|
throw new Error('Missing required fields: pid, port, directory');
|
|
66
70
|
}
|
|
67
|
-
let port =
|
|
68
|
-
let pid =
|
|
69
|
-
if (
|
|
70
|
-
throw new Error('Invalid port or pid - must be
|
|
71
|
+
let port = parsePositiveInteger(serverInfo.port);
|
|
72
|
+
let pid = parsePositiveInteger(serverInfo.pid);
|
|
73
|
+
if (port === null || pid === null) {
|
|
74
|
+
throw new Error('Invalid port or pid - must be positive integers');
|
|
71
75
|
}
|
|
72
76
|
let registry = this.read();
|
|
73
77
|
|
|
@@ -171,15 +175,7 @@ export class ServerRegistry {
|
|
|
171
175
|
* The menubar app listens for this in addition to file watching.
|
|
172
176
|
*/
|
|
173
177
|
notifyMenubar() {
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
execSync('notifyutil -p dev.vizzly.serverChanged', {
|
|
177
|
-
stdio: 'ignore',
|
|
178
|
-
timeout: 500
|
|
179
|
-
});
|
|
180
|
-
} catch {
|
|
181
|
-
// Non-fatal - menubar will still see changes via file watching
|
|
182
|
-
}
|
|
178
|
+
notifyServerRegistryChanged();
|
|
183
179
|
}
|
|
184
180
|
|
|
185
181
|
/**
|
|
@@ -218,6 +214,31 @@ export class ServerRegistry {
|
|
|
218
214
|
return startPort;
|
|
219
215
|
}
|
|
220
216
|
}
|
|
217
|
+
function parsePositiveInteger(value) {
|
|
218
|
+
if (typeof value === 'number') {
|
|
219
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
220
|
+
}
|
|
221
|
+
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) {
|
|
222
|
+
return Number(value);
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
export function notifyServerRegistryChanged({
|
|
227
|
+
platform = process.platform,
|
|
228
|
+
execFile = execFileSync
|
|
229
|
+
} = {}) {
|
|
230
|
+
if (platform !== 'darwin') return false;
|
|
231
|
+
try {
|
|
232
|
+
execFile('notifyutil', ['-p', 'dev.vizzly.serverChanged'], {
|
|
233
|
+
stdio: 'ignore',
|
|
234
|
+
timeout: 500
|
|
235
|
+
});
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
// Non-fatal - menubar will still see changes via file watching
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
221
242
|
|
|
222
243
|
/**
|
|
223
244
|
* Check if a port is free (not in use by any process)
|
|
@@ -57,6 +57,29 @@ export function clearBaselineData(paths) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Clear per-run screenshot artifacts while preserving approved baselines.
|
|
62
|
+
*
|
|
63
|
+
* @param {{ currentPath: string, diffPath: string }} paths
|
|
64
|
+
*/
|
|
65
|
+
export function clearRunData(paths) {
|
|
66
|
+
let {
|
|
67
|
+
currentPath,
|
|
68
|
+
diffPath
|
|
69
|
+
} = paths;
|
|
70
|
+
for (let dir of [currentPath, diffPath]) {
|
|
71
|
+
if (existsSync(dir)) {
|
|
72
|
+
rmSync(dir, {
|
|
73
|
+
recursive: true,
|
|
74
|
+
force: true
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
mkdirSync(dir, {
|
|
78
|
+
recursive: true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
/**
|
|
61
84
|
* Save an image as baseline
|
|
62
85
|
*
|
|
@@ -31,7 +31,7 @@ export async function downloadHotspots(options) {
|
|
|
31
31
|
}
|
|
32
32
|
try {
|
|
33
33
|
let response = await api.getHotspots(screenshotNames);
|
|
34
|
-
if (!response
|
|
34
|
+
if (!response?.hotspots) {
|
|
35
35
|
return {
|
|
36
36
|
success: false,
|
|
37
37
|
error: 'API returned no hotspot data'
|
|
@@ -55,17 +55,4 @@ export async function downloadHotspots(options) {
|
|
|
55
55
|
error: error.message
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Extract screenshot names from a list of screenshots
|
|
62
|
-
*
|
|
63
|
-
* @param {Array} screenshots - Screenshots with name property
|
|
64
|
-
* @returns {string[]}
|
|
65
|
-
*/
|
|
66
|
-
export function extractScreenshotNames(screenshots) {
|
|
67
|
-
if (!screenshots || !Array.isArray(screenshots)) {
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
return screenshots.map(s => s.name).filter(Boolean);
|
|
71
58
|
}
|