@vizzly-testing/cli 0.19.2 → 0.20.1-beta.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/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +1 -1
- package/dist/client/index.js +0 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/finalize.js +41 -15
- package/dist/commands/login.js +7 -6
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +5 -4
- package/dist/commands/run.js +158 -90
- package/dist/commands/status.js +22 -18
- package/dist/commands/tdd.js +105 -78
- package/dist/commands/upload.js +61 -26
- package/dist/commands/whoami.js +4 -4
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/report-generator/core.js +315 -0
- package/dist/report-generator/index.js +8 -0
- package/dist/report-generator/operations.js +196 -0
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +80 -48
- package/dist/server-manager/core.js +183 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +208 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/static-report-generator.js +21 -163
- package/dist/services/test-runner.js +90 -249
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1081 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +4 -2
- package/dist/types/index.d.ts +5 -0
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/config-schema.js +8 -3
- package/package.json +7 -12
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/tdd-service.js +0 -1429
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot Server Core - Pure functions for screenshot server logic
|
|
3
|
+
*
|
|
4
|
+
* No I/O, no side effects - just data transformations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Request Validation
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate screenshot request body
|
|
13
|
+
* @param {Object} body - Request body
|
|
14
|
+
* @returns {{ valid: boolean, error: string|null }}
|
|
15
|
+
*/
|
|
16
|
+
export function validateScreenshotRequest(body) {
|
|
17
|
+
if (!body?.name || !body?.image) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
error: 'name and image are required'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
valid: true,
|
|
25
|
+
error: null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if request is for the screenshot endpoint
|
|
31
|
+
* @param {string} method - HTTP method
|
|
32
|
+
* @param {string} url - Request URL
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
export function isScreenshotEndpoint(method, url) {
|
|
36
|
+
return method === 'POST' && url === '/screenshot';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Build ID Handling
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get effective build ID (falls back to 'default')
|
|
45
|
+
* @param {string|null|undefined} buildId - Build ID from request
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function getEffectiveBuildId(buildId) {
|
|
49
|
+
return buildId || 'default';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Response Building
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build success response object
|
|
58
|
+
* @returns {{ status: number, body: Object }}
|
|
59
|
+
*/
|
|
60
|
+
export function buildSuccessResponse() {
|
|
61
|
+
return {
|
|
62
|
+
status: 200,
|
|
63
|
+
body: {
|
|
64
|
+
success: true
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build error response object
|
|
71
|
+
* @param {number} status - HTTP status code
|
|
72
|
+
* @param {string} message - Error message
|
|
73
|
+
* @returns {{ status: number, body: Object }}
|
|
74
|
+
*/
|
|
75
|
+
export function buildErrorResponse(status, message) {
|
|
76
|
+
return {
|
|
77
|
+
status,
|
|
78
|
+
body: {
|
|
79
|
+
error: message
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build not found response
|
|
86
|
+
* @returns {{ status: number, body: Object }}
|
|
87
|
+
*/
|
|
88
|
+
export function buildNotFoundResponse() {
|
|
89
|
+
return buildErrorResponse(404, 'Not found');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build bad request response
|
|
94
|
+
* @param {string} message - Error message
|
|
95
|
+
* @returns {{ status: number, body: Object }}
|
|
96
|
+
*/
|
|
97
|
+
export function buildBadRequestResponse(message) {
|
|
98
|
+
return buildErrorResponse(400, message);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build internal error response
|
|
103
|
+
* @returns {{ status: number, body: Object }}
|
|
104
|
+
*/
|
|
105
|
+
export function buildInternalErrorResponse() {
|
|
106
|
+
return buildErrorResponse(500, 'Internal server error');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Server Configuration
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build server listen options
|
|
115
|
+
* @param {Object} config - Server configuration
|
|
116
|
+
* @returns {{ port: number, host: string }}
|
|
117
|
+
*/
|
|
118
|
+
export function buildServerListenOptions(config) {
|
|
119
|
+
return {
|
|
120
|
+
port: config?.server?.port || 3000,
|
|
121
|
+
host: '127.0.0.1'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build server started message
|
|
127
|
+
* @param {number} port - Server port
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
export function buildServerStartedMessage(port) {
|
|
131
|
+
return `Screenshot server listening on http://127.0.0.1:${port}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build server stopped message
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
export function buildServerStoppedMessage() {
|
|
139
|
+
return 'Screenshot server stopped';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Screenshot Data Extraction
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract screenshot data from request body
|
|
148
|
+
* @param {Object} body - Request body
|
|
149
|
+
* @returns {{ name: string, image: string, properties?: Object }}
|
|
150
|
+
*/
|
|
151
|
+
export function extractScreenshotData(body) {
|
|
152
|
+
return {
|
|
153
|
+
name: body.name,
|
|
154
|
+
image: body.image,
|
|
155
|
+
properties: body.properties
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot Server Module
|
|
3
|
+
*
|
|
4
|
+
* Exports pure functions (core) and I/O operations for screenshot server functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Core - pure functions
|
|
8
|
+
export { buildBadRequestResponse, buildErrorResponse, buildInternalErrorResponse, buildNotFoundResponse, buildServerListenOptions, buildServerStartedMessage, buildServerStoppedMessage, buildSuccessResponse, extractScreenshotData, getEffectiveBuildId, isScreenshotEndpoint, validateScreenshotRequest } from './core.js';
|
|
9
|
+
|
|
10
|
+
// Operations - I/O with dependency injection
|
|
11
|
+
export { handleRequest, parseRequestBody, startServer, stopServer } from './operations.js';
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot Server Operations - I/O operations with dependency injection
|
|
3
|
+
*
|
|
4
|
+
* Each operation takes its dependencies as parameters for testability.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { buildBadRequestResponse, buildInternalErrorResponse, buildNotFoundResponse, buildServerListenOptions, buildServerStartedMessage, buildServerStoppedMessage, buildSuccessResponse, extractScreenshotData, getEffectiveBuildId, isScreenshotEndpoint, validateScreenshotRequest } from './core.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Request Body Parsing
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse request body as JSON
|
|
15
|
+
* @param {Object} options - Options
|
|
16
|
+
* @param {Object} options.req - HTTP request
|
|
17
|
+
* @param {Object} options.deps - Dependencies
|
|
18
|
+
* @param {Function} options.deps.createError - Error factory
|
|
19
|
+
* @returns {Promise<Object>} Parsed body
|
|
20
|
+
*/
|
|
21
|
+
export function parseRequestBody({
|
|
22
|
+
req,
|
|
23
|
+
deps
|
|
24
|
+
}) {
|
|
25
|
+
let {
|
|
26
|
+
createError
|
|
27
|
+
} = deps;
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let body = '';
|
|
30
|
+
req.on('data', chunk => {
|
|
31
|
+
body += chunk.toString();
|
|
32
|
+
});
|
|
33
|
+
req.on('end', () => {
|
|
34
|
+
try {
|
|
35
|
+
resolve(JSON.parse(body));
|
|
36
|
+
} catch {
|
|
37
|
+
reject(createError('Invalid JSON in request body', 'INVALID_JSON'));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
req.on('error', error => {
|
|
41
|
+
reject(createError(`Request error: ${error.message}`, 'REQUEST_ERROR'));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Request Handling
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handle incoming HTTP request
|
|
52
|
+
* @param {Object} options - Options
|
|
53
|
+
* @param {Object} options.req - HTTP request
|
|
54
|
+
* @param {Object} options.res - HTTP response
|
|
55
|
+
* @param {Object} options.deps - Dependencies
|
|
56
|
+
* @param {Object} options.deps.buildManager - Build manager for screenshot storage
|
|
57
|
+
* @param {Function} options.deps.createError - Error factory
|
|
58
|
+
* @param {Object} options.deps.output - Output utilities
|
|
59
|
+
*/
|
|
60
|
+
export async function handleRequest({
|
|
61
|
+
req,
|
|
62
|
+
res,
|
|
63
|
+
deps
|
|
64
|
+
}) {
|
|
65
|
+
let {
|
|
66
|
+
buildManager,
|
|
67
|
+
createError,
|
|
68
|
+
output
|
|
69
|
+
} = deps;
|
|
70
|
+
|
|
71
|
+
// Check if this is a screenshot endpoint
|
|
72
|
+
if (!isScreenshotEndpoint(req.method, req.url)) {
|
|
73
|
+
let response = buildNotFoundResponse();
|
|
74
|
+
sendResponse(res, response);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
// Parse request body
|
|
79
|
+
let body = await parseRequestBody({
|
|
80
|
+
req,
|
|
81
|
+
deps: {
|
|
82
|
+
createError
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Validate required fields
|
|
87
|
+
let validation = validateScreenshotRequest(body);
|
|
88
|
+
if (!validation.valid) {
|
|
89
|
+
let response = buildBadRequestResponse(validation.error);
|
|
90
|
+
sendResponse(res, response);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Process screenshot
|
|
95
|
+
let effectiveBuildId = getEffectiveBuildId(body.buildId);
|
|
96
|
+
let screenshotData = extractScreenshotData(body);
|
|
97
|
+
await buildManager.addScreenshot(effectiveBuildId, screenshotData);
|
|
98
|
+
let response = buildSuccessResponse();
|
|
99
|
+
sendResponse(res, response);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
output.error('Failed to process screenshot:', error);
|
|
102
|
+
let response = buildInternalErrorResponse();
|
|
103
|
+
sendResponse(res, response);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Send response to client
|
|
109
|
+
* @param {Object} res - HTTP response
|
|
110
|
+
* @param {{ status: number, body: Object }} response - Response data
|
|
111
|
+
*/
|
|
112
|
+
function sendResponse(res, response) {
|
|
113
|
+
res.statusCode = response.status;
|
|
114
|
+
res.end(JSON.stringify(response.body));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Server Lifecycle
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Start the HTTP server
|
|
123
|
+
* @param {Object} options - Options
|
|
124
|
+
* @param {Object} options.config - Server configuration
|
|
125
|
+
* @param {Function} options.requestHandler - Request handler function
|
|
126
|
+
* @param {Object} options.deps - Dependencies
|
|
127
|
+
* @param {Function} options.deps.createHttpServer - HTTP server factory (http.createServer)
|
|
128
|
+
* @param {Function} options.deps.createError - Error factory
|
|
129
|
+
* @param {Object} options.deps.output - Output utilities
|
|
130
|
+
* @returns {Promise<Object>} HTTP server instance
|
|
131
|
+
*/
|
|
132
|
+
export function startServer({
|
|
133
|
+
config,
|
|
134
|
+
requestHandler,
|
|
135
|
+
deps
|
|
136
|
+
}) {
|
|
137
|
+
let {
|
|
138
|
+
createHttpServer,
|
|
139
|
+
createError,
|
|
140
|
+
output
|
|
141
|
+
} = deps;
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
let server = createHttpServer(requestHandler);
|
|
144
|
+
let {
|
|
145
|
+
port,
|
|
146
|
+
host
|
|
147
|
+
} = buildServerListenOptions(config);
|
|
148
|
+
server.listen(port, host, error => {
|
|
149
|
+
if (error) {
|
|
150
|
+
reject(createError(`Failed to start screenshot server: ${error.message}`, 'SERVER_ERROR'));
|
|
151
|
+
} else {
|
|
152
|
+
output.info(buildServerStartedMessage(port));
|
|
153
|
+
resolve(server);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Stop the HTTP server
|
|
161
|
+
* @param {Object} options - Options
|
|
162
|
+
* @param {Object|null} options.server - HTTP server instance
|
|
163
|
+
* @param {Object} options.deps - Dependencies
|
|
164
|
+
* @param {Object} options.deps.output - Output utilities
|
|
165
|
+
* @returns {Promise<void>}
|
|
166
|
+
*/
|
|
167
|
+
export function stopServer({
|
|
168
|
+
server,
|
|
169
|
+
deps
|
|
170
|
+
}) {
|
|
171
|
+
let {
|
|
172
|
+
output
|
|
173
|
+
} = deps;
|
|
174
|
+
if (!server) {
|
|
175
|
+
return Promise.resolve();
|
|
176
|
+
}
|
|
177
|
+
return new Promise(resolve => {
|
|
178
|
+
server.close(() => {
|
|
179
|
+
output.info(buildServerStoppedMessage());
|
|
180
|
+
resolve();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
package/dist/sdk/index.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
import { EventEmitter } from 'node:events';
|
|
14
14
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
15
15
|
import { ScreenshotServer } from '../services/screenshot-server.js';
|
|
16
|
-
import { createTDDService } from '../services/tdd-service.js';
|
|
17
16
|
import { createUploader } from '../services/uploader.js';
|
|
17
|
+
import { createTDDService } from '../tdd/tdd-service.js';
|
|
18
18
|
import { loadConfig } from '../utils/config-loader.js';
|
|
19
19
|
import { resolveImageBuffer } from '../utils/file-helpers.js';
|
|
20
20
|
import * as output from '../utils/output.js';
|
|
@@ -357,9 +357,10 @@ export class VizzlySDK extends EventEmitter {
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
|
|
360
|
+
|
|
361
361
|
// Export service creators for advanced usage
|
|
362
362
|
export { createUploader } from '../services/uploader.js';
|
|
363
|
+
export { createTDDService } from '../tdd/tdd-service.js';
|
|
363
364
|
// Re-export key utilities and errors
|
|
364
365
|
export { loadConfig } from '../utils/config-loader.js';
|
|
365
366
|
export * as output from '../utils/output.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
+
import { uploadScreenshot as defaultUploadScreenshot } from '../../api/index.js';
|
|
4
5
|
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
5
6
|
import * as output from '../../utils/output.js';
|
|
6
7
|
|
|
@@ -34,7 +35,15 @@ import * as output from '../../utils/output.js';
|
|
|
34
35
|
* └─────────────────────────────────────────────────────────────┘
|
|
35
36
|
*/
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Create an API handler for screenshot uploads.
|
|
40
|
+
* @param {Object} client - API client with request method
|
|
41
|
+
* @param {Object} options - Optional dependencies for testing
|
|
42
|
+
* @param {Function} options.uploadScreenshot - Upload function (defaults to API uploadScreenshot)
|
|
43
|
+
*/
|
|
44
|
+
export const createApiHandler = (client, {
|
|
45
|
+
uploadScreenshot = defaultUploadScreenshot
|
|
46
|
+
} = {}) => {
|
|
38
47
|
let vizzlyDisabled = false;
|
|
39
48
|
let screenshotCount = 0;
|
|
40
49
|
let uploadPromises = [];
|
|
@@ -52,13 +61,13 @@ export const createApiHandler = apiService => {
|
|
|
52
61
|
};
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
// buildId is optional - API
|
|
64
|
+
// buildId is optional - API will handle it appropriately
|
|
56
65
|
|
|
57
|
-
if (!
|
|
66
|
+
if (!client) {
|
|
58
67
|
return {
|
|
59
68
|
statusCode: 500,
|
|
60
69
|
body: {
|
|
61
|
-
error: 'API
|
|
70
|
+
error: 'API client not available'
|
|
62
71
|
}
|
|
63
72
|
};
|
|
64
73
|
}
|
|
@@ -114,7 +123,7 @@ export const createApiHandler = apiService => {
|
|
|
114
123
|
screenshotCount++;
|
|
115
124
|
|
|
116
125
|
// Fire upload in background - DON'T AWAIT!
|
|
117
|
-
|
|
126
|
+
let uploadPromise = uploadScreenshot(client, buildId, name, imageBuffer, properties ?? {}).then(result => {
|
|
118
127
|
if (!result.skipped) {
|
|
119
128
|
output.debug('upload', name);
|
|
120
129
|
}
|
|
@@ -1,17 +1,66 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { getDimensionsSync } from '@vizzly-testing/honeydiff';
|
|
5
|
-
import { TddService } from '../../
|
|
6
|
-
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
7
|
-
import * as
|
|
8
|
-
import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
|
|
1
|
+
import { Buffer as defaultBuffer } from 'node:buffer';
|
|
2
|
+
import { existsSync as defaultExistsSync, readFileSync as defaultReadFileSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
|
|
3
|
+
import { join as defaultJoin, resolve as defaultResolve } from 'node:path';
|
|
4
|
+
import { getDimensionsSync as defaultGetDimensionsSync } from '@vizzly-testing/honeydiff';
|
|
5
|
+
import { TddService as DefaultTddService } from '../../tdd/tdd-service.js';
|
|
6
|
+
import { detectImageInputType as defaultDetectImageInputType } from '../../utils/image-input-detector.js';
|
|
7
|
+
import * as defaultOutput from '../../utils/output.js';
|
|
8
|
+
import { sanitizeScreenshotName as defaultSanitizeScreenshotName, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../../utils/security.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unwrap double-nested properties if needed
|
|
12
|
+
* Client SDK wraps options in properties field, so we may get { properties: { properties: {...} } }
|
|
13
|
+
*/
|
|
14
|
+
export const unwrapProperties = properties => {
|
|
15
|
+
if (!properties) return {};
|
|
16
|
+
if (properties.properties && typeof properties.properties === 'object') {
|
|
17
|
+
// Merge top-level properties with nested properties
|
|
18
|
+
let unwrapped = {
|
|
19
|
+
...properties,
|
|
20
|
+
...properties.properties
|
|
21
|
+
};
|
|
22
|
+
// Remove the nested properties field to avoid confusion
|
|
23
|
+
delete unwrapped.properties;
|
|
24
|
+
return unwrapped;
|
|
25
|
+
}
|
|
26
|
+
return properties;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract properties to top-level format matching cloud API
|
|
31
|
+
* Normalizes viewport to viewport_width/height, ensures browser is set
|
|
32
|
+
*/
|
|
33
|
+
export const extractProperties = validatedProperties => {
|
|
34
|
+
if (!validatedProperties) return {};
|
|
35
|
+
return {
|
|
36
|
+
...validatedProperties,
|
|
37
|
+
// Normalize viewport to top-level viewport_width/height (cloud format)
|
|
38
|
+
viewport_width: validatedProperties.viewport?.width ?? validatedProperties.viewport_width ?? null,
|
|
39
|
+
viewport_height: validatedProperties.viewport?.height ?? validatedProperties.viewport_height ?? null,
|
|
40
|
+
browser: validatedProperties.browser ?? null,
|
|
41
|
+
// Preserve nested structure in metadata for backward compatibility
|
|
42
|
+
metadata: validatedProperties
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert absolute file paths to web-accessible URLs
|
|
48
|
+
*/
|
|
49
|
+
export const convertPathToUrl = (filePath, vizzlyDir) => {
|
|
50
|
+
if (!filePath) return null;
|
|
51
|
+
// Convert absolute path to relative path from .vizzly directory
|
|
52
|
+
if (filePath.startsWith(vizzlyDir)) {
|
|
53
|
+
let relativePath = filePath.substring(vizzlyDir.length + 1);
|
|
54
|
+
return `/images/${relativePath}`;
|
|
55
|
+
}
|
|
56
|
+
return filePath;
|
|
57
|
+
};
|
|
9
58
|
|
|
10
59
|
/**
|
|
11
60
|
* Group comparisons by screenshot name with variant structure
|
|
12
61
|
* Matches cloud product's grouping logic from comparison.js
|
|
13
62
|
*/
|
|
14
|
-
const groupComparisons = comparisons => {
|
|
63
|
+
export const groupComparisons = comparisons => {
|
|
15
64
|
const groups = new Map();
|
|
16
65
|
|
|
17
66
|
// Group by screenshot name
|
|
@@ -87,7 +136,22 @@ const groupComparisons = comparisons => {
|
|
|
87
136
|
return a.name.localeCompare(b.name);
|
|
88
137
|
});
|
|
89
138
|
};
|
|
90
|
-
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false) => {
|
|
139
|
+
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false, deps = {}) => {
|
|
140
|
+
// Inject dependencies with defaults
|
|
141
|
+
let {
|
|
142
|
+
TddService = DefaultTddService,
|
|
143
|
+
existsSync = defaultExistsSync,
|
|
144
|
+
readFileSync = defaultReadFileSync,
|
|
145
|
+
writeFileSync = defaultWriteFileSync,
|
|
146
|
+
join = defaultJoin,
|
|
147
|
+
resolve = defaultResolve,
|
|
148
|
+
Buffer = defaultBuffer,
|
|
149
|
+
getDimensionsSync = defaultGetDimensionsSync,
|
|
150
|
+
detectImageInputType = defaultDetectImageInputType,
|
|
151
|
+
sanitizeScreenshotName = defaultSanitizeScreenshotName,
|
|
152
|
+
validateScreenshotProperties = defaultValidateScreenshotProperties,
|
|
153
|
+
output = defaultOutput
|
|
154
|
+
} = deps;
|
|
91
155
|
const tddService = new TddService(config, workingDir, setBaseline);
|
|
92
156
|
const reportPath = join(workingDir, '.vizzly', 'report-data.json');
|
|
93
157
|
const readReportData = () => {
|
|
@@ -217,18 +281,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
217
281
|
}
|
|
218
282
|
|
|
219
283
|
// Unwrap double-nested properties if needed (client SDK wraps options in properties field)
|
|
220
|
-
|
|
221
|
-
// and client SDK wraps it as { properties: options }
|
|
222
|
-
let unwrappedProperties = properties;
|
|
223
|
-
if (properties.properties && typeof properties.properties === 'object') {
|
|
224
|
-
// Merge top-level properties with nested properties
|
|
225
|
-
unwrappedProperties = {
|
|
226
|
-
...properties,
|
|
227
|
-
...properties.properties
|
|
228
|
-
};
|
|
229
|
-
// Remove the nested properties field to avoid confusion
|
|
230
|
-
delete unwrappedProperties.properties;
|
|
231
|
-
}
|
|
284
|
+
let unwrappedProperties = unwrapProperties(properties);
|
|
232
285
|
|
|
233
286
|
// Validate and sanitize properties
|
|
234
287
|
let validatedProperties;
|
|
@@ -246,19 +299,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
246
299
|
}
|
|
247
300
|
|
|
248
301
|
// Extract ALL properties to top-level (matching cloud API behavior)
|
|
249
|
-
|
|
250
|
-
// Spread all validated properties first, then normalize viewport/browser for cloud format
|
|
251
|
-
const extractedProperties = {
|
|
252
|
-
...validatedProperties,
|
|
253
|
-
// Normalize viewport to top-level viewport_width/height (cloud format)
|
|
254
|
-
// Use nullish coalescing to preserve any existing top-level values
|
|
255
|
-
viewport_width: validatedProperties.viewport?.width ?? validatedProperties.viewport_width ?? null,
|
|
256
|
-
viewport_height: validatedProperties.viewport?.height ?? validatedProperties.viewport_height ?? null,
|
|
257
|
-
browser: validatedProperties.browser ?? null,
|
|
258
|
-
// Preserve nested structure in metadata for backward compatibility
|
|
259
|
-
// Signature generation checks multiple locations: top-level, metadata.*, metadata.properties.*
|
|
260
|
-
metadata: validatedProperties
|
|
261
|
-
};
|
|
302
|
+
const extractedProperties = extractProperties(validatedProperties);
|
|
262
303
|
|
|
263
304
|
// Support both base64 encoded images and file paths
|
|
264
305
|
// Vitest browser mode returns file paths, so we need to handle both
|
|
@@ -336,16 +377,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
336
377
|
// Comparison tracked by tdd.js event handler
|
|
337
378
|
|
|
338
379
|
// Convert absolute file paths to web-accessible URLs
|
|
339
|
-
const
|
|
340
|
-
if (!filePath) return null;
|
|
341
|
-
// Convert absolute path to relative path from .vizzly directory
|
|
342
|
-
const vizzlyDir = join(workingDir, '.vizzly');
|
|
343
|
-
if (filePath.startsWith(vizzlyDir)) {
|
|
344
|
-
const relativePath = filePath.substring(vizzlyDir.length + 1);
|
|
345
|
-
return `/images/${relativePath}`;
|
|
346
|
-
}
|
|
347
|
-
return filePath;
|
|
348
|
-
};
|
|
380
|
+
const vizzlyDir = join(workingDir, '.vizzly');
|
|
349
381
|
|
|
350
382
|
// Record the comparison for the dashboard
|
|
351
383
|
const newComparison = {
|
|
@@ -354,9 +386,9 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
354
386
|
name: comparison.name,
|
|
355
387
|
originalName: name,
|
|
356
388
|
status: comparison.status,
|
|
357
|
-
baseline: convertPathToUrl(comparison.baseline),
|
|
358
|
-
current: convertPathToUrl(comparison.current),
|
|
359
|
-
diff: convertPathToUrl(comparison.diff),
|
|
389
|
+
baseline: convertPathToUrl(comparison.baseline, vizzlyDir),
|
|
390
|
+
current: convertPathToUrl(comparison.current, vizzlyDir),
|
|
391
|
+
diff: convertPathToUrl(comparison.diff, vizzlyDir),
|
|
360
392
|
diffPercentage: comparison.diffPercentage,
|
|
361
393
|
threshold: comparison.threshold,
|
|
362
394
|
properties: extractedProperties,
|