@testrelic/playwright-analytics 2.4.10 → 2.4.11-next.12
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 +69 -1
- package/dist/api-fixture.cjs +3 -0
- package/dist/api-fixture.cjs.map +1 -0
- package/dist/api-fixture.d.cts +25 -0
- package/dist/api-fixture.d.ts +25 -0
- package/dist/api-fixture.js +3 -0
- package/dist/api-fixture.js.map +1 -0
- package/dist/cli.cjs +868 -0
- package/dist/fixture.cjs +3 -0
- package/dist/fixture.cjs.map +1 -0
- package/dist/fixture.d.cts +70 -0
- package/dist/fixture.d.ts +70 -0
- package/dist/fixture.js +3 -0
- package/dist/fixture.js.map +1 -0
- package/dist/index.cjs +2603 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +198 -0
- package/dist/index.d.ts +198 -0
- package/dist/index.js +2603 -0
- package/dist/index.js.map +1 -0
- package/dist/merge.cjs +2 -0
- package/dist/merge.cjs.map +1 -0
- package/dist/merge.d.cts +12 -0
- package/dist/merge.d.ts +12 -0
- package/dist/merge.js +2 -0
- package/dist/merge.js.map +1 -0
- package/package.json +11 -3
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/jsonl-stream.ts
|
|
14
|
+
async function readJsonlPage(filePath, page, pageSize, knownTotal) {
|
|
15
|
+
const skip = (page - 1) * pageSize;
|
|
16
|
+
const items = [];
|
|
17
|
+
let lineCount = 0;
|
|
18
|
+
const rl = (0, import_node_readline.createInterface)({
|
|
19
|
+
input: (0, import_node_fs2.createReadStream)(filePath, { encoding: "utf-8" }),
|
|
20
|
+
crlfDelay: Infinity
|
|
21
|
+
});
|
|
22
|
+
for await (const line of rl) {
|
|
23
|
+
if (line.length === 0) continue;
|
|
24
|
+
if (lineCount >= skip && items.length < pageSize) {
|
|
25
|
+
try {
|
|
26
|
+
items.push(JSON.parse(line));
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
lineCount++;
|
|
31
|
+
if (items.length >= pageSize && knownTotal !== void 0) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const total = knownTotal ?? lineCount;
|
|
36
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
37
|
+
return { items, total, page, pageSize, totalPages };
|
|
38
|
+
}
|
|
39
|
+
var import_node_fs2, import_node_path2, import_node_os, import_node_crypto2, import_node_readline, TESTRELIC_TMP_DIR;
|
|
40
|
+
var init_jsonl_stream = __esm({
|
|
41
|
+
"src/jsonl-stream.ts"() {
|
|
42
|
+
"use strict";
|
|
43
|
+
import_node_fs2 = require("fs");
|
|
44
|
+
import_node_path2 = require("path");
|
|
45
|
+
import_node_os = require("os");
|
|
46
|
+
import_node_crypto2 = require("crypto");
|
|
47
|
+
import_node_readline = require("readline");
|
|
48
|
+
TESTRELIC_TMP_DIR = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), "testrelic-data");
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// src/report-server-routes.ts
|
|
53
|
+
function sendJson(res, status, body) {
|
|
54
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
55
|
+
res.end(JSON.stringify(body));
|
|
56
|
+
}
|
|
57
|
+
function setCorsHeaders(res) {
|
|
58
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
59
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS");
|
|
60
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
61
|
+
}
|
|
62
|
+
function readJsonFile(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
if (!(0, import_node_fs3.existsSync)(filePath)) return null;
|
|
65
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf-8"));
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function getDirSize(dirPath) {
|
|
71
|
+
let size = 0;
|
|
72
|
+
try {
|
|
73
|
+
const entries = (0, import_node_fs3.readdirSync)(dirPath, { withFileTypes: true });
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const fullPath = (0, import_node_path3.join)(dirPath, entry.name);
|
|
76
|
+
if (entry.isFile()) {
|
|
77
|
+
size += (0, import_node_fs3.statSync)(fullPath).size;
|
|
78
|
+
} else if (entry.isDirectory()) {
|
|
79
|
+
size += getDirSize(fullPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
return size;
|
|
85
|
+
}
|
|
86
|
+
function handleHealth(_req, res, reportDir, startTime) {
|
|
87
|
+
const index = readJsonFile((0, import_node_path3.join)(reportDir, "index.json"));
|
|
88
|
+
sendJson(res, 200, {
|
|
89
|
+
status: "ok",
|
|
90
|
+
reportMode: "streaming",
|
|
91
|
+
testCount: index?.length ?? 0,
|
|
92
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function handleSummary(_req, res, reportDir) {
|
|
96
|
+
const summary = readJsonFile((0, import_node_path3.join)(reportDir, "summary.json"));
|
|
97
|
+
if (!summary) {
|
|
98
|
+
sendJson(res, 404, { error: "Summary not found" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
sendJson(res, 200, summary);
|
|
102
|
+
}
|
|
103
|
+
function handleTests(req, res, reportDir) {
|
|
104
|
+
const index = readJsonFile((0, import_node_path3.join)(reportDir, "index.json"));
|
|
105
|
+
if (!index) {
|
|
106
|
+
sendJson(res, 404, { error: "Test index not found" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
110
|
+
const params = url.searchParams;
|
|
111
|
+
const page = Math.max(1, parseInt(params.get("page") ?? "1", 10) || 1);
|
|
112
|
+
const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, parseInt(params.get("pageSize") ?? "100", 10) || 100));
|
|
113
|
+
const statusFilter = params.get("status")?.split(",").filter(Boolean) ?? null;
|
|
114
|
+
const fileFilter = params.get("file") ?? null;
|
|
115
|
+
const searchQuery = params.get("search")?.toLowerCase() ?? null;
|
|
116
|
+
const tagFilter = params.get("tag")?.split(",").filter(Boolean) ?? null;
|
|
117
|
+
const sortField = params.get("sort") ?? "file";
|
|
118
|
+
const sortOrder = params.get("order") === "desc" ? -1 : 1;
|
|
119
|
+
let filtered = index;
|
|
120
|
+
if (statusFilter && statusFilter.length > 0) {
|
|
121
|
+
filtered = filtered.filter((t) => statusFilter.includes(t.status));
|
|
122
|
+
}
|
|
123
|
+
if (fileFilter) {
|
|
124
|
+
filtered = filtered.filter((t) => t.filePath === fileFilter || t.filePath.startsWith(fileFilter + "/"));
|
|
125
|
+
}
|
|
126
|
+
if (searchQuery) {
|
|
127
|
+
filtered = filtered.filter(
|
|
128
|
+
(t) => t.title.toLowerCase().includes(searchQuery) || t.filePath.toLowerCase().includes(searchQuery)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (tagFilter && tagFilter.length > 0) {
|
|
132
|
+
filtered = filtered.filter((t) => tagFilter.some((tag) => t.tags.includes(tag)));
|
|
133
|
+
}
|
|
134
|
+
filtered = [...filtered].sort((a, b) => {
|
|
135
|
+
let cmp = 0;
|
|
136
|
+
switch (sortField) {
|
|
137
|
+
case "duration":
|
|
138
|
+
cmp = a.duration - b.duration;
|
|
139
|
+
break;
|
|
140
|
+
case "status":
|
|
141
|
+
cmp = a.status.localeCompare(b.status);
|
|
142
|
+
break;
|
|
143
|
+
case "title":
|
|
144
|
+
cmp = a.title.localeCompare(b.title);
|
|
145
|
+
break;
|
|
146
|
+
case "file":
|
|
147
|
+
default:
|
|
148
|
+
cmp = a.filePath.localeCompare(b.filePath);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
return cmp * sortOrder;
|
|
152
|
+
});
|
|
153
|
+
const totalItems = filtered.length;
|
|
154
|
+
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
|
|
155
|
+
const startIdx = (page - 1) * pageSize;
|
|
156
|
+
const tests = filtered.slice(startIdx, startIdx + pageSize);
|
|
157
|
+
sendJson(res, 200, {
|
|
158
|
+
tests,
|
|
159
|
+
pagination: { page, pageSize, totalItems, totalPages },
|
|
160
|
+
filters: {
|
|
161
|
+
status: statusFilter,
|
|
162
|
+
file: fileFilter,
|
|
163
|
+
search: searchQuery,
|
|
164
|
+
tag: tagFilter
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function handleTestDetail(_req, res, reportDir, testId) {
|
|
169
|
+
if (!SAFE_ID_PATTERN.test(testId)) {
|
|
170
|
+
sendJson(res, 400, { error: "Invalid test ID format" });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const metaPath = (0, import_node_path3.join)(reportDir, "tests", testId, "meta.json");
|
|
174
|
+
const legacyPath = (0, import_node_path3.join)(reportDir, "tests", `${testId}.json`);
|
|
175
|
+
const filePath = (0, import_node_fs3.existsSync)(metaPath) ? metaPath : (0, import_node_fs3.existsSync)(legacyPath) ? legacyPath : null;
|
|
176
|
+
if (!filePath) {
|
|
177
|
+
sendJson(res, 404, { error: `Test not found: ${testId}` });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const data = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
182
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
183
|
+
res.end(data);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function handleTestDataFile(req, res, reportDir, testId, dataType) {
|
|
189
|
+
if (!SAFE_ID_PATTERN.test(testId)) {
|
|
190
|
+
sendJson(res, 400, { error: "Invalid test ID format" });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const fileNames = {
|
|
194
|
+
"network": "network.jsonl",
|
|
195
|
+
"console": "console.jsonl",
|
|
196
|
+
"api-calls": "api-calls.jsonl"
|
|
197
|
+
};
|
|
198
|
+
const jsonlPath = (0, import_node_path3.join)(reportDir, "tests", testId, fileNames[dataType]);
|
|
199
|
+
if (!(0, import_node_fs3.existsSync)(jsonlPath)) {
|
|
200
|
+
sendJson(res, 200, { items: [], total: 0, page: 1, pageSize: 50, totalPages: 0 });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
205
|
+
const params = url.searchParams;
|
|
206
|
+
const page = Math.max(1, parseInt(params.get("page") ?? "1", 10) || 1);
|
|
207
|
+
const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, parseInt(params.get("pageSize") ?? "50", 10) || 50));
|
|
208
|
+
let knownTotal;
|
|
209
|
+
const metaPath = (0, import_node_path3.join)(reportDir, "tests", testId, "meta.json");
|
|
210
|
+
if ((0, import_node_fs3.existsSync)(metaPath)) {
|
|
211
|
+
try {
|
|
212
|
+
const meta = JSON.parse((0, import_node_fs3.readFileSync)(metaPath, "utf-8"));
|
|
213
|
+
switch (dataType) {
|
|
214
|
+
case "network":
|
|
215
|
+
knownTotal = meta.networkRequestsCount;
|
|
216
|
+
break;
|
|
217
|
+
case "console":
|
|
218
|
+
knownTotal = meta.consoleLogsCount;
|
|
219
|
+
break;
|
|
220
|
+
case "api-calls":
|
|
221
|
+
knownTotal = meta.apiCallsCount;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const result = await readJsonlPage(jsonlPath, page, pageSize, knownTotal);
|
|
228
|
+
sendJson(res, 200, result);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function handleFiles(_req, res, reportDir) {
|
|
234
|
+
const index = readJsonFile((0, import_node_path3.join)(reportDir, "index.json"));
|
|
235
|
+
if (!index) {
|
|
236
|
+
sendJson(res, 404, { error: "Test index not found" });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const t of index) {
|
|
241
|
+
if (t.isRetry) continue;
|
|
242
|
+
let stats = fileMap.get(t.filePath);
|
|
243
|
+
if (!stats) {
|
|
244
|
+
stats = { total: 0, passed: 0, failed: 0, flaky: 0, skipped: 0, timedOut: 0 };
|
|
245
|
+
fileMap.set(t.filePath, stats);
|
|
246
|
+
}
|
|
247
|
+
stats.total++;
|
|
248
|
+
switch (t.status) {
|
|
249
|
+
case "passed":
|
|
250
|
+
stats.passed++;
|
|
251
|
+
break;
|
|
252
|
+
case "failed":
|
|
253
|
+
stats.failed++;
|
|
254
|
+
break;
|
|
255
|
+
case "flaky":
|
|
256
|
+
stats.flaky++;
|
|
257
|
+
break;
|
|
258
|
+
case "skipped":
|
|
259
|
+
stats.skipped++;
|
|
260
|
+
break;
|
|
261
|
+
case "timedout":
|
|
262
|
+
stats.timedOut++;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const files = Array.from(fileMap.entries()).map(([filePath, s]) => ({ filePath, ...s })).sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
267
|
+
sendJson(res, 200, { files });
|
|
268
|
+
}
|
|
269
|
+
function handleArtifactsList(_req, res, artifactsDir) {
|
|
270
|
+
if (!(0, import_node_fs3.existsSync)(artifactsDir)) {
|
|
271
|
+
sendJson(res, 200, { runs: [], totalSizeBytes: 0 });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const runs = [];
|
|
276
|
+
let totalSizeBytes = 0;
|
|
277
|
+
const entries = (0, import_node_fs3.readdirSync)(artifactsDir, { withFileTypes: true });
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
if (!entry.isDirectory() || !TIMESTAMP_PATTERN.test(entry.name)) continue;
|
|
280
|
+
const runDir = (0, import_node_path3.join)(artifactsDir, entry.name);
|
|
281
|
+
const size = getDirSize(runDir);
|
|
282
|
+
const testDirs = (0, import_node_fs3.readdirSync)(runDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
283
|
+
runs.push({ folderName: entry.name, totalSizeBytes: size, testCount: testDirs.length });
|
|
284
|
+
totalSizeBytes += size;
|
|
285
|
+
}
|
|
286
|
+
runs.sort((a, b) => b.folderName.localeCompare(a.folderName));
|
|
287
|
+
sendJson(res, 200, { runs, totalSizeBytes });
|
|
288
|
+
} catch (err) {
|
|
289
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function handleArtifactsDeleteAll(_req, res, artifactsDir) {
|
|
293
|
+
try {
|
|
294
|
+
let deletedCount = 0;
|
|
295
|
+
let freedBytes = 0;
|
|
296
|
+
if ((0, import_node_fs3.existsSync)(artifactsDir)) {
|
|
297
|
+
const entries = (0, import_node_fs3.readdirSync)(artifactsDir, { withFileTypes: true });
|
|
298
|
+
for (const entry of entries) {
|
|
299
|
+
if (!entry.isDirectory() || !TIMESTAMP_PATTERN.test(entry.name)) continue;
|
|
300
|
+
const runDir = (0, import_node_path3.join)(artifactsDir, entry.name);
|
|
301
|
+
const size = getDirSize(runDir);
|
|
302
|
+
(0, import_node_fs3.rmSync)(runDir, { recursive: true, force: true });
|
|
303
|
+
freedBytes += size;
|
|
304
|
+
deletedCount++;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
sendJson(res, 200, { deletedCount, freedBytes });
|
|
308
|
+
} catch (err) {
|
|
309
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function handleArtifactDelete(_req, res, artifactsDir, folderName) {
|
|
313
|
+
if (!TIMESTAMP_PATTERN.test(folderName)) {
|
|
314
|
+
sendJson(res, 400, { error: "Invalid folder name" });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const runDir = (0, import_node_path3.join)(artifactsDir, folderName);
|
|
318
|
+
try {
|
|
319
|
+
const stat = (0, import_node_fs3.statSync)(runDir);
|
|
320
|
+
if (!stat.isDirectory()) {
|
|
321
|
+
sendJson(res, 404, { error: "Not found" });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
sendJson(res, 404, { error: "Not found" });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const freedBytes = getDirSize(runDir);
|
|
330
|
+
(0, import_node_fs3.rmSync)(runDir, { recursive: true, force: true });
|
|
331
|
+
sendJson(res, 200, { deleted: folderName, freedBytes });
|
|
332
|
+
} catch (err) {
|
|
333
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function handleShutdown(_req, res, server) {
|
|
337
|
+
sendJson(res, 200, { status: "shutting_down" });
|
|
338
|
+
server.close();
|
|
339
|
+
}
|
|
340
|
+
function serveStaticFile(_req, res, filePath) {
|
|
341
|
+
if (!(0, import_node_fs3.existsSync)(filePath)) {
|
|
342
|
+
sendJson(res, 404, { error: "File not found" });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const data = (0, import_node_fs3.readFileSync)(filePath);
|
|
347
|
+
const ext = (0, import_node_path3.extname)(filePath).toLowerCase();
|
|
348
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
349
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
350
|
+
res.end(data);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
var import_node_fs3, import_node_path3, SAFE_ID_PATTERN, TIMESTAMP_PATTERN, MAX_PAGE_SIZE, MIME_TYPES;
|
|
356
|
+
var init_report_server_routes = __esm({
|
|
357
|
+
"src/report-server-routes.ts"() {
|
|
358
|
+
"use strict";
|
|
359
|
+
import_node_fs3 = require("fs");
|
|
360
|
+
import_node_path3 = require("path");
|
|
361
|
+
init_jsonl_stream();
|
|
362
|
+
SAFE_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
363
|
+
TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/;
|
|
364
|
+
MAX_PAGE_SIZE = 500;
|
|
365
|
+
MIME_TYPES = {
|
|
366
|
+
".png": "image/png",
|
|
367
|
+
".jpg": "image/jpeg",
|
|
368
|
+
".jpeg": "image/jpeg",
|
|
369
|
+
".gif": "image/gif",
|
|
370
|
+
".webp": "image/webp",
|
|
371
|
+
".webm": "video/webm",
|
|
372
|
+
".mp4": "video/mp4",
|
|
373
|
+
".json": "application/json",
|
|
374
|
+
".html": "text/html",
|
|
375
|
+
".css": "text/css",
|
|
376
|
+
".js": "text/javascript",
|
|
377
|
+
".svg": "image/svg+xml"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// src/report-server.ts
|
|
383
|
+
var report_server_exports = {};
|
|
384
|
+
__export(report_server_exports, {
|
|
385
|
+
startReportServer: () => startReportServer
|
|
386
|
+
});
|
|
387
|
+
function startReportServer(reportDir, options) {
|
|
388
|
+
return new Promise((resolve2, reject) => {
|
|
389
|
+
const startPort = options?.port ?? DEFAULT_PORT;
|
|
390
|
+
const startTime = Date.now();
|
|
391
|
+
let inactivityTimer;
|
|
392
|
+
let currentAttempt = 0;
|
|
393
|
+
const artifactsDir = (0, import_node_fs4.existsSync)((0, import_node_path4.join)(reportDir, "artifacts")) ? (0, import_node_path4.join)(reportDir, "artifacts") : (0, import_node_fs4.existsSync)((0, import_node_path4.join)(reportDir, "..", "artifacts")) ? (0, import_node_path4.join)(reportDir, "..", "artifacts") : (0, import_node_path4.join)(reportDir, "artifacts");
|
|
394
|
+
function resetTimer() {
|
|
395
|
+
clearTimeout(inactivityTimer);
|
|
396
|
+
inactivityTimer = setTimeout(() => {
|
|
397
|
+
server.close();
|
|
398
|
+
}, INACTIVITY_TIMEOUT_MS);
|
|
399
|
+
}
|
|
400
|
+
let htmlReportPath = options?.htmlPath ?? null;
|
|
401
|
+
if (!htmlReportPath) {
|
|
402
|
+
const parentDir = (0, import_node_path4.dirname)(reportDir);
|
|
403
|
+
try {
|
|
404
|
+
const htmlFile = (0, import_node_fs4.readdirSync)(parentDir).find((f) => f.endsWith(".html"));
|
|
405
|
+
if (htmlFile) htmlReportPath = (0, import_node_path4.join)(parentDir, htmlFile);
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const server = (0, import_node_http.createServer)((req, res) => {
|
|
410
|
+
resetTimer();
|
|
411
|
+
setCorsHeaders(res);
|
|
412
|
+
if (req.method === "OPTIONS") {
|
|
413
|
+
res.writeHead(204);
|
|
414
|
+
res.end();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
let pathname;
|
|
418
|
+
try {
|
|
419
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
420
|
+
pathname = url.pathname;
|
|
421
|
+
} catch {
|
|
422
|
+
sendJson(res, 400, { error: "Invalid URL" });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
|
|
426
|
+
if (htmlReportPath && (0, import_node_fs4.existsSync)(htmlReportPath)) {
|
|
427
|
+
serveStaticFile(req, res, htmlReportPath);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
sendJson(res, 404, { error: "HTML report not found" });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (req.method === "GET" && pathname === "/api/health") {
|
|
434
|
+
handleHealth(req, res, reportDir, startTime);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (req.method === "GET" && pathname === "/api/summary") {
|
|
438
|
+
handleSummary(req, res, reportDir);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (req.method === "GET" && pathname === "/api/tests") {
|
|
442
|
+
handleTests(req, res, reportDir);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (req.method === "GET" && pathname === "/api/files") {
|
|
446
|
+
handleFiles(req, res, reportDir);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const testDataMatch = pathname.match(/^\/api\/tests\/([a-f0-9]+)\/(network|console|api-calls)$/);
|
|
450
|
+
if (req.method === "GET" && testDataMatch) {
|
|
451
|
+
handleTestDataFile(req, res, reportDir, testDataMatch[1], testDataMatch[2]);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const testMatch = pathname.match(/^\/api\/tests\/([a-f0-9]+)$/);
|
|
455
|
+
if (req.method === "GET" && testMatch) {
|
|
456
|
+
handleTestDetail(req, res, reportDir, testMatch[1]);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (req.method === "GET" && pathname === "/api/artifacts") {
|
|
460
|
+
handleArtifactsList(req, res, artifactsDir);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (req.method === "DELETE" && pathname === "/api/artifacts") {
|
|
464
|
+
handleArtifactsDeleteAll(req, res, artifactsDir);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const deleteMatch = pathname.match(/^\/api\/artifacts\/(.+)$/);
|
|
468
|
+
if (req.method === "DELETE" && deleteMatch) {
|
|
469
|
+
handleArtifactDelete(req, res, artifactsDir, decodeURIComponent(deleteMatch[1]));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (req.method === "GET" && pathname.startsWith("/artifacts/")) {
|
|
473
|
+
const relPath = decodeURIComponent(pathname.slice("/artifacts/".length));
|
|
474
|
+
if (relPath.includes("..") || relPath.includes("\0")) {
|
|
475
|
+
sendJson(res, 400, { error: "Invalid path" });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
serveStaticFile(req, res, (0, import_node_path4.join)(artifactsDir, relPath));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (req.method === "POST" && pathname === "/api/shutdown") {
|
|
482
|
+
handleShutdown(req, res, server);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
sendJson(res, 404, { error: "Not found" });
|
|
486
|
+
});
|
|
487
|
+
function tryListen(port) {
|
|
488
|
+
const errorHandler = (err) => {
|
|
489
|
+
if (err.code === "EADDRINUSE" && currentAttempt < MAX_PORT_ATTEMPTS) {
|
|
490
|
+
currentAttempt++;
|
|
491
|
+
tryListen(port + 1);
|
|
492
|
+
} else {
|
|
493
|
+
reject(err);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
server.once("error", errorHandler);
|
|
497
|
+
server.listen(port, "127.0.0.1", () => {
|
|
498
|
+
server.removeListener("error", errorHandler);
|
|
499
|
+
const addr = server.address();
|
|
500
|
+
if (!addr || typeof addr === "string") {
|
|
501
|
+
reject(new Error("Failed to get server address"));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
resetTimer();
|
|
505
|
+
resolve2({
|
|
506
|
+
port: addr.port,
|
|
507
|
+
dispose: () => new Promise((res) => {
|
|
508
|
+
clearTimeout(inactivityTimer);
|
|
509
|
+
server.close(() => res());
|
|
510
|
+
})
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
tryListen(startPort);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
var import_node_http, import_node_path4, import_node_fs4, DEFAULT_PORT, MAX_PORT_ATTEMPTS, INACTIVITY_TIMEOUT_MS;
|
|
518
|
+
var init_report_server = __esm({
|
|
519
|
+
"src/report-server.ts"() {
|
|
520
|
+
"use strict";
|
|
521
|
+
import_node_http = require("http");
|
|
522
|
+
import_node_path4 = require("path");
|
|
523
|
+
import_node_fs4 = require("fs");
|
|
524
|
+
init_report_server_routes();
|
|
525
|
+
DEFAULT_PORT = 9323;
|
|
526
|
+
MAX_PORT_ATTEMPTS = 10;
|
|
527
|
+
INACTIVITY_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// src/browser-open.ts
|
|
532
|
+
var browser_open_exports = {};
|
|
533
|
+
__export(browser_open_exports, {
|
|
534
|
+
openInBrowser: () => openInBrowser
|
|
535
|
+
});
|
|
536
|
+
function openInBrowser(filePath) {
|
|
537
|
+
try {
|
|
538
|
+
const platform = process.platform;
|
|
539
|
+
let command;
|
|
540
|
+
if (platform === "darwin") {
|
|
541
|
+
command = `open "${filePath}"`;
|
|
542
|
+
} else if (platform === "win32") {
|
|
543
|
+
command = `start "" "${filePath}"`;
|
|
544
|
+
} else {
|
|
545
|
+
command = `xdg-open "${filePath}"`;
|
|
546
|
+
}
|
|
547
|
+
(0, import_node_child_process.exec)(command, (err) => {
|
|
548
|
+
if (err) {
|
|
549
|
+
process.stderr.write(
|
|
550
|
+
`[testrelic] Failed to open browser: ${err.message}
|
|
551
|
+
`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
var import_node_child_process;
|
|
559
|
+
var init_browser_open = __esm({
|
|
560
|
+
"src/browser-open.ts"() {
|
|
561
|
+
"use strict";
|
|
562
|
+
import_node_child_process = require("child_process");
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// src/merge.ts
|
|
567
|
+
var import_node_crypto = require("crypto");
|
|
568
|
+
var import_node_fs = require("fs");
|
|
569
|
+
var import_node_path = require("path");
|
|
570
|
+
var import_core = require("@testrelic/core");
|
|
571
|
+
function isStreamingDir(path) {
|
|
572
|
+
try {
|
|
573
|
+
const stat = (0, import_node_fs.statSync)(path);
|
|
574
|
+
return stat.isDirectory() && (0, import_node_fs.existsSync)((0, import_node_path.join)(path, "index.json"));
|
|
575
|
+
} catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function loadStreamingReport(dir) {
|
|
580
|
+
const summaryPath = (0, import_node_path.join)(dir, "summary.json");
|
|
581
|
+
const indexPath = (0, import_node_path.join)(dir, "index.json");
|
|
582
|
+
let summary;
|
|
583
|
+
try {
|
|
584
|
+
summary = JSON.parse((0, import_node_fs.readFileSync)(summaryPath, "utf-8"));
|
|
585
|
+
} catch (err) {
|
|
586
|
+
throw (0, import_core.createError)(import_core.ErrorCode.MERGE_READ_FAILED, `Failed to read summary.json from: ${dir}`, err);
|
|
587
|
+
}
|
|
588
|
+
let index;
|
|
589
|
+
try {
|
|
590
|
+
index = JSON.parse((0, import_node_fs.readFileSync)(indexPath, "utf-8"));
|
|
591
|
+
} catch (err) {
|
|
592
|
+
throw (0, import_core.createError)(import_core.ErrorCode.MERGE_READ_FAILED, `Failed to read index.json from: ${dir}`, err);
|
|
593
|
+
}
|
|
594
|
+
return { summary, index, dir };
|
|
595
|
+
}
|
|
596
|
+
async function mergeReports(files, options) {
|
|
597
|
+
const reports = [];
|
|
598
|
+
const streamingReports = [];
|
|
599
|
+
for (const file of files) {
|
|
600
|
+
if (isStreamingDir(file)) {
|
|
601
|
+
streamingReports.push(loadStreamingReport(file));
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
let raw;
|
|
605
|
+
try {
|
|
606
|
+
raw = (0, import_node_fs.readFileSync)(file, "utf-8");
|
|
607
|
+
} catch (err) {
|
|
608
|
+
throw (0, import_core.createError)(
|
|
609
|
+
import_core.ErrorCode.MERGE_READ_FAILED,
|
|
610
|
+
`Failed to read file: ${file}`,
|
|
611
|
+
err
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
let parsed;
|
|
615
|
+
try {
|
|
616
|
+
parsed = JSON.parse(raw);
|
|
617
|
+
} catch (err) {
|
|
618
|
+
throw (0, import_core.createError)(
|
|
619
|
+
import_core.ErrorCode.MERGE_INVALID_SCHEMA,
|
|
620
|
+
`Invalid JSON in file: ${file}`,
|
|
621
|
+
err
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (!(0, import_core.isValidTestRunReport)(parsed)) {
|
|
625
|
+
throw (0, import_core.createError)(
|
|
626
|
+
import_core.ErrorCode.MERGE_INVALID_SCHEMA,
|
|
627
|
+
`Invalid report schema in file: ${file}`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
reports.push(parsed);
|
|
631
|
+
}
|
|
632
|
+
const shardRunIds = reports.map((r) => r.testRunId);
|
|
633
|
+
const allTimelines = [];
|
|
634
|
+
for (const report of reports) {
|
|
635
|
+
allTimelines.push(...report.timeline);
|
|
636
|
+
}
|
|
637
|
+
allTimelines.sort((a, b) => {
|
|
638
|
+
const timeA = "visitedAt" in a ? a.visitedAt : a.timestamp;
|
|
639
|
+
const timeB = "visitedAt" in b ? b.visitedAt : b.timestamp;
|
|
640
|
+
return new Date(timeA).getTime() - new Date(timeB).getTime();
|
|
641
|
+
});
|
|
642
|
+
if (streamingReports.length > 0) {
|
|
643
|
+
const outputDir = (0, import_node_path.dirname)(options.output);
|
|
644
|
+
const mergedReportDir = (0, import_node_path.join)(outputDir, ".testrelic-report");
|
|
645
|
+
const mergedTestsDir = (0, import_node_path.join)(mergedReportDir, "tests");
|
|
646
|
+
(0, import_node_fs.mkdirSync)(mergedTestsDir, { recursive: true });
|
|
647
|
+
const seenTestIds = /* @__PURE__ */ new Set();
|
|
648
|
+
const mergedIndex = [];
|
|
649
|
+
for (const sr of streamingReports) {
|
|
650
|
+
const srcTestsDir = (0, import_node_path.join)(sr.dir, "tests");
|
|
651
|
+
if ((0, import_node_fs.existsSync)(srcTestsDir)) {
|
|
652
|
+
const testFiles = (0, import_node_fs.readdirSync)(srcTestsDir);
|
|
653
|
+
for (const f of testFiles) {
|
|
654
|
+
if (f.endsWith(".json")) {
|
|
655
|
+
(0, import_node_fs.cpSync)((0, import_node_path.join)(srcTestsDir, f), (0, import_node_path.join)(mergedTestsDir, f));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
for (const entry of sr.index) {
|
|
660
|
+
if (!seenTestIds.has(entry.id)) {
|
|
661
|
+
seenTestIds.add(entry.id);
|
|
662
|
+
mergedIndex.push(entry);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(mergedReportDir, "index.json"), JSON.stringify(mergedIndex), "utf-8");
|
|
667
|
+
}
|
|
668
|
+
const allSummaries = [
|
|
669
|
+
...reports.map((r) => r.summary),
|
|
670
|
+
...streamingReports.map((sr) => sr.summary)
|
|
671
|
+
];
|
|
672
|
+
const summary = recalculateSummaryFromList(allSummaries, allTimelines.length);
|
|
673
|
+
const startedAt = reports.reduce(
|
|
674
|
+
(earliest, r) => r.startedAt < earliest ? r.startedAt : earliest,
|
|
675
|
+
reports[0]?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
676
|
+
);
|
|
677
|
+
const completedAt = reports.reduce(
|
|
678
|
+
(latest, r) => r.completedAt > latest ? r.completedAt : latest,
|
|
679
|
+
reports[0]?.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
680
|
+
);
|
|
681
|
+
const totalDuration = new Date(completedAt).getTime() - new Date(startedAt).getTime();
|
|
682
|
+
const merged = {
|
|
683
|
+
schemaVersion: reports[0]?.schemaVersion ?? "1.0.0",
|
|
684
|
+
testRunId: options.testRunId ?? (0, import_node_crypto.randomUUID)(),
|
|
685
|
+
startedAt,
|
|
686
|
+
completedAt,
|
|
687
|
+
totalDuration,
|
|
688
|
+
summary,
|
|
689
|
+
ci: reports.find((r) => r.ci !== null)?.ci ?? null,
|
|
690
|
+
metadata: reports.find((r) => r.metadata !== null)?.metadata ?? null,
|
|
691
|
+
timeline: allTimelines,
|
|
692
|
+
shardRunIds
|
|
693
|
+
};
|
|
694
|
+
const dir = (0, import_node_path.dirname)(options.output);
|
|
695
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
696
|
+
(0, import_node_fs.writeFileSync)(options.output, JSON.stringify(merged, null, 2), "utf-8");
|
|
697
|
+
return merged;
|
|
698
|
+
}
|
|
699
|
+
function recalculateSummaryFromList(summaries, timelineLength) {
|
|
700
|
+
let total = 0;
|
|
701
|
+
let passed = 0;
|
|
702
|
+
let failed = 0;
|
|
703
|
+
let flaky = 0;
|
|
704
|
+
let skipped = 0;
|
|
705
|
+
let timedout = 0;
|
|
706
|
+
let totalApiCalls = 0;
|
|
707
|
+
let totalAssertions = 0;
|
|
708
|
+
let passedAssertions = 0;
|
|
709
|
+
let failedAssertions = 0;
|
|
710
|
+
let totalNavigations = 0;
|
|
711
|
+
for (const s of summaries) {
|
|
712
|
+
total += s.total;
|
|
713
|
+
passed += s.passed;
|
|
714
|
+
failed += s.failed;
|
|
715
|
+
flaky += s.flaky;
|
|
716
|
+
skipped += s.skipped;
|
|
717
|
+
timedout += s.timedout ?? 0;
|
|
718
|
+
totalApiCalls += s.totalApiCalls ?? 0;
|
|
719
|
+
totalAssertions += s.totalAssertions ?? 0;
|
|
720
|
+
passedAssertions += s.passedAssertions ?? 0;
|
|
721
|
+
failedAssertions += s.failedAssertions ?? 0;
|
|
722
|
+
totalNavigations += s.totalNavigations ?? 0;
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
total,
|
|
726
|
+
passed,
|
|
727
|
+
failed,
|
|
728
|
+
flaky,
|
|
729
|
+
skipped,
|
|
730
|
+
timedout,
|
|
731
|
+
totalApiCalls,
|
|
732
|
+
uniqueApiUrls: 0,
|
|
733
|
+
apiCallsByMethod: {},
|
|
734
|
+
apiCallsByStatusRange: { "2xx": 0, "3xx": 0, "4xx": 0, "5xx": 0, error: 0 },
|
|
735
|
+
apiResponseTime: null,
|
|
736
|
+
totalAssertions,
|
|
737
|
+
passedAssertions,
|
|
738
|
+
failedAssertions,
|
|
739
|
+
totalNavigations,
|
|
740
|
+
uniqueNavigationUrls: 0,
|
|
741
|
+
totalTimelineSteps: timelineLength,
|
|
742
|
+
totalActionSteps: 0,
|
|
743
|
+
actionStepsByCategory: {}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/cli.ts
|
|
748
|
+
var import_node_path5 = require("path");
|
|
749
|
+
var import_node_fs5 = require("fs");
|
|
750
|
+
async function main() {
|
|
751
|
+
const args = process.argv.slice(2);
|
|
752
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
753
|
+
printUsage();
|
|
754
|
+
process.exit(0);
|
|
755
|
+
}
|
|
756
|
+
const command = args[0];
|
|
757
|
+
if (command === "merge") {
|
|
758
|
+
await handleMerge(args.slice(1));
|
|
759
|
+
} else if (command === "serve" || command === "open") {
|
|
760
|
+
await handleServe(args.slice(1));
|
|
761
|
+
} else {
|
|
762
|
+
process.stderr.write(`Unknown command: ${command}
|
|
763
|
+
`);
|
|
764
|
+
printUsage();
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async function handleMerge(mergeArgs) {
|
|
769
|
+
const outputIndex = mergeArgs.findIndex((a) => a === "-o" || a === "--output");
|
|
770
|
+
if (outputIndex === -1 || outputIndex === mergeArgs.length - 1) {
|
|
771
|
+
process.stderr.write("Error: -o <output> is required\n");
|
|
772
|
+
printUsage();
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
const output = mergeArgs[outputIndex + 1];
|
|
776
|
+
const files = [
|
|
777
|
+
...mergeArgs.slice(0, outputIndex),
|
|
778
|
+
...mergeArgs.slice(outputIndex + 2)
|
|
779
|
+
];
|
|
780
|
+
if (files.length === 0) {
|
|
781
|
+
process.stderr.write("Error: No input files specified\n");
|
|
782
|
+
printUsage();
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
const merged = await mergeReports(files, { output });
|
|
787
|
+
process.stderr.write(
|
|
788
|
+
`Merged ${files.length} reports (${merged.summary.total} tests) \u2192 ${output}
|
|
789
|
+
`
|
|
790
|
+
);
|
|
791
|
+
} catch (err) {
|
|
792
|
+
process.stderr.write(
|
|
793
|
+
`Error: ${err instanceof Error ? err.message : String(err)}
|
|
794
|
+
`
|
|
795
|
+
);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function handleServe(serveArgs) {
|
|
800
|
+
const portIndex = serveArgs.findIndex((a) => a === "--port" || a === "-p");
|
|
801
|
+
let port;
|
|
802
|
+
let dir;
|
|
803
|
+
if (portIndex !== -1 && portIndex < serveArgs.length - 1) {
|
|
804
|
+
port = parseInt(serveArgs[portIndex + 1], 10);
|
|
805
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
806
|
+
process.stderr.write("Error: Invalid port number\n");
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
const rest = [...serveArgs.slice(0, portIndex), ...serveArgs.slice(portIndex + 2)];
|
|
810
|
+
dir = rest[0];
|
|
811
|
+
} else {
|
|
812
|
+
dir = serveArgs[0];
|
|
813
|
+
}
|
|
814
|
+
if (!dir) {
|
|
815
|
+
process.stderr.write("Error: Report directory is required\n");
|
|
816
|
+
process.stderr.write("Usage: testrelic serve <dir> [--port <port>]\n");
|
|
817
|
+
process.exit(1);
|
|
818
|
+
}
|
|
819
|
+
const reportDir = (0, import_node_path5.resolve)(dir);
|
|
820
|
+
if (!(0, import_node_fs5.existsSync)(reportDir)) {
|
|
821
|
+
process.stderr.write(`Error: Directory not found: ${reportDir}
|
|
822
|
+
`);
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
try {
|
|
826
|
+
const { startReportServer: startReportServer2 } = await Promise.resolve().then(() => (init_report_server(), report_server_exports));
|
|
827
|
+
const { openInBrowser: openInBrowser2 } = await Promise.resolve().then(() => (init_browser_open(), browser_open_exports));
|
|
828
|
+
const handle = await startReportServer2(reportDir, port ? { port } : void 0);
|
|
829
|
+
const url = `http://127.0.0.1:${handle.port}`;
|
|
830
|
+
process.stderr.write(`
|
|
831
|
+
Report server running at: ${url}
|
|
832
|
+
Press Ctrl+C to stop
|
|
833
|
+
|
|
834
|
+
`);
|
|
835
|
+
openInBrowser2(url);
|
|
836
|
+
process.on("SIGINT", () => {
|
|
837
|
+
handle.dispose();
|
|
838
|
+
process.exit(0);
|
|
839
|
+
});
|
|
840
|
+
process.on("SIGTERM", () => {
|
|
841
|
+
handle.dispose();
|
|
842
|
+
process.exit(0);
|
|
843
|
+
});
|
|
844
|
+
} catch (err) {
|
|
845
|
+
process.stderr.write(
|
|
846
|
+
`Error: ${err instanceof Error ? err.message : String(err)}
|
|
847
|
+
`
|
|
848
|
+
);
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function printUsage() {
|
|
853
|
+
process.stderr.write(
|
|
854
|
+
`Usage: testrelic <command>
|
|
855
|
+
|
|
856
|
+
Commands:
|
|
857
|
+
merge <files...> -o <output> Merge multiple shard report JSON files
|
|
858
|
+
serve <dir> [--port <port>] Start a local server for a streaming report
|
|
859
|
+
open <dir> [--port <port>] Alias for serve
|
|
860
|
+
|
|
861
|
+
Examples:
|
|
862
|
+
npx testrelic merge shard-1.json shard-2.json -o merged.json
|
|
863
|
+
npx testrelic serve ./test-results/.testrelic-report
|
|
864
|
+
npx testrelic serve ./test-results/.testrelic-report --port 9400
|
|
865
|
+
`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
main();
|