test-proxy-recorder 0.1.5 → 0.1.7
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 +334 -207
- package/dist/index.cjs +205 -62
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.mjs +205 -62
- package/dist/proxy.js +205 -62
- package/package.json +8 -3
package/dist/proxy.js
CHANGED
|
@@ -161,14 +161,16 @@ var ProxyServer = class {
|
|
|
161
161
|
this.proxy.on("error", this.handleProxyError.bind(this));
|
|
162
162
|
this.proxy.on("proxyRes", this.handleProxyResponse.bind(this));
|
|
163
163
|
}
|
|
164
|
-
handleProxyError(err,
|
|
164
|
+
handleProxyError(err, req, res) {
|
|
165
165
|
console.error("Proxy error:", err);
|
|
166
166
|
if (!(res instanceof http.ServerResponse)) {
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
if (!res.headersSent) {
|
|
170
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
170
171
|
res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
|
|
171
|
-
"Content-Type": "application/json"
|
|
172
|
+
"Content-Type": "application/json",
|
|
173
|
+
...corsHeaders
|
|
172
174
|
});
|
|
173
175
|
}
|
|
174
176
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
@@ -179,24 +181,51 @@ var ProxyServer = class {
|
|
|
179
181
|
this.recordResponse(req, proxyRes);
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Get CORS headers for a given request
|
|
186
|
+
* @param req The incoming HTTP request
|
|
187
|
+
* @returns An object containing CORS headers
|
|
188
|
+
*/
|
|
189
|
+
getCorsHeaders(req) {
|
|
183
190
|
const origin = req.headers.origin;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
return {
|
|
192
|
+
"access-control-allow-origin": origin || "*",
|
|
193
|
+
"access-control-allow-credentials": "true",
|
|
194
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
195
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
196
|
+
"access-control-expose-headers": "*"
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
addCorsHeaders(proxyRes, req) {
|
|
200
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
201
|
+
Object.assign(proxyRes.headers, corsHeaders);
|
|
189
202
|
}
|
|
190
203
|
getTarget() {
|
|
191
204
|
const target = this.targets[this.currentTargetIndex];
|
|
192
205
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
193
206
|
return target;
|
|
194
207
|
}
|
|
208
|
+
parseGetParams(req) {
|
|
209
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
210
|
+
const mode = url.searchParams.get("mode");
|
|
211
|
+
const id = url.searchParams.get("id") || void 0;
|
|
212
|
+
const timeoutParam = url.searchParams.get("timeout");
|
|
213
|
+
const timeout = timeoutParam ? Number.parseInt(timeoutParam, 10) : void 0;
|
|
214
|
+
if (!mode) {
|
|
215
|
+
throw new Error("Mode parameter is required");
|
|
216
|
+
}
|
|
217
|
+
return { mode, id, timeout };
|
|
218
|
+
}
|
|
195
219
|
async handleControlRequest(req, res) {
|
|
196
220
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
221
|
+
let data;
|
|
222
|
+
if (req.method === "GET") {
|
|
223
|
+
data = this.parseGetParams(req);
|
|
224
|
+
} else {
|
|
225
|
+
const body = await readRequestBody(req);
|
|
226
|
+
console.log("MODE CHANGE (POST)", body);
|
|
227
|
+
data = JSON.parse(body);
|
|
228
|
+
}
|
|
200
229
|
const { mode, id, timeout: requestTimeout } = data;
|
|
201
230
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
202
231
|
this.clearModeTimeout();
|
|
@@ -224,7 +253,7 @@ var ProxyServer = class {
|
|
|
224
253
|
async switchMode(mode, id) {
|
|
225
254
|
if (this.currentSession) {
|
|
226
255
|
console.log("Switching mode, saving current session first");
|
|
227
|
-
await this.saveCurrentSession();
|
|
256
|
+
await this.saveCurrentSession(true);
|
|
228
257
|
console.log("Session saved, continuing with mode switch");
|
|
229
258
|
}
|
|
230
259
|
switch (mode) {
|
|
@@ -279,33 +308,41 @@ var ProxyServer = class {
|
|
|
279
308
|
if (timeout && timeout > 0) {
|
|
280
309
|
this.modeTimeout = setTimeout(async () => {
|
|
281
310
|
console.log("Timeout reached, switching back to transparent mode");
|
|
282
|
-
await this.saveCurrentSession();
|
|
311
|
+
await this.saveCurrentSession(true);
|
|
283
312
|
this.switchToTransparentMode();
|
|
284
313
|
this.modeTimeout = null;
|
|
285
314
|
}, timeout);
|
|
286
315
|
}
|
|
287
316
|
}
|
|
288
|
-
async saveCurrentSession() {
|
|
317
|
+
async saveCurrentSession(filterIncomplete = false) {
|
|
289
318
|
if (!this.currentSession) {
|
|
290
319
|
console.log("No current session to save");
|
|
291
320
|
return;
|
|
292
321
|
}
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
322
|
+
if (filterIncomplete) {
|
|
323
|
+
const incompleteCount = this.currentSession.recordings.filter(
|
|
324
|
+
(r) => !r.response
|
|
325
|
+
).length;
|
|
326
|
+
if (incompleteCount > 0) {
|
|
327
|
+
console.log(
|
|
328
|
+
`Removing ${incompleteCount} incomplete recording(s) without responses`
|
|
329
|
+
);
|
|
330
|
+
this.currentSession.recordings = this.currentSession.recordings.filter(
|
|
331
|
+
(r) => r.response
|
|
332
|
+
);
|
|
333
|
+
}
|
|
296
334
|
}
|
|
297
335
|
console.log(
|
|
298
336
|
`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
|
|
299
337
|
);
|
|
300
338
|
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
301
339
|
}
|
|
302
|
-
|
|
340
|
+
saveRequestRecordSync(req, body) {
|
|
303
341
|
if (!this.currentSession) {
|
|
342
|
+
console.log("saveRequestRecordSync: No current session");
|
|
304
343
|
return;
|
|
305
344
|
}
|
|
306
345
|
const key = getReqID(req);
|
|
307
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
308
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
309
346
|
const record = {
|
|
310
347
|
request: {
|
|
311
348
|
method: req.method,
|
|
@@ -315,9 +352,34 @@ var ProxyServer = class {
|
|
|
315
352
|
},
|
|
316
353
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
317
354
|
key,
|
|
318
|
-
sequence:
|
|
355
|
+
sequence: -1
|
|
356
|
+
// Temporary, will be set when response arrives
|
|
319
357
|
};
|
|
320
358
|
this.currentSession.recordings.push(record);
|
|
359
|
+
console.log(
|
|
360
|
+
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
361
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
updateRequestBodySync(req, body) {
|
|
365
|
+
if (!this.currentSession) {
|
|
366
|
+
console.log("updateRequestBodySync: No current session");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const key = getReqID(req);
|
|
370
|
+
const record = this.currentSession.recordings.findLast(
|
|
371
|
+
(r) => r.key === key && !r.response
|
|
372
|
+
);
|
|
373
|
+
if (!record) {
|
|
374
|
+
console.error(
|
|
375
|
+
`updateRequestBodySync: Could not find request record for ${req.method} ${req.url}`
|
|
376
|
+
);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
record.request.body = body || null;
|
|
380
|
+
console.log(
|
|
381
|
+
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
|
|
382
|
+
);
|
|
321
383
|
}
|
|
322
384
|
async recordResponse(req, proxyRes) {
|
|
323
385
|
if (!this.currentSession) {
|
|
@@ -342,59 +404,115 @@ var ProxyServer = class {
|
|
|
342
404
|
headers: proxyRes.headers,
|
|
343
405
|
body: body || null
|
|
344
406
|
};
|
|
345
|
-
await this.saveCurrentSession();
|
|
346
407
|
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
347
408
|
});
|
|
348
409
|
}
|
|
410
|
+
async recordResponseData(req, proxyRes, body) {
|
|
411
|
+
if (!this.currentSession) {
|
|
412
|
+
console.log("recordResponseData: No current session");
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
const key = getReqID(req);
|
|
416
|
+
const record = this.currentSession.recordings.findLast(
|
|
417
|
+
(r) => r.key === key && !r.response
|
|
418
|
+
);
|
|
419
|
+
if (!record) {
|
|
420
|
+
const host = req.headers.host || "unknown";
|
|
421
|
+
const recordsWithKey = this.currentSession.recordings.filter(
|
|
422
|
+
(r) => r.key === key
|
|
423
|
+
);
|
|
424
|
+
console.error(
|
|
425
|
+
`Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
|
|
426
|
+
);
|
|
427
|
+
console.error(
|
|
428
|
+
` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
|
|
429
|
+
);
|
|
430
|
+
console.error(
|
|
431
|
+
` Records with key:`,
|
|
432
|
+
recordsWithKey.map((r) => ({
|
|
433
|
+
seq: r.sequence,
|
|
434
|
+
hasResponse: !!r.response
|
|
435
|
+
}))
|
|
436
|
+
);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
record.response = {
|
|
440
|
+
statusCode: proxyRes.statusCode,
|
|
441
|
+
headers: proxyRes.headers,
|
|
442
|
+
body: body || null
|
|
443
|
+
};
|
|
444
|
+
record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
445
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
446
|
+
record.sequence = currentSequence;
|
|
447
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
448
|
+
console.log(
|
|
449
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
|
|
450
|
+
);
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
349
453
|
async handleReplayRequest(req, res) {
|
|
350
454
|
const key = getReqID(req);
|
|
351
455
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
352
456
|
try {
|
|
353
457
|
const session = await loadRecordingSession(filePath);
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
);
|
|
358
|
-
if (!record) {
|
|
458
|
+
const host = req.headers.host || "unknown";
|
|
459
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
|
|
460
|
+
if (recordsWithKey.length === 0) {
|
|
359
461
|
throw new Error(
|
|
360
|
-
`No recording found for ${key}
|
|
462
|
+
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
361
463
|
);
|
|
362
464
|
}
|
|
465
|
+
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
466
|
+
let record;
|
|
467
|
+
if (recordsWithKey.length > 1) {
|
|
468
|
+
record = recordsWithKey[recordsWithKey.length - 1];
|
|
469
|
+
} else {
|
|
470
|
+
const recordIndex = usageCount % recordsWithKey.length;
|
|
471
|
+
record = recordsWithKey[recordIndex];
|
|
472
|
+
}
|
|
473
|
+
console.log(
|
|
474
|
+
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
|
|
475
|
+
);
|
|
476
|
+
this.replaySequenceMap.set(key, usageCount + 1);
|
|
363
477
|
if (!record.response) {
|
|
364
|
-
throw new Error(
|
|
478
|
+
throw new Error(
|
|
479
|
+
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
480
|
+
);
|
|
365
481
|
}
|
|
366
|
-
this.replaySequenceMap.set(key, currentSequence + 1);
|
|
367
482
|
const { statusCode, headers, body } = record.response;
|
|
368
|
-
const origin = req.headers.origin;
|
|
369
483
|
const responseHeaders = {
|
|
370
484
|
...headers,
|
|
371
|
-
|
|
372
|
-
"access-control-allow-credentials": "true"
|
|
485
|
+
...this.getCorsHeaders(req)
|
|
373
486
|
};
|
|
374
487
|
res.writeHead(statusCode, responseHeaders);
|
|
375
488
|
res.end(body);
|
|
376
|
-
console.log(
|
|
377
|
-
`Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
|
|
378
|
-
);
|
|
379
489
|
} catch (error) {
|
|
380
|
-
this.handleReplayError(res, error, key, filePath);
|
|
490
|
+
this.handleReplayError(req, res, error, key, filePath);
|
|
381
491
|
}
|
|
382
492
|
}
|
|
383
|
-
handleReplayError(res, err, key, filePath) {
|
|
493
|
+
handleReplayError(req, res, err, key, filePath) {
|
|
384
494
|
const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
385
495
|
console.error("Replay error:", err);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
filePath
|
|
496
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
497
|
+
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
498
|
+
"Content-Type": "application/json",
|
|
499
|
+
...corsHeaders
|
|
391
500
|
});
|
|
501
|
+
res.end(
|
|
502
|
+
JSON.stringify({
|
|
503
|
+
error: isFileNotFound ? "Recording file not found" : "Recording not found",
|
|
504
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
|
505
|
+
key,
|
|
506
|
+
filePath
|
|
507
|
+
})
|
|
508
|
+
);
|
|
392
509
|
}
|
|
393
510
|
async handleRequest(req, res) {
|
|
394
511
|
if (req.method === "OPTIONS") {
|
|
395
512
|
return this.handleCorsPreflightRequest(req, res);
|
|
396
513
|
}
|
|
397
|
-
|
|
514
|
+
const urlPath = req.url?.split("?")[0] || "";
|
|
515
|
+
if (urlPath === CONTROL_ENDPOINT) {
|
|
398
516
|
return this.handleControlRequest(req, res);
|
|
399
517
|
}
|
|
400
518
|
if (this.mode === Modes.replay) {
|
|
@@ -403,12 +521,9 @@ var ProxyServer = class {
|
|
|
403
521
|
await this.handleProxyRequest(req, res);
|
|
404
522
|
}
|
|
405
523
|
handleCorsPreflightRequest(req, res) {
|
|
406
|
-
const
|
|
524
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
407
525
|
res.writeHead(HTTP_STATUS_OK, {
|
|
408
|
-
|
|
409
|
-
"Access-Control-Allow-Credentials": "true",
|
|
410
|
-
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
411
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
526
|
+
...corsHeaders,
|
|
412
527
|
"Access-Control-Max-Age": "86400"
|
|
413
528
|
// 24 hours
|
|
414
529
|
});
|
|
@@ -418,6 +533,7 @@ var ProxyServer = class {
|
|
|
418
533
|
const target = this.getTarget();
|
|
419
534
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
420
535
|
if (this.mode === Modes.record) {
|
|
536
|
+
this.saveRequestRecordSync(req, null);
|
|
421
537
|
await this.bufferAndProxyRequest(req, res, target);
|
|
422
538
|
} else {
|
|
423
539
|
this.proxy.web(req, res, { target });
|
|
@@ -428,11 +544,20 @@ var ProxyServer = class {
|
|
|
428
544
|
req.on("data", (chunk) => {
|
|
429
545
|
chunks.push(chunk);
|
|
430
546
|
});
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
547
|
+
try {
|
|
548
|
+
await new Promise((resolve, reject) => {
|
|
549
|
+
req.on("end", () => resolve());
|
|
550
|
+
req.on("error", (err) => reject(err));
|
|
551
|
+
setTimeout(
|
|
552
|
+
() => reject(new Error("Request buffering timeout")),
|
|
553
|
+
3e4
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
} catch (error) {
|
|
557
|
+
console.error("Error buffering request:", error);
|
|
558
|
+
}
|
|
434
559
|
const body = Buffer.concat(chunks).toString("utf8");
|
|
435
|
-
|
|
560
|
+
this.updateRequestBodySync(req, body);
|
|
436
561
|
const targetUrl = new URL(target);
|
|
437
562
|
const isHttps = targetUrl.protocol === "https:";
|
|
438
563
|
const requestModule = isHttps ? https : http;
|
|
@@ -447,9 +572,33 @@ var ProxyServer = class {
|
|
|
447
572
|
},
|
|
448
573
|
(proxyRes) => {
|
|
449
574
|
this.addCorsHeaders(proxyRes, req);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
575
|
+
const responseChunks = [];
|
|
576
|
+
proxyRes.on("data", (chunk) => {
|
|
577
|
+
responseChunks.push(chunk);
|
|
578
|
+
});
|
|
579
|
+
proxyRes.on("end", async () => {
|
|
580
|
+
const responseBody = Buffer.concat(responseChunks);
|
|
581
|
+
const recorded = await this.recordResponseData(
|
|
582
|
+
req,
|
|
583
|
+
proxyRes,
|
|
584
|
+
responseBody.toString("utf8")
|
|
585
|
+
);
|
|
586
|
+
const responseHeaders = {
|
|
587
|
+
...proxyRes.headers,
|
|
588
|
+
...this.getCorsHeaders(req)
|
|
589
|
+
};
|
|
590
|
+
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
591
|
+
res.end(responseBody);
|
|
592
|
+
if (recorded) {
|
|
593
|
+
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
proxyRes.on("error", (err) => {
|
|
597
|
+
console.error("Proxy response error:", err);
|
|
598
|
+
if (!res.headersSent) {
|
|
599
|
+
this.handleProxyError(err, req, res);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
453
602
|
}
|
|
454
603
|
);
|
|
455
604
|
proxyReq.on("error", (err) => {
|
|
@@ -501,9 +650,6 @@ var ProxyServer = class {
|
|
|
501
650
|
if (backendWs.readyState === WebSocket.OPEN) {
|
|
502
651
|
backendWs.send(message);
|
|
503
652
|
}
|
|
504
|
-
this.saveCurrentSession().catch((error) => {
|
|
505
|
-
console.error("Failed to save WebSocket recording:", error);
|
|
506
|
-
});
|
|
507
653
|
});
|
|
508
654
|
backendWs.on("message", (data) => {
|
|
509
655
|
const message = data.toString();
|
|
@@ -515,9 +661,6 @@ var ProxyServer = class {
|
|
|
515
661
|
if (clientWs.readyState === WebSocket.OPEN) {
|
|
516
662
|
clientWs.send(message);
|
|
517
663
|
}
|
|
518
|
-
this.saveCurrentSession().catch((error) => {
|
|
519
|
-
console.error("Failed to save WebSocket recording:", error);
|
|
520
|
-
});
|
|
521
664
|
});
|
|
522
665
|
clientWs.on("error", (err) => {
|
|
523
666
|
console.error("Client WebSocket error:", err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"README.md",
|
|
33
33
|
"LICENSE"
|
|
34
34
|
],
|
|
35
|
+
"pnpm": {
|
|
36
|
+
"onlyBuiltDependencies": ["esbuild"]
|
|
37
|
+
},
|
|
35
38
|
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
|
|
36
39
|
"scripts": {
|
|
37
40
|
"start": "node dist/proxy.js",
|
|
@@ -42,7 +45,8 @@
|
|
|
42
45
|
"lint:fix": "eslint src --ext .ts --fix",
|
|
43
46
|
"typecheck": "tsc --noEmit",
|
|
44
47
|
"test": "vitest",
|
|
45
|
-
"test:run": "vitest run"
|
|
48
|
+
"test:run": "vitest run",
|
|
49
|
+
"test:coverage": "vitest run --coverage"
|
|
46
50
|
},
|
|
47
51
|
"keywords": [
|
|
48
52
|
"playwright",
|
|
@@ -91,6 +95,7 @@
|
|
|
91
95
|
"@types/ws": "^8.18.1",
|
|
92
96
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
93
97
|
"@typescript-eslint/parser": "^8.0.0",
|
|
98
|
+
"@vitest/coverage-v8": "^4.0.8",
|
|
94
99
|
"eslint": "^9.0.0",
|
|
95
100
|
"eslint-config-prettier": "^10.1.8",
|
|
96
101
|
"eslint-plugin-prettier": "^5.5.4",
|
|
@@ -101,7 +106,7 @@
|
|
|
101
106
|
"tsup": "^8.5.0",
|
|
102
107
|
"tsx": "^4.19.0",
|
|
103
108
|
"typescript": "^5.6.0",
|
|
104
|
-
"vitest": "^
|
|
109
|
+
"vitest": "^4.0.8",
|
|
105
110
|
"ws": "^8.18.3"
|
|
106
111
|
}
|
|
107
112
|
}
|