@crvy/rprtr 0.0.7 → 0.0.9
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/CHANGELOG.md +10 -0
- package/dist/{chunk-OMDYTNWY.js → chunk-TS64TROX.js} +20 -139
- package/dist/cli.js +2 -2
- package/dist/debug-log.d.ts +3 -0
- package/dist/debug-log.d.ts.map +1 -0
- package/dist/index.css +0 -22
- package/dist/index.js +83 -127
- package/dist/reporter-artifact-ops.d.ts +22 -0
- package/dist/reporter-artifact-ops.d.ts.map +1 -0
- package/dist/reporter.cjs +194 -176
- package/dist/reporter.d.ts +0 -5
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +135 -117
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server.cjs +35 -154
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/server/report-watch.d.ts +0 -9
- package/dist/server/report-watch.d.ts.map +0 -1
- package/dist/{chunk-JGS2VWQP.js → chunk-EJRLZZ22.js} +142 -142
package/dist/reporter.js
CHANGED
|
@@ -8,10 +8,24 @@ import {
|
|
|
8
8
|
finalizeRunEvent,
|
|
9
9
|
resolveBaselineTargets,
|
|
10
10
|
safeParse
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-EJRLZZ22.js";
|
|
12
12
|
|
|
13
13
|
// src/reporter.ts
|
|
14
14
|
import { existsSync } from "fs";
|
|
15
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
16
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
17
|
+
import pLimit2 from "p-limit";
|
|
18
|
+
|
|
19
|
+
// src/debug-log.ts
|
|
20
|
+
var isDebug = () => process.env.DEBUG !== void 0 && process.env.DEBUG !== "";
|
|
21
|
+
var log = (...args) => {
|
|
22
|
+
if (isDebug()) console.log(...args);
|
|
23
|
+
};
|
|
24
|
+
var logError = (...args) => {
|
|
25
|
+
if (isDebug()) console.error(...args);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/reporter-artifact-ops.ts
|
|
15
29
|
import { copyFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
16
30
|
import { dirname as dirname2, join } from "path";
|
|
17
31
|
import pLimit from "p-limit";
|
|
@@ -137,6 +151,98 @@ async function writeReportArtifact(options) {
|
|
|
137
151
|
await writeFile(reportHtmlPath, html);
|
|
138
152
|
}
|
|
139
153
|
|
|
154
|
+
// src/reporter-artifact-ops.ts
|
|
155
|
+
var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
|
|
156
|
+
var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
|
|
157
|
+
var MAX_CONCURRENT_FILE_OPS = 5;
|
|
158
|
+
var SAFE_ARTIFACT_CHARACTER = /^[A-Za-z0-9._-]$/;
|
|
159
|
+
function encodeArtifactPathSegment(segment) {
|
|
160
|
+
if (segment === ".") return CURRENT_DIRECTORY_ARTIFACT_SEGMENT;
|
|
161
|
+
if (segment === "..") return PARENT_DIRECTORY_ARTIFACT_SEGMENT;
|
|
162
|
+
return Array.from(
|
|
163
|
+
segment,
|
|
164
|
+
(character) => SAFE_ARTIFACT_CHARACTER.test(character) ? character : encodeURIComponent(character)
|
|
165
|
+
).join("");
|
|
166
|
+
}
|
|
167
|
+
function safeArtifactPath(name) {
|
|
168
|
+
return name.split("/").map(encodeArtifactPathSegment).join("/");
|
|
169
|
+
}
|
|
170
|
+
function sanitizeId(id) {
|
|
171
|
+
return id.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
172
|
+
}
|
|
173
|
+
async function copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments) {
|
|
174
|
+
const attachmentName = `${target.attachmentBaseName}-expected.png`;
|
|
175
|
+
const artifactPath = safeArtifactPath(attachmentName);
|
|
176
|
+
const destPath = join(testScreenshotDir, artifactPath);
|
|
177
|
+
try {
|
|
178
|
+
await mkdir2(dirname2(destPath), { recursive: true });
|
|
179
|
+
await copyFile(target.snapshotPath, destPath);
|
|
180
|
+
savedAttachments.push({ name: attachmentName, path: `${safeTestId}/${artifactPath}`, contentType: "image/png" });
|
|
181
|
+
log(`[CrvyRprtr] Attached baseline: ${target.snapshotPath}`);
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function saveAttachments(screenshotDir, testId, result) {
|
|
186
|
+
const savedAttachments = [];
|
|
187
|
+
const safeTestId = sanitizeId(testId);
|
|
188
|
+
const testScreenshotDir = join(screenshotDir, safeTestId);
|
|
189
|
+
const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
|
|
190
|
+
await Promise.all(
|
|
191
|
+
result.attachments.filter(
|
|
192
|
+
(attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
|
|
193
|
+
).map(
|
|
194
|
+
(attachment) => limit(async () => {
|
|
195
|
+
try {
|
|
196
|
+
const artifactPath = safeArtifactPath(attachment.name);
|
|
197
|
+
const destPath = join(testScreenshotDir, artifactPath);
|
|
198
|
+
await mkdir2(dirname2(destPath), { recursive: true });
|
|
199
|
+
await copyFile(attachment.path, destPath);
|
|
200
|
+
savedAttachments.push({
|
|
201
|
+
name: attachment.name,
|
|
202
|
+
path: `${safeTestId}/${artifactPath}`,
|
|
203
|
+
contentType: attachment.contentType ?? "image/png"
|
|
204
|
+
});
|
|
205
|
+
log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logError(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, error);
|
|
208
|
+
savedAttachments.push({
|
|
209
|
+
name: attachment.name,
|
|
210
|
+
path: attachment.path,
|
|
211
|
+
contentType: attachment.contentType ?? "image/png"
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
return savedAttachments;
|
|
218
|
+
}
|
|
219
|
+
async function writeOfflineReport(runEvents, offlineReportPath, workerIndex) {
|
|
220
|
+
if (runEvents.length === 0) {
|
|
221
|
+
log("[CrvyRprtr] No offline events to write");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const report = {
|
|
226
|
+
version: 1,
|
|
227
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
228
|
+
workers: workerIndex + 1,
|
|
229
|
+
events: runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex }))
|
|
230
|
+
};
|
|
231
|
+
await writeFile2(offlineReportPath, JSON.stringify(report, null, 2));
|
|
232
|
+
log(`[CrvyRprtr] Wrote offline report: ${offlineReportPath}`);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logError("[CrvyRprtr] Failed to write offline report:", error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function writeStaticArtifact(runEvents, screenshotDir, reportHtmlPath) {
|
|
238
|
+
try {
|
|
239
|
+
await writeReportArtifact({ events: runEvents, screenshotDir, reportHtmlPath });
|
|
240
|
+
log(`[CrvyRprtr] Wrote report artifact: ${reportHtmlPath}`);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
logError("[CrvyRprtr] Failed to write report artifact:", error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
140
246
|
// src/reporter-utils.ts
|
|
141
247
|
var NAMED_SCREENSHOT_STEP_TITLE = /toHaveScreenshot\((.+?)\)/;
|
|
142
248
|
var UNNAMED_SCREENSHOT_STEP_TITLE = /^Expect "toHaveScreenshot"(?:\s|$)/;
|
|
@@ -207,19 +313,6 @@ function extractScreenshotDeclarations(steps) {
|
|
|
207
313
|
}
|
|
208
314
|
|
|
209
315
|
// src/reporter.ts
|
|
210
|
-
var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
|
|
211
|
-
var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
|
|
212
|
-
var MAX_CONCURRENT_FILE_OPS = 5;
|
|
213
|
-
var SAFE_ARTIFACT_CHARACTER = /^[A-Za-z0-9._-]$/;
|
|
214
|
-
var encodeArtifactPathSegment = (segment) => {
|
|
215
|
-
if (segment === ".") return CURRENT_DIRECTORY_ARTIFACT_SEGMENT;
|
|
216
|
-
if (segment === "..") return PARENT_DIRECTORY_ARTIFACT_SEGMENT;
|
|
217
|
-
return Array.from(
|
|
218
|
-
segment,
|
|
219
|
-
(character) => SAFE_ARTIFACT_CHARACTER.test(character) ? character : encodeURIComponent(character)
|
|
220
|
-
).join("");
|
|
221
|
-
};
|
|
222
|
-
var safeArtifactPath = (name) => name.split("/").map(encodeArtifactPathSegment).join("/");
|
|
223
316
|
var CrvyRprtr = class {
|
|
224
317
|
ws = null;
|
|
225
318
|
serverUrl;
|
|
@@ -247,43 +340,43 @@ var CrvyRprtr = class {
|
|
|
247
340
|
this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
|
|
248
341
|
}
|
|
249
342
|
async onBegin(config, suite) {
|
|
250
|
-
this.configDir = config.configFile === void 0 ? config.rootDir :
|
|
251
|
-
|
|
252
|
-
await
|
|
343
|
+
this.configDir = config.configFile === void 0 ? config.rootDir : dirname3(config.configFile);
|
|
344
|
+
log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
|
|
345
|
+
await mkdir3(this.screenshotDir, { recursive: true });
|
|
253
346
|
this.connect();
|
|
254
347
|
}
|
|
255
348
|
connect() {
|
|
256
349
|
const WebSocketConstructor = globalThis.WebSocket;
|
|
257
350
|
if (typeof WebSocketConstructor !== "function") {
|
|
258
|
-
|
|
351
|
+
log("[CrvyRprtr] WebSocket unavailable in current runtime; offline mode enabled");
|
|
259
352
|
this.enableOfflineMode();
|
|
260
353
|
return;
|
|
261
354
|
}
|
|
262
355
|
try {
|
|
263
356
|
this.ws = new WebSocketConstructor(this.serverUrl);
|
|
264
357
|
this.ws.onopen = () => {
|
|
265
|
-
|
|
358
|
+
log("[CrvyRprtr] Connected to Crvy Rprtr server");
|
|
266
359
|
this.isOfflineMode = false;
|
|
267
360
|
for (const message of this.queue) this.ws.send(message);
|
|
268
361
|
this.queue = [];
|
|
269
362
|
};
|
|
270
363
|
this.ws.onerror = (error) => {
|
|
271
|
-
|
|
364
|
+
logError("[CrvyRprtr] WebSocket error:", error);
|
|
272
365
|
this.enableOfflineMode();
|
|
273
366
|
};
|
|
274
367
|
this.ws.onclose = () => {
|
|
275
|
-
|
|
368
|
+
log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
|
|
276
369
|
this.enableOfflineMode();
|
|
277
370
|
};
|
|
278
371
|
} catch (error) {
|
|
279
|
-
|
|
372
|
+
logError("[CrvyRprtr] Failed to connect:", error);
|
|
280
373
|
this.enableOfflineMode();
|
|
281
374
|
}
|
|
282
375
|
}
|
|
283
376
|
enableOfflineMode() {
|
|
284
377
|
if (this.isOfflineMode) return;
|
|
285
378
|
this.isOfflineMode = this.hadOfflineMode = true;
|
|
286
|
-
|
|
379
|
+
log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
|
|
287
380
|
}
|
|
288
381
|
describeTitlePath(test) {
|
|
289
382
|
const titlePath = [];
|
|
@@ -309,7 +402,7 @@ var CrvyRprtr = class {
|
|
|
309
402
|
}
|
|
310
403
|
async onTestEnd(test, result) {
|
|
311
404
|
const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
|
|
312
|
-
const savedAttachments = await this.
|
|
405
|
+
const savedAttachments = await saveAttachments(this.screenshotDir, test.id, result);
|
|
313
406
|
try {
|
|
314
407
|
await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
|
|
315
408
|
this.send({
|
|
@@ -349,113 +442,38 @@ var CrvyRprtr = class {
|
|
|
349
442
|
snapshotPathExists: existsSync
|
|
350
443
|
};
|
|
351
444
|
}
|
|
352
|
-
async copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments) {
|
|
353
|
-
const attachmentName = `${target.attachmentBaseName}-expected.png`;
|
|
354
|
-
const artifactPath = safeArtifactPath(attachmentName);
|
|
355
|
-
const destPath = join(testScreenshotDir, artifactPath);
|
|
356
|
-
try {
|
|
357
|
-
await mkdir2(dirname2(destPath), { recursive: true });
|
|
358
|
-
await copyFile(target.snapshotPath, destPath);
|
|
359
|
-
savedAttachments.push({ name: attachmentName, path: `${safeTestId}/${artifactPath}`, contentType: "image/png" });
|
|
360
|
-
console.log(`[CrvyRprtr] Attached baseline: ${target.snapshotPath}`);
|
|
361
|
-
} catch {
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
445
|
async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
|
|
365
446
|
if (status !== "passed" || screenshotDeclarations.length === 0) return;
|
|
366
447
|
const input = this.baselineInput(test, screenshotDeclarations);
|
|
367
448
|
if (input === null) return;
|
|
368
449
|
const targets = resolveBaselineTargets(input);
|
|
369
450
|
if (targets.length === 0) return;
|
|
370
|
-
const safeTestId =
|
|
371
|
-
const testScreenshotDir =
|
|
372
|
-
const limit =
|
|
451
|
+
const safeTestId = sanitizeId(test.id);
|
|
452
|
+
const testScreenshotDir = join2(this.screenshotDir, safeTestId);
|
|
453
|
+
const limit = pLimit2(5);
|
|
373
454
|
await Promise.all(
|
|
374
455
|
targets.map(
|
|
375
|
-
(target) => limit(() =>
|
|
376
|
-
)
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
async saveAttachments(testId, result) {
|
|
380
|
-
const savedAttachments = [];
|
|
381
|
-
const safeTestId = this.sanitizeId(testId);
|
|
382
|
-
const testScreenshotDir = join(this.screenshotDir, safeTestId);
|
|
383
|
-
const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
|
|
384
|
-
await Promise.all(
|
|
385
|
-
result.attachments.filter(
|
|
386
|
-
(attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
|
|
387
|
-
).map(
|
|
388
|
-
(attachment) => limit(async () => {
|
|
389
|
-
try {
|
|
390
|
-
const artifactPath = safeArtifactPath(attachment.name);
|
|
391
|
-
const destPath = join(testScreenshotDir, artifactPath);
|
|
392
|
-
await mkdir2(dirname2(destPath), { recursive: true });
|
|
393
|
-
await copyFile(attachment.path, destPath);
|
|
394
|
-
savedAttachments.push({
|
|
395
|
-
name: attachment.name,
|
|
396
|
-
path: `${safeTestId}/${artifactPath}`,
|
|
397
|
-
contentType: attachment.contentType
|
|
398
|
-
});
|
|
399
|
-
console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, error);
|
|
402
|
-
savedAttachments.push({
|
|
403
|
-
name: attachment.name,
|
|
404
|
-
path: attachment.path,
|
|
405
|
-
contentType: attachment.contentType
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
})
|
|
456
|
+
(target) => limit(() => copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
|
|
409
457
|
)
|
|
410
458
|
);
|
|
411
|
-
return savedAttachments;
|
|
412
|
-
}
|
|
413
|
-
sanitizeId(id) {
|
|
414
|
-
return id.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
415
|
-
}
|
|
416
|
-
async writeOfflineReport() {
|
|
417
|
-
if (this.runEvents.length === 0) console.log("[CrvyRprtr] No offline events to write");
|
|
418
|
-
if (this.runEvents.length === 0) return;
|
|
419
|
-
try {
|
|
420
|
-
const report = {
|
|
421
|
-
version: 1,
|
|
422
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
423
|
-
workers: this.workerIndex + 1,
|
|
424
|
-
events: this.runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex: this.workerIndex }))
|
|
425
|
-
};
|
|
426
|
-
await writeFile2(this.offlineReportPath, JSON.stringify(report, null, 2));
|
|
427
|
-
console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.error("[CrvyRprtr] Failed to write offline report:", error);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
async writeStaticArtifact() {
|
|
433
|
-
try {
|
|
434
|
-
await writeReportArtifact({
|
|
435
|
-
events: this.runEvents,
|
|
436
|
-
screenshotDir: this.screenshotDir,
|
|
437
|
-
reportHtmlPath: this.reportHtmlPath
|
|
438
|
-
});
|
|
439
|
-
console.log(`[CrvyRprtr] Wrote report artifact: ${this.reportHtmlPath}`);
|
|
440
|
-
} catch (error) {
|
|
441
|
-
console.error("[CrvyRprtr] Failed to write report artifact:", error);
|
|
442
|
-
}
|
|
443
459
|
}
|
|
444
460
|
async onEnd(result) {
|
|
445
461
|
this.send({ type: "run-end", data: { status: result.status } });
|
|
446
|
-
await this.
|
|
447
|
-
if (this.hadOfflineMode) await this.
|
|
462
|
+
await writeStaticArtifact(this.runEvents, this.screenshotDir, this.reportHtmlPath);
|
|
463
|
+
if (this.hadOfflineMode) await writeOfflineReport(this.runEvents, this.offlineReportPath, this.workerIndex);
|
|
448
464
|
await new Promise((resolve2) => {
|
|
449
|
-
if (this.ws
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
this.ws
|
|
458
|
-
|
|
465
|
+
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
|
466
|
+
resolve2();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
this.ws.onclose = () => {
|
|
470
|
+
resolve2();
|
|
471
|
+
};
|
|
472
|
+
setTimeout(() => {
|
|
473
|
+
this.ws?.close();
|
|
474
|
+
resolve2();
|
|
475
|
+
}, 1e3);
|
|
476
|
+
this.ws.close();
|
|
459
477
|
});
|
|
460
478
|
}
|
|
461
479
|
send(message) {
|
package/dist/server/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAI/C,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,8BAA8B,CAAC,EAAE,MAAM,CAAA;IACvC,sCAAsC,CAAC,EAAE,MAAM,CAAA;CAChD;AAUD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAA;IAChC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,aAAa,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClD,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3D;AA8LD,wBAAsB,eAAe,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,SAAS,CAAC,CA6BrF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/server/handlers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE;QACV,SAAS,EAAE,OAAO,CAAA;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;QAC/B,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,YAAY,EAAE,OAAO,CAAA;QACrB,aAAa,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,SAAS,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAA;IAChC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC1B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/server/handlers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE;QACV,SAAS,EAAE,OAAO,CAAA;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;QAC/B,QAAQ,EAAE,MAAM,EAAE,CAAA;QAClB,YAAY,EAAE,OAAO,CAAA;QACrB,aAAa,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,SAAS,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAA;IAChC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC1B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAK9E;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAW1E;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrG;AAED,wBAAgB,aAAa,IAAI,IAAI,CAIpC;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAIpD"}
|
package/dist/server.cjs
CHANGED
|
@@ -35,7 +35,7 @@ __export(server_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(server_exports);
|
|
36
36
|
|
|
37
37
|
// src/server/app.ts
|
|
38
|
-
var
|
|
38
|
+
var import_path5 = require("path");
|
|
39
39
|
var import_url = require("url");
|
|
40
40
|
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
41
41
|
|
|
@@ -530,7 +530,9 @@ function broadcastToBrowsers(wsClients, msg) {
|
|
|
530
530
|
// src/server/handlers.ts
|
|
531
531
|
function handleTestBegin(ctx, data) {
|
|
532
532
|
const test = applyTestBeginEvent(ctx, data);
|
|
533
|
+
ctx.reportData.isRunning = true;
|
|
533
534
|
console.log(` \u25B6 [${test.browser ?? "?"}] ${test.title}`);
|
|
535
|
+
broadcastToBrowsers(ctx.wsClients, { type: "test-begin", data });
|
|
534
536
|
}
|
|
535
537
|
function handleTestEnd(ctx, data) {
|
|
536
538
|
const result = applyTestEndEvent(ctx, data);
|
|
@@ -560,121 +562,13 @@ function handleSync(ctx) {
|
|
|
560
562
|
broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
|
|
561
563
|
}
|
|
562
564
|
|
|
563
|
-
// src/server/report-watch.ts
|
|
564
|
-
var import_promises3 = require("fs/promises");
|
|
565
|
-
var import_path3 = require("path");
|
|
566
|
-
var OFFLINE_REPORT_FILE_PATTERN2 = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
567
|
-
function createDebouncedRefresh(reload, delayMs = 50) {
|
|
568
|
-
let timer = null;
|
|
569
|
-
return () => {
|
|
570
|
-
if (timer !== null) {
|
|
571
|
-
clearTimeout(timer);
|
|
572
|
-
}
|
|
573
|
-
timer = setTimeout(() => {
|
|
574
|
-
timer = null;
|
|
575
|
-
void reload();
|
|
576
|
-
}, delayMs);
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
function isFileNotFound2(error) {
|
|
580
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
581
|
-
}
|
|
582
|
-
async function describeFile(filePath, label) {
|
|
583
|
-
try {
|
|
584
|
-
const fileStats = await (0, import_promises3.stat)(filePath);
|
|
585
|
-
return `${label}:${fileStats.size}:${fileStats.mtimeMs}`;
|
|
586
|
-
} catch (error) {
|
|
587
|
-
if (isFileNotFound2(error)) {
|
|
588
|
-
return null;
|
|
589
|
-
}
|
|
590
|
-
throw error;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
async function createOfflineFingerprint(offlineReportDir) {
|
|
594
|
-
if (!await isDirectory(offlineReportDir)) {
|
|
595
|
-
return "offline:missing";
|
|
596
|
-
}
|
|
597
|
-
try {
|
|
598
|
-
const entries = await (0, import_promises3.readdir)(offlineReportDir, { withFileTypes: true });
|
|
599
|
-
const relevantEntries = entries.filter(
|
|
600
|
-
(entry) => entry.isFile() && (entry.name === "report.json" || OFFLINE_REPORT_FILE_PATTERN2.test(entry.name))
|
|
601
|
-
).sort((left, right) => left.name.localeCompare(right.name));
|
|
602
|
-
const parts = await Promise.all(
|
|
603
|
-
relevantEntries.map((entry) => describeFile((0, import_path3.join)(offlineReportDir, entry.name), entry.name))
|
|
604
|
-
);
|
|
605
|
-
return `offline:${parts.filter((part) => part !== null).join("|")}`;
|
|
606
|
-
} catch (error) {
|
|
607
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
608
|
-
console.warn(`[Server] Unable to scan ${offlineReportDir}: ${errorMsg}`);
|
|
609
|
-
return "offline:error";
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
async function createScreenshotFingerprint(screenshotDir, relativePath = "") {
|
|
613
|
-
const directoryPath = relativePath === "" ? screenshotDir : (0, import_path3.join)(screenshotDir, relativePath);
|
|
614
|
-
if (!await isDirectory(directoryPath)) {
|
|
615
|
-
return relativePath === "" ? ["screenshots:missing"] : [];
|
|
616
|
-
}
|
|
617
|
-
try {
|
|
618
|
-
const entries = await (0, import_promises3.readdir)(directoryPath, { withFileTypes: true });
|
|
619
|
-
const parts = await Promise.all(
|
|
620
|
-
entries.sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
621
|
-
const entryRelativePath = relativePath === "" ? entry.name : (0, import_path3.join)(relativePath, entry.name);
|
|
622
|
-
if (entry.isDirectory()) {
|
|
623
|
-
return createScreenshotFingerprint(screenshotDir, entryRelativePath);
|
|
624
|
-
}
|
|
625
|
-
return describeFile((0, import_path3.join)(screenshotDir, entryRelativePath), entryRelativePath);
|
|
626
|
-
})
|
|
627
|
-
);
|
|
628
|
-
return parts.flat().filter((part) => part !== null);
|
|
629
|
-
} catch (error) {
|
|
630
|
-
if (isFileNotFound2(error)) {
|
|
631
|
-
return relativePath === "" ? ["screenshots:missing"] : [];
|
|
632
|
-
}
|
|
633
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
634
|
-
console.warn(`[Server] Unable to scan ${directoryPath}: ${errorMsg}`);
|
|
635
|
-
return relativePath === "" ? ["screenshots:error"] : [];
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
async function createArtifactsFingerprint(options) {
|
|
639
|
-
const [offlineFingerprint, screenshotFingerprint] = await Promise.all([
|
|
640
|
-
createOfflineFingerprint(options.offlineReportDir),
|
|
641
|
-
createScreenshotFingerprint(options.screenshotDir)
|
|
642
|
-
]);
|
|
643
|
-
return `${offlineFingerprint}::${screenshotFingerprint.join("|")}`;
|
|
644
|
-
}
|
|
645
|
-
async function watchReportArtifacts(options) {
|
|
646
|
-
let fingerprint = await createArtifactsFingerprint(options);
|
|
647
|
-
let isPolling = false;
|
|
648
|
-
const interval = setInterval(() => {
|
|
649
|
-
if (isPolling) {
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
isPolling = true;
|
|
653
|
-
void createArtifactsFingerprint(options).then((nextFingerprint) => {
|
|
654
|
-
if (nextFingerprint === fingerprint) {
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
fingerprint = nextFingerprint;
|
|
658
|
-
options.scheduleRefresh();
|
|
659
|
-
}).catch((error) => {
|
|
660
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
661
|
-
console.warn(`[Server] Artifact refresh poll failed: ${errorMsg}`);
|
|
662
|
-
}).finally(() => {
|
|
663
|
-
isPolling = false;
|
|
664
|
-
});
|
|
665
|
-
}, 100);
|
|
666
|
-
return () => {
|
|
667
|
-
clearInterval(interval);
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
|
|
671
565
|
// src/server/routes.ts
|
|
672
566
|
var import_fs = require("fs");
|
|
673
|
-
var
|
|
567
|
+
var import_path4 = require("path");
|
|
674
568
|
|
|
675
569
|
// src/snapshot-path-resolver.ts
|
|
676
570
|
var import_crypto = require("crypto");
|
|
677
|
-
var
|
|
571
|
+
var import_path3 = require("path");
|
|
678
572
|
var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
|
|
679
573
|
var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
|
|
680
574
|
function isUnsafeFilePathCharacter(character) {
|
|
@@ -709,16 +603,16 @@ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
|
|
|
709
603
|
const end = length - middle.length - start;
|
|
710
604
|
return value.slice(0, start) + middle + value.slice(-end);
|
|
711
605
|
}
|
|
712
|
-
function sanitizeFilePathBeforeExtension(filePath, extension = (0,
|
|
606
|
+
function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
|
|
713
607
|
const base = filePath.slice(0, filePath.length - extension.length);
|
|
714
608
|
return sanitizeForFilePath(base) + extension;
|
|
715
609
|
}
|
|
716
610
|
function addSuffixToFilePath(filePath, suffix) {
|
|
717
|
-
const extension = (0,
|
|
611
|
+
const extension = (0, import_path3.extname)(filePath);
|
|
718
612
|
return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
|
|
719
613
|
}
|
|
720
614
|
function normalizedSnapshotDir(config) {
|
|
721
|
-
return (0,
|
|
615
|
+
return (0, import_path3.resolve)(config.configDir, config.snapshotDir);
|
|
722
616
|
}
|
|
723
617
|
function templateValue(template, token, value) {
|
|
724
618
|
return template.replace(
|
|
@@ -728,8 +622,8 @@ function templateValue(template, token, value) {
|
|
|
728
622
|
}
|
|
729
623
|
function applyTemplate(input, nameArgument, extension) {
|
|
730
624
|
const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
|
|
731
|
-
const relativeTestFilePath = (0,
|
|
732
|
-
const parsed = (0,
|
|
625
|
+
const relativeTestFilePath = (0, import_path3.relative)(input.config.testDir, input.testFile);
|
|
626
|
+
const parsed = (0, import_path3.parse)(relativeTestFilePath);
|
|
733
627
|
const tokens = [
|
|
734
628
|
["testDir", input.config.testDir],
|
|
735
629
|
["snapshotDir", normalizedSnapshotDir(input.config)],
|
|
@@ -747,16 +641,16 @@ function applyTemplate(input, nameArgument, extension) {
|
|
|
747
641
|
(currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
|
|
748
642
|
template
|
|
749
643
|
);
|
|
750
|
-
return (0,
|
|
644
|
+
return (0, import_path3.resolve)(input.config.configDir, snapshotPath);
|
|
751
645
|
}
|
|
752
|
-
function removeExtension(filePath, extension = (0,
|
|
646
|
+
function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
|
|
753
647
|
return filePath.slice(0, filePath.length - extension.length);
|
|
754
648
|
}
|
|
755
649
|
function snapshotNameParts(declaredName) {
|
|
756
|
-
const extension = (0,
|
|
650
|
+
const extension = (0, import_path3.extname)(declaredName) || ".png";
|
|
757
651
|
return {
|
|
758
652
|
extension,
|
|
759
|
-
filePath: (0,
|
|
653
|
+
filePath: (0, import_path3.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
|
|
760
654
|
};
|
|
761
655
|
}
|
|
762
656
|
function filePathForOccurrence(filePath, occurrenceIndex) {
|
|
@@ -780,7 +674,7 @@ function resolveStringCallTarget(input, declaration) {
|
|
|
780
674
|
function resolveArrayCallTarget(input, declaration) {
|
|
781
675
|
const { extension, filePath } = snapshotNameParts(declaration.declaredName);
|
|
782
676
|
const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
|
|
783
|
-
const nameArgument = (0,
|
|
677
|
+
const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(occurrenceFilePath), (0, import_path3.basename)(occurrenceFilePath, extension));
|
|
784
678
|
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
785
679
|
}
|
|
786
680
|
function resolveNamedTarget(input, declaration) {
|
|
@@ -816,7 +710,7 @@ function resolveTarget(input, declaration) {
|
|
|
816
710
|
case "unnamed": {
|
|
817
711
|
const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
|
|
818
712
|
const extension = ".png";
|
|
819
|
-
const nameArgument = (0,
|
|
713
|
+
const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(anonymousFileName), (0, import_path3.basename)(anonymousFileName, extension));
|
|
820
714
|
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
821
715
|
}
|
|
822
716
|
}
|
|
@@ -834,7 +728,7 @@ function isWebSocketUpgradeRequest(req) {
|
|
|
834
728
|
return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
835
729
|
}
|
|
836
730
|
async function handleRoot(ctx) {
|
|
837
|
-
const html = await respondWithFile((0,
|
|
731
|
+
const html = await respondWithFile((0, import_path4.join)(ctx.staticDir, "index.html"), "text/html");
|
|
838
732
|
return html ?? new Response("Not Found", { status: 404 });
|
|
839
733
|
}
|
|
840
734
|
async function handleAppCss() {
|
|
@@ -853,7 +747,7 @@ function handleApiReport(ctx) {
|
|
|
853
747
|
}
|
|
854
748
|
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
855
749
|
function actualPathFromUrl(ctx, actualUrl) {
|
|
856
|
-
return actualUrl.startsWith("/screenshots/") ? (0,
|
|
750
|
+
return actualUrl.startsWith("/screenshots/") ? (0, import_path4.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length)) : actualUrl;
|
|
857
751
|
}
|
|
858
752
|
function reporterTitlePath(test) {
|
|
859
753
|
const testFile = test.location?.file;
|
|
@@ -871,8 +765,8 @@ function resolveApprovalTarget(ctx, test, retry, imageName) {
|
|
|
871
765
|
declarations: [declaration],
|
|
872
766
|
config: {
|
|
873
767
|
configDir: ctx.approvalRouting.configDir,
|
|
874
|
-
testDir: ctx.approvalRouting.playwrightTestDir ?? (0,
|
|
875
|
-
snapshotDir: ctx.approvalRouting.playwrightSnapshotDir ?? (0,
|
|
768
|
+
testDir: ctx.approvalRouting.playwrightTestDir ?? (0, import_path4.dirname)(testFile),
|
|
769
|
+
snapshotDir: ctx.approvalRouting.playwrightSnapshotDir ?? (0, import_path4.dirname)(testFile),
|
|
876
770
|
projectName: test.browser,
|
|
877
771
|
snapshotSuffix: process.platform,
|
|
878
772
|
snapshotPathTemplate: ctx.approvalRouting.playwrightSnapshotPathTemplate,
|
|
@@ -997,7 +891,7 @@ async function handleScreenshots(ctx, req) {
|
|
|
997
891
|
}
|
|
998
892
|
async function handleDist(ctx, req) {
|
|
999
893
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
1000
|
-
const filePath = (0,
|
|
894
|
+
const filePath = (0, import_path4.join)(ctx.staticDir, path);
|
|
1001
895
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
1002
896
|
const file = await respondWithFile(filePath, contentType);
|
|
1003
897
|
return file ?? new Response("Not Found", { status: 404 });
|
|
@@ -1101,10 +995,6 @@ async function loadOfflineReports(offlineReportDir, reportData) {
|
|
|
1101
995
|
screenshotsBaseUrl: "/screenshots/"
|
|
1102
996
|
});
|
|
1103
997
|
}
|
|
1104
|
-
function resetReloadableReportData(reportData) {
|
|
1105
|
-
reportData.tests = {};
|
|
1106
|
-
reportData.isUpdateMode = false;
|
|
1107
|
-
}
|
|
1108
998
|
async function handleParsedWebSocketMessage(ctx, msg) {
|
|
1109
999
|
switch (msg.type) {
|
|
1110
1000
|
case "test-begin": {
|
|
@@ -1153,19 +1043,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
1153
1043
|
};
|
|
1154
1044
|
}
|
|
1155
1045
|
async function resolveStaticDir(staticDir) {
|
|
1156
|
-
const currentDir = (0,
|
|
1046
|
+
const currentDir = (0, import_path5.dirname)((0, import_url.fileURLToPath)(import_meta.url));
|
|
1157
1047
|
const candidates = staticDir === void 0 ? [
|
|
1158
1048
|
currentDir,
|
|
1159
|
-
(0,
|
|
1160
|
-
(0,
|
|
1161
|
-
(0,
|
|
1162
|
-
(0,
|
|
1163
|
-
(0,
|
|
1164
|
-
] : [staticDir, (0,
|
|
1049
|
+
(0, import_path5.join)(currentDir, "dist"),
|
|
1050
|
+
(0, import_path5.join)(currentDir, "..", "dist"),
|
|
1051
|
+
(0, import_path5.join)(currentDir, "..", "..", "dist"),
|
|
1052
|
+
(0, import_path5.join)(currentDir, ".."),
|
|
1053
|
+
(0, import_path5.join)(currentDir, "..", "..")
|
|
1054
|
+
] : [staticDir, (0, import_path5.join)(staticDir, "dist")];
|
|
1165
1055
|
const resolvedCandidates = await Promise.all(
|
|
1166
1056
|
candidates.map(async (candidate) => ({
|
|
1167
1057
|
candidate,
|
|
1168
|
-
exists: await fileExists((0,
|
|
1058
|
+
exists: await fileExists((0, import_path5.join)(candidate, "index.html"))
|
|
1169
1059
|
}))
|
|
1170
1060
|
);
|
|
1171
1061
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -1176,9 +1066,9 @@ async function resolveStaticDir(staticDir) {
|
|
|
1176
1066
|
}
|
|
1177
1067
|
async function resolveReportPath(reportPath) {
|
|
1178
1068
|
if (await isDirectory(reportPath)) {
|
|
1179
|
-
return { reportFile: (0,
|
|
1069
|
+
return { reportFile: (0, import_path5.join)(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
1180
1070
|
}
|
|
1181
|
-
return { reportFile: reportPath, offlineReportDir: (0,
|
|
1071
|
+
return { reportFile: reportPath, offlineReportDir: (0, import_path5.dirname)(reportPath) };
|
|
1182
1072
|
}
|
|
1183
1073
|
function createRoutesContext(reportData, staticDir, saveReport, options) {
|
|
1184
1074
|
return {
|
|
@@ -1209,22 +1099,13 @@ async function createServerApp(options = {}) {
|
|
|
1209
1099
|
const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
|
|
1210
1100
|
const handleRequest = (req) => handleHttpRequest(routesContext, req);
|
|
1211
1101
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
await loadReport(reportFile, reportData);
|
|
1215
|
-
await loadOfflineReports(offlineReportDir, reportData);
|
|
1216
|
-
broadcastToBrowsers(wsClients, { type: "sync", data: reportData });
|
|
1217
|
-
};
|
|
1218
|
-
await reloadFromDisk();
|
|
1219
|
-
const close = await watchReportArtifacts({
|
|
1220
|
-
offlineReportDir,
|
|
1221
|
-
screenshotDir: reportData.screenshotDir,
|
|
1222
|
-
scheduleRefresh: createDebouncedRefresh(reloadFromDisk)
|
|
1223
|
-
});
|
|
1102
|
+
await loadReport(reportFile, reportData);
|
|
1103
|
+
await loadOfflineReports(offlineReportDir, reportData);
|
|
1224
1104
|
return {
|
|
1225
1105
|
port,
|
|
1226
1106
|
wsClients,
|
|
1227
|
-
close
|
|
1107
|
+
close: () => {
|
|
1108
|
+
},
|
|
1228
1109
|
handleRequest,
|
|
1229
1110
|
handleWebSocketMessage
|
|
1230
1111
|
};
|