@vizzly-testing/cli 0.3.2 → 0.5.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 +50 -28
- package/dist/cli.js +34 -30
- package/dist/client/index.js +1 -1
- package/dist/commands/run.js +38 -11
- package/dist/commands/tdd.js +30 -18
- package/dist/commands/upload.js +56 -5
- package/dist/server/handlers/api-handler.js +83 -0
- package/dist/server/handlers/tdd-handler.js +138 -0
- package/dist/server/http-server.js +132 -0
- package/dist/services/api-service.js +22 -2
- package/dist/services/server-manager.js +45 -29
- package/dist/services/test-runner.js +66 -69
- package/dist/services/uploader.js +11 -4
- package/dist/types/commands/run.d.ts +4 -1
- package/dist/types/commands/tdd.d.ts +5 -1
- package/dist/types/sdk/index.d.ts +6 -6
- package/dist/types/server/handlers/api-handler.d.ts +49 -0
- package/dist/types/server/handlers/tdd-handler.d.ts +85 -0
- package/dist/types/server/http-server.d.ts +5 -0
- package/dist/types/services/api-service.d.ts +1 -0
- package/dist/types/services/server-manager.d.ts +148 -3
- package/dist/types/services/test-runner.d.ts +1 -0
- package/dist/types/services/uploader.d.ts +2 -1
- package/dist/types/utils/ci-env.d.ts +55 -0
- package/dist/types/utils/config-helpers.d.ts +1 -1
- package/dist/types/utils/console-ui.d.ts +1 -1
- package/dist/types/utils/git.d.ts +12 -0
- package/dist/utils/ci-env.js +293 -0
- package/dist/utils/console-ui.js +4 -14
- package/dist/utils/git.js +38 -0
- package/docs/api-reference.md +17 -5
- package/docs/getting-started.md +1 -1
- package/docs/tdd-mode.md +9 -9
- package/docs/test-integration.md +9 -17
- package/docs/upload-command.md +7 -0
- package/package.json +4 -5
- package/dist/screenshot-wrapper.js +0 -68
- package/dist/server/index.js +0 -522
- package/dist/services/service-utils.js +0 -171
- package/dist/types/index.js +0 -153
- package/dist/types/screenshot-wrapper.d.ts +0 -27
- package/dist/types/server/index.d.ts +0 -38
- package/dist/types/services/service-utils.d.ts +0 -45
- package/dist/types/types/index.d.ts +0 -373
- package/dist/types/utils/diagnostics.d.ts +0 -69
- package/dist/types/utils/error-messages.d.ts +0 -42
- package/dist/types/utils/framework-detector.d.ts +0 -5
- package/dist/types/utils/help.d.ts +0 -11
- package/dist/types/utils/image-comparison.d.ts +0 -42
- package/dist/types/utils/package.d.ts +0 -1
- package/dist/types/utils/project-detection.d.ts +0 -19
- package/dist/types/utils/ui-helpers.d.ts +0 -23
- package/dist/utils/diagnostics.js +0 -184
- package/dist/utils/error-messages.js +0 -34
- package/dist/utils/framework-detector.js +0 -40
- package/dist/utils/help.js +0 -66
- package/dist/utils/image-comparison.js +0 -172
- package/dist/utils/package.js +0 -9
- package/dist/utils/project-detection.js +0 -145
- package/dist/utils/ui-helpers.js +0 -86
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { createServiceLogger } from '../../utils/logger-factory.js';
|
|
3
|
+
import { TddService } from '../../services/tdd-service.js';
|
|
4
|
+
import { colors } from '../../utils/colors.js';
|
|
5
|
+
const logger = createServiceLogger('TDD-HANDLER');
|
|
6
|
+
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison) => {
|
|
7
|
+
const tddService = new TddService(config, workingDir);
|
|
8
|
+
const builds = new Map();
|
|
9
|
+
const initialize = async () => {
|
|
10
|
+
logger.info('🔄 TDD mode enabled - setting up local comparison...');
|
|
11
|
+
const baseline = await tddService.loadBaseline();
|
|
12
|
+
if (!baseline) {
|
|
13
|
+
if (config.apiKey) {
|
|
14
|
+
logger.info('📥 No local baseline found, downloading from Vizzly...');
|
|
15
|
+
await tddService.downloadBaselines(baselineBuild, baselineComparison);
|
|
16
|
+
} else {
|
|
17
|
+
logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
logger.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const registerBuild = buildId => {
|
|
24
|
+
builds.set(buildId, {
|
|
25
|
+
id: buildId,
|
|
26
|
+
name: `TDD Build ${buildId}`,
|
|
27
|
+
branch: 'current',
|
|
28
|
+
environment: 'test',
|
|
29
|
+
screenshots: [],
|
|
30
|
+
createdAt: Date.now()
|
|
31
|
+
});
|
|
32
|
+
logger.debug(`Registered TDD build: ${buildId}`);
|
|
33
|
+
};
|
|
34
|
+
const handleScreenshot = async (buildId, name, image, properties = {}) => {
|
|
35
|
+
const build = builds.get(buildId);
|
|
36
|
+
if (!build) {
|
|
37
|
+
throw new Error(`Build ${buildId} not found`);
|
|
38
|
+
}
|
|
39
|
+
const screenshot = {
|
|
40
|
+
name,
|
|
41
|
+
imageData: image,
|
|
42
|
+
properties,
|
|
43
|
+
timestamp: Date.now()
|
|
44
|
+
};
|
|
45
|
+
build.screenshots.push(screenshot);
|
|
46
|
+
const imageBuffer = Buffer.from(image, 'base64');
|
|
47
|
+
const comparison = await tddService.compareScreenshot(name, imageBuffer, properties);
|
|
48
|
+
if (comparison.status === 'failed') {
|
|
49
|
+
return {
|
|
50
|
+
statusCode: 422,
|
|
51
|
+
body: {
|
|
52
|
+
error: 'Visual difference detected',
|
|
53
|
+
details: `Screenshot '${name}' differs from baseline`,
|
|
54
|
+
comparison: {
|
|
55
|
+
name: comparison.name,
|
|
56
|
+
status: comparison.status,
|
|
57
|
+
baseline: comparison.baseline,
|
|
58
|
+
current: comparison.current,
|
|
59
|
+
diff: comparison.diff
|
|
60
|
+
},
|
|
61
|
+
tddMode: true
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (comparison.status === 'baseline-updated') {
|
|
66
|
+
return {
|
|
67
|
+
statusCode: 200,
|
|
68
|
+
body: {
|
|
69
|
+
status: 'success',
|
|
70
|
+
message: `Baseline updated for ${name}`,
|
|
71
|
+
comparison: {
|
|
72
|
+
name: comparison.name,
|
|
73
|
+
status: comparison.status,
|
|
74
|
+
baseline: comparison.baseline,
|
|
75
|
+
current: comparison.current
|
|
76
|
+
},
|
|
77
|
+
tddMode: true
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (comparison.status === 'error') {
|
|
82
|
+
return {
|
|
83
|
+
statusCode: 500,
|
|
84
|
+
body: {
|
|
85
|
+
error: `Comparison failed: ${comparison.error}`,
|
|
86
|
+
tddMode: true
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
logger.debug(`✅ TDD: ${comparison.status.toUpperCase()} ${name}`);
|
|
91
|
+
return {
|
|
92
|
+
statusCode: 200,
|
|
93
|
+
body: {
|
|
94
|
+
success: true,
|
|
95
|
+
comparison: {
|
|
96
|
+
name: comparison.name,
|
|
97
|
+
status: comparison.status
|
|
98
|
+
},
|
|
99
|
+
tddMode: true
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const getScreenshotCount = buildId => {
|
|
104
|
+
const build = builds.get(buildId);
|
|
105
|
+
return build ? build.screenshots.length : 0;
|
|
106
|
+
};
|
|
107
|
+
const finishBuild = async buildId => {
|
|
108
|
+
const build = builds.get(buildId);
|
|
109
|
+
if (!build) {
|
|
110
|
+
throw new Error(`Build ${buildId} not found`);
|
|
111
|
+
}
|
|
112
|
+
if (build.screenshots.length === 0) {
|
|
113
|
+
throw new Error('No screenshots to process. Make sure your tests are calling the Vizzly screenshot function.');
|
|
114
|
+
}
|
|
115
|
+
const results = tddService.printResults();
|
|
116
|
+
builds.delete(buildId);
|
|
117
|
+
return {
|
|
118
|
+
id: buildId,
|
|
119
|
+
name: build.name,
|
|
120
|
+
tddMode: true,
|
|
121
|
+
results,
|
|
122
|
+
url: null,
|
|
123
|
+
passed: results.failed === 0 && results.errors === 0
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const cleanup = () => {
|
|
127
|
+
builds.clear();
|
|
128
|
+
logger.debug('TDD handler cleanup completed');
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
initialize,
|
|
132
|
+
registerBuild,
|
|
133
|
+
handleScreenshot,
|
|
134
|
+
getScreenshotCount,
|
|
135
|
+
finishBuild,
|
|
136
|
+
cleanup
|
|
137
|
+
};
|
|
138
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { createServiceLogger } from '../utils/logger-factory.js';
|
|
3
|
+
const logger = createServiceLogger('HTTP-SERVER');
|
|
4
|
+
export const createHttpServer = (port, screenshotHandler, emitter = null) => {
|
|
5
|
+
let server = null;
|
|
6
|
+
const parseRequestBody = req => {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
let body = '';
|
|
9
|
+
req.on('data', chunk => {
|
|
10
|
+
body += chunk.toString();
|
|
11
|
+
});
|
|
12
|
+
req.on('end', () => {
|
|
13
|
+
try {
|
|
14
|
+
const data = JSON.parse(body);
|
|
15
|
+
resolve(data);
|
|
16
|
+
} catch {
|
|
17
|
+
reject(new Error('Invalid JSON'));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
req.on('error', reject);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
const handleRequest = async (req, res) => {
|
|
24
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
25
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
26
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
27
|
+
res.setHeader('Content-Type', 'application/json');
|
|
28
|
+
if (req.method === 'OPTIONS') {
|
|
29
|
+
res.statusCode = 200;
|
|
30
|
+
res.end();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
34
|
+
res.statusCode = 200;
|
|
35
|
+
res.end(JSON.stringify({
|
|
36
|
+
status: 'ok',
|
|
37
|
+
port: port,
|
|
38
|
+
uptime: process.uptime()
|
|
39
|
+
}));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (req.method === 'POST' && req.url === '/screenshot') {
|
|
43
|
+
try {
|
|
44
|
+
const body = await parseRequestBody(req);
|
|
45
|
+
const {
|
|
46
|
+
buildId,
|
|
47
|
+
name,
|
|
48
|
+
properties,
|
|
49
|
+
image
|
|
50
|
+
} = body;
|
|
51
|
+
if (!buildId || !name || !image) {
|
|
52
|
+
res.statusCode = 400;
|
|
53
|
+
res.end(JSON.stringify({
|
|
54
|
+
error: 'buildId, name, and image are required'
|
|
55
|
+
}));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const result = await screenshotHandler.handleScreenshot(buildId, name, image, properties);
|
|
59
|
+
|
|
60
|
+
// Emit screenshot captured event if emitter is available
|
|
61
|
+
if (emitter && result.statusCode === 200) {
|
|
62
|
+
emitter.emit('screenshot-captured', {
|
|
63
|
+
name,
|
|
64
|
+
count: screenshotHandler.getScreenshotCount?.(buildId) || 0,
|
|
65
|
+
skipped: result.body?.skipped
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
res.statusCode = result.statusCode;
|
|
69
|
+
res.end(JSON.stringify(result.body));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error('Screenshot processing error:', error);
|
|
72
|
+
res.statusCode = 500;
|
|
73
|
+
res.end(JSON.stringify({
|
|
74
|
+
error: 'Failed to process screenshot'
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.statusCode = 404;
|
|
80
|
+
res.end(JSON.stringify({
|
|
81
|
+
error: 'Not found'
|
|
82
|
+
}));
|
|
83
|
+
};
|
|
84
|
+
const start = () => {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
server = createServer(async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
await handleRequest(req, res);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error('Server error:', error);
|
|
91
|
+
res.statusCode = 500;
|
|
92
|
+
res.setHeader('Content-Type', 'application/json');
|
|
93
|
+
res.end(JSON.stringify({
|
|
94
|
+
error: 'Internal server error'
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
server.listen(port, '127.0.0.1', error => {
|
|
99
|
+
if (error) {
|
|
100
|
+
reject(error);
|
|
101
|
+
} else {
|
|
102
|
+
logger.debug(`HTTP server listening on http://127.0.0.1:${port}`);
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
server.on('error', error => {
|
|
107
|
+
if (error.code === 'EADDRINUSE') {
|
|
108
|
+
reject(new Error(`Port ${port} is already in use. Try a different port with --port.`));
|
|
109
|
+
} else {
|
|
110
|
+
reject(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
const stop = () => {
|
|
116
|
+
if (server) {
|
|
117
|
+
return new Promise(resolve => {
|
|
118
|
+
server.close(() => {
|
|
119
|
+
server = null;
|
|
120
|
+
logger.debug('HTTP server stopped');
|
|
121
|
+
resolve();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
start,
|
|
129
|
+
stop,
|
|
130
|
+
getServer: () => server
|
|
131
|
+
};
|
|
132
|
+
};
|
|
@@ -16,6 +16,7 @@ export class ApiService {
|
|
|
16
16
|
constructor(options = {}) {
|
|
17
17
|
this.baseUrl = options.baseUrl || getApiUrl();
|
|
18
18
|
this.token = options.token || getApiToken();
|
|
19
|
+
this.uploadAll = options.uploadAll || false;
|
|
19
20
|
|
|
20
21
|
// Build User-Agent string
|
|
21
22
|
const command = options.command || 'run'; // Default to 'run' for API service
|
|
@@ -104,7 +105,9 @@ export class ApiService {
|
|
|
104
105
|
headers: {
|
|
105
106
|
'Content-Type': 'application/json'
|
|
106
107
|
},
|
|
107
|
-
body: JSON.stringify(
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
build: metadata
|
|
110
|
+
})
|
|
108
111
|
});
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -147,7 +150,24 @@ export class ApiService {
|
|
|
147
150
|
* @returns {Promise<Object>} Upload result
|
|
148
151
|
*/
|
|
149
152
|
async uploadScreenshot(buildId, name, buffer, metadata = {}) {
|
|
150
|
-
//
|
|
153
|
+
// Skip SHA deduplication entirely if uploadAll flag is set
|
|
154
|
+
if (this.uploadAll) {
|
|
155
|
+
// Upload directly without SHA calculation or checking
|
|
156
|
+
return this.request(`/api/sdk/builds/${buildId}/screenshots`, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: {
|
|
159
|
+
'Content-Type': 'application/json'
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
name,
|
|
163
|
+
image_data: buffer.toString('base64'),
|
|
164
|
+
properties: metadata ?? {}
|
|
165
|
+
// No SHA included when bypassing deduplication
|
|
166
|
+
})
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Normal flow with SHA deduplication
|
|
151
171
|
const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
152
172
|
|
|
153
173
|
// Check if this SHA already exists
|
|
@@ -1,44 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server Manager Service
|
|
3
|
-
* Manages the
|
|
3
|
+
* Manages the HTTP server with functional handlers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { BaseService } from './base-service.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createHttpServer } from '../server/http-server.js';
|
|
8
|
+
import { createTddHandler } from '../server/handlers/tdd-handler.js';
|
|
9
|
+
import { createApiHandler } from '../server/handlers/api-handler.js';
|
|
8
10
|
import { EventEmitter } from 'events';
|
|
9
11
|
export class ServerManager extends BaseService {
|
|
10
12
|
constructor(config, logger) {
|
|
11
13
|
super(config, {
|
|
12
14
|
logger
|
|
13
15
|
});
|
|
14
|
-
this.
|
|
16
|
+
this.httpServer = null;
|
|
17
|
+
this.handler = null;
|
|
18
|
+
this.emitter = null;
|
|
15
19
|
}
|
|
16
|
-
async start(buildId = null,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Create event emitter for server events
|
|
23
|
-
const emitter = new EventEmitter();
|
|
24
|
-
this.server = new VizzlyServer({
|
|
25
|
-
port: this.config?.server?.port || 47392,
|
|
26
|
-
config: this.config,
|
|
27
|
-
buildId,
|
|
28
|
-
buildInfo,
|
|
29
|
-
vizzlyApi: buildInfo || mode === 'eager' ? await this.createApiService() : null,
|
|
30
|
-
tddMode: mode === 'tdd',
|
|
31
|
-
// TDD mode only when explicitly set
|
|
32
|
-
baselineBuild: this.config?.baselineBuildId,
|
|
33
|
-
baselineComparison: this.config?.baselineComparisonId,
|
|
34
|
-
workingDir: process.cwd(),
|
|
35
|
-
emitter // Pass the emitter to the server
|
|
36
|
-
});
|
|
37
|
-
await super.start();
|
|
20
|
+
async start(buildId = null, tddMode = false) {
|
|
21
|
+
this.buildId = buildId;
|
|
22
|
+
this.tddMode = tddMode;
|
|
23
|
+
return super.start();
|
|
38
24
|
}
|
|
39
25
|
async onStart() {
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
this.emitter = new EventEmitter();
|
|
27
|
+
const port = this.config?.server?.port || 47392;
|
|
28
|
+
if (this.tddMode) {
|
|
29
|
+
this.handler = createTddHandler(this.config, process.cwd(), this.config?.baselineBuildId, this.config?.baselineComparisonId);
|
|
30
|
+
await this.handler.initialize();
|
|
31
|
+
if (this.buildId) {
|
|
32
|
+
this.handler.registerBuild(this.buildId);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
const apiService = await this.createApiService();
|
|
36
|
+
this.handler = createApiHandler(apiService);
|
|
37
|
+
}
|
|
38
|
+
this.httpServer = createHttpServer(port, this.handler, this.emitter);
|
|
39
|
+
if (this.httpServer) {
|
|
40
|
+
await this.httpServer.start();
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
async createApiService() {
|
|
@@ -54,8 +53,25 @@ export class ServerManager extends BaseService {
|
|
|
54
53
|
});
|
|
55
54
|
}
|
|
56
55
|
async onStop() {
|
|
57
|
-
if (this.
|
|
58
|
-
await this.
|
|
56
|
+
if (this.httpServer) {
|
|
57
|
+
await this.httpServer.stop();
|
|
59
58
|
}
|
|
59
|
+
if (this.handler?.cleanup) {
|
|
60
|
+
try {
|
|
61
|
+
this.handler.cleanup();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.logger.debug('Handler cleanup error:', error.message);
|
|
64
|
+
// Don't throw - cleanup errors shouldn't fail the stop process
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Expose server interface for compatibility
|
|
70
|
+
get server() {
|
|
71
|
+
return {
|
|
72
|
+
emitter: this.emitter,
|
|
73
|
+
getScreenshotCount: buildId => this.handler?.getScreenshotCount?.(buildId) || 0,
|
|
74
|
+
finishBuild: buildId => this.handler?.finishBuild?.(buildId)
|
|
75
|
+
};
|
|
60
76
|
}
|
|
61
77
|
}
|
|
@@ -40,87 +40,47 @@ export class TestRunner extends BaseService {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
try {
|
|
43
|
-
let buildInfo = null;
|
|
44
43
|
let buildUrl = null;
|
|
45
44
|
let screenshotCount = 0;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.logger.debug(`TDD build created with ID: ${build.id}`);
|
|
52
|
-
} else if (options.eager) {
|
|
53
|
-
// Eager mode: create build immediately via API
|
|
54
|
-
this.logger.debug('Eager mode: creating build via API...');
|
|
45
|
+
|
|
46
|
+
// Create build based on mode
|
|
47
|
+
buildId = await this.createBuild(options, tdd);
|
|
48
|
+
if (!tdd && buildId) {
|
|
49
|
+
// Get build URL for API mode
|
|
55
50
|
const apiService = await this.createApiService();
|
|
56
51
|
if (apiService) {
|
|
57
|
-
|
|
58
|
-
build
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
commit_sha: options.commit,
|
|
63
|
-
commit_message: options.message
|
|
52
|
+
try {
|
|
53
|
+
const build = await apiService.getBuild(buildId);
|
|
54
|
+
buildUrl = build.url;
|
|
55
|
+
if (buildUrl) {
|
|
56
|
+
this.logger.info(`Build URL: ${buildUrl}`);
|
|
64
57
|
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
buildUrl = buildResult.url;
|
|
68
|
-
this.logger.debug(`Eager build created with ID: ${buildId}`);
|
|
69
|
-
if (buildUrl) {
|
|
70
|
-
this.logger.info(`Build URL: ${buildUrl}`);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
this.logger.debug('Could not retrieve build URL:', error.message);
|
|
71
60
|
}
|
|
72
|
-
|
|
73
|
-
// Emit build created event for eager mode
|
|
74
|
-
this.emit('build-created', {
|
|
75
|
-
buildId: buildResult.id,
|
|
76
|
-
url: buildResult.url,
|
|
77
|
-
name: buildResult.name || options.buildName
|
|
78
|
-
});
|
|
79
|
-
} else {
|
|
80
|
-
this.logger.warn('No API key available for eager build creation, falling back to lazy mode');
|
|
81
61
|
}
|
|
82
|
-
} else {
|
|
83
|
-
// Lazy mode: prepare build info for API creation on first screenshot
|
|
84
|
-
buildInfo = {
|
|
85
|
-
buildName: options.buildName || `Test Run ${new Date().toISOString()}`,
|
|
86
|
-
branch: options.branch || 'main',
|
|
87
|
-
environment: options.environment || 'test',
|
|
88
|
-
commitSha: options.commit,
|
|
89
|
-
commitMessage: options.message
|
|
90
|
-
};
|
|
91
62
|
}
|
|
92
63
|
|
|
93
|
-
// Start server with appropriate
|
|
94
|
-
|
|
95
|
-
await this.serverManager.start(buildId, buildInfo, mode);
|
|
64
|
+
// Start server with appropriate handler
|
|
65
|
+
await this.serverManager.start(buildId, tdd);
|
|
96
66
|
|
|
97
67
|
// Forward server events
|
|
98
|
-
if (this.serverManager.server
|
|
99
|
-
this.serverManager.server.emitter.on('build-created', buildInfo => {
|
|
100
|
-
// Update local buildId and buildUrl from server
|
|
101
|
-
buildId = buildInfo.buildId;
|
|
102
|
-
buildUrl = buildInfo.url;
|
|
103
|
-
this.emit('build-created', buildInfo);
|
|
104
|
-
});
|
|
68
|
+
if (this.serverManager.server?.emitter) {
|
|
105
69
|
this.serverManager.server.emitter.on('screenshot-captured', screenshotInfo => {
|
|
106
70
|
screenshotCount++;
|
|
107
71
|
this.emit('screenshot-captured', screenshotInfo);
|
|
108
72
|
});
|
|
109
73
|
}
|
|
110
|
-
if (tdd) {
|
|
111
|
-
this.logger.debug('TDD service ready for comparisons');
|
|
112
|
-
}
|
|
113
74
|
const env = {
|
|
114
75
|
...process.env,
|
|
115
76
|
VIZZLY_SERVER_URL: `http://localhost:${this.config.server.port}`,
|
|
116
|
-
VIZZLY_BUILD_ID: buildId
|
|
117
|
-
// Use 'lazy' for API-driven builds
|
|
77
|
+
VIZZLY_BUILD_ID: buildId,
|
|
118
78
|
VIZZLY_ENABLED: 'true',
|
|
119
79
|
VIZZLY_SET_BASELINE: options.setBaseline || options['set-baseline'] ? 'true' : 'false'
|
|
120
80
|
};
|
|
121
81
|
await this.executeTestCommand(testCommand, env);
|
|
122
82
|
|
|
123
|
-
// Finalize
|
|
83
|
+
// Finalize build
|
|
124
84
|
const executionTime = Date.now() - startTime;
|
|
125
85
|
await this.finalizeBuild(buildId, tdd, true, executionTime);
|
|
126
86
|
return {
|
|
@@ -133,14 +93,45 @@ export class TestRunner extends BaseService {
|
|
|
133
93
|
} catch (error) {
|
|
134
94
|
this.logger.error('Test run failed:', error);
|
|
135
95
|
|
|
136
|
-
// Finalize
|
|
96
|
+
// Finalize build on failure
|
|
137
97
|
const executionTime = Date.now() - startTime;
|
|
138
98
|
await this.finalizeBuild(buildId, tdd, false, executionTime);
|
|
139
99
|
throw error;
|
|
140
100
|
} finally {
|
|
141
101
|
await this.serverManager.stop();
|
|
142
|
-
|
|
143
|
-
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async createBuild(options, tdd) {
|
|
105
|
+
if (tdd) {
|
|
106
|
+
// TDD mode: create local build
|
|
107
|
+
this.logger.debug('TDD mode: creating local build...');
|
|
108
|
+
const build = await this.buildManager.createBuild(options);
|
|
109
|
+
this.logger.debug(`TDD build created with ID: ${build.id}`);
|
|
110
|
+
return build.id;
|
|
111
|
+
} else {
|
|
112
|
+
// API mode: create build via API
|
|
113
|
+
this.logger.debug('Creating build via API...');
|
|
114
|
+
const apiService = await this.createApiService();
|
|
115
|
+
if (apiService) {
|
|
116
|
+
const buildResult = await apiService.createBuild({
|
|
117
|
+
name: options.buildName || `Test Run ${new Date().toISOString()}`,
|
|
118
|
+
branch: options.branch || 'main',
|
|
119
|
+
environment: options.environment || 'test',
|
|
120
|
+
commit_sha: options.commit,
|
|
121
|
+
commit_message: options.message,
|
|
122
|
+
github_pull_request_number: options.pullRequestNumber
|
|
123
|
+
});
|
|
124
|
+
this.logger.debug(`Build created with ID: ${buildResult.id}`);
|
|
125
|
+
|
|
126
|
+
// Emit build created event
|
|
127
|
+
this.emit('build-created', {
|
|
128
|
+
buildId: buildResult.id,
|
|
129
|
+
url: buildResult.url,
|
|
130
|
+
name: buildResult.name || options.buildName
|
|
131
|
+
});
|
|
132
|
+
return buildResult.id;
|
|
133
|
+
} else {
|
|
134
|
+
throw new VizzlyError('No API key available for build creation', 'API_KEY_MISSING');
|
|
144
135
|
}
|
|
145
136
|
}
|
|
146
137
|
}
|
|
@@ -151,9 +142,8 @@ export class TestRunner extends BaseService {
|
|
|
151
142
|
} = await import('./api-service.js');
|
|
152
143
|
return new ApiService({
|
|
153
144
|
...this.config,
|
|
154
|
-
command: 'run'
|
|
155
|
-
|
|
156
|
-
logger: this.logger
|
|
145
|
+
command: 'run',
|
|
146
|
+
uploadAll: this.config.uploadAll
|
|
157
147
|
});
|
|
158
148
|
}
|
|
159
149
|
async finalizeBuild(buildId, isTddMode, success, executionTime) {
|
|
@@ -163,15 +153,16 @@ export class TestRunner extends BaseService {
|
|
|
163
153
|
}
|
|
164
154
|
try {
|
|
165
155
|
if (isTddMode) {
|
|
166
|
-
// TDD mode: use
|
|
167
|
-
if (this.
|
|
168
|
-
await this.
|
|
169
|
-
success
|
|
170
|
-
});
|
|
156
|
+
// TDD mode: use server handler to finalize (local-only)
|
|
157
|
+
if (this.serverManager.server?.finishBuild) {
|
|
158
|
+
await this.serverManager.server.finishBuild(buildId);
|
|
171
159
|
this.logger.debug(`TDD build ${buildId} finalized with success: ${success}`);
|
|
160
|
+
} else {
|
|
161
|
+
// In TDD mode without a server, just log that finalization is skipped
|
|
162
|
+
this.logger.debug(`TDD build ${buildId} finalization skipped (local-only mode)`);
|
|
172
163
|
}
|
|
173
164
|
} else {
|
|
174
|
-
// API mode
|
|
165
|
+
// API mode: use API service to update build status
|
|
175
166
|
const apiService = await this.createApiService();
|
|
176
167
|
if (apiService) {
|
|
177
168
|
await apiService.finalizeBuild(buildId, success, executionTime);
|
|
@@ -180,6 +171,12 @@ export class TestRunner extends BaseService {
|
|
|
180
171
|
} catch (error) {
|
|
181
172
|
// Don't fail the entire run if build finalization fails
|
|
182
173
|
this.logger.warn(`Failed to finalize build ${buildId}:`, error.message);
|
|
174
|
+
// Emit event for UI handling
|
|
175
|
+
this.emit('build-finalize-failed', {
|
|
176
|
+
buildId,
|
|
177
|
+
error: error.message,
|
|
178
|
+
stack: error.stack
|
|
179
|
+
});
|
|
183
180
|
}
|
|
184
181
|
}
|
|
185
182
|
async executeTestCommand(testCommand, env) {
|
|
@@ -11,7 +11,7 @@ import { createUploaderLogger } from '../utils/logger-factory.js';
|
|
|
11
11
|
import { ApiService } from './api-service.js';
|
|
12
12
|
import { getDefaultBranch } from '../utils/git.js';
|
|
13
13
|
import { UploadError, TimeoutError, ValidationError } from '../errors/vizzly-error.js';
|
|
14
|
-
const DEFAULT_BATCH_SIZE =
|
|
14
|
+
const DEFAULT_BATCH_SIZE = 50;
|
|
15
15
|
const DEFAULT_SHA_CHECK_BATCH_SIZE = 100;
|
|
16
16
|
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
17
17
|
|
|
@@ -50,6 +50,7 @@ export function createUploader({
|
|
|
50
50
|
message,
|
|
51
51
|
environment = 'production',
|
|
52
52
|
threshold,
|
|
53
|
+
pullRequestNumber,
|
|
53
54
|
onProgress = () => {}
|
|
54
55
|
}) {
|
|
55
56
|
try {
|
|
@@ -80,12 +81,14 @@ export function createUploader({
|
|
|
80
81
|
}
|
|
81
82
|
onProgress({
|
|
82
83
|
phase: 'scanning',
|
|
84
|
+
message: `Found ${files.length} screenshots`,
|
|
83
85
|
total: files.length
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
// Process files to get metadata
|
|
87
89
|
const fileMetadata = await processFiles(files, signal, current => onProgress({
|
|
88
90
|
phase: 'processing',
|
|
91
|
+
message: `Processing files`,
|
|
89
92
|
current,
|
|
90
93
|
total: files.length
|
|
91
94
|
}));
|
|
@@ -94,10 +97,11 @@ export function createUploader({
|
|
|
94
97
|
const buildInfo = {
|
|
95
98
|
name: buildName || `Upload ${new Date().toISOString()}`,
|
|
96
99
|
branch: branch || (await getDefaultBranch()) || 'main',
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
commit_sha: commit,
|
|
101
|
+
commit_message: message,
|
|
99
102
|
environment,
|
|
100
|
-
threshold
|
|
103
|
+
threshold,
|
|
104
|
+
github_pull_request_number: pullRequestNumber
|
|
101
105
|
};
|
|
102
106
|
const build = await api.createBuild(buildInfo);
|
|
103
107
|
const buildId = build.id;
|
|
@@ -110,6 +114,7 @@ export function createUploader({
|
|
|
110
114
|
} = await checkExistingFiles(fileMetadata, api, signal, buildId);
|
|
111
115
|
onProgress({
|
|
112
116
|
phase: 'deduplication',
|
|
117
|
+
message: `Checking for duplicates (${toUpload.length} to upload, ${existing.length} existing)`,
|
|
113
118
|
toUpload: toUpload.length,
|
|
114
119
|
existing: existing.length,
|
|
115
120
|
total: files.length
|
|
@@ -127,12 +132,14 @@ export function createUploader({
|
|
|
127
132
|
batchSize: batchSize,
|
|
128
133
|
onProgress: current => onProgress({
|
|
129
134
|
phase: 'uploading',
|
|
135
|
+
message: `Uploading screenshots`,
|
|
130
136
|
current,
|
|
131
137
|
total: toUpload.length
|
|
132
138
|
})
|
|
133
139
|
});
|
|
134
140
|
onProgress({
|
|
135
141
|
phase: 'completed',
|
|
142
|
+
message: `Upload completed`,
|
|
136
143
|
buildId: result.buildId,
|
|
137
144
|
url: result.url
|
|
138
145
|
});
|