@testdino/playwright 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +212 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +211 -86
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +332 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +332 -94
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -57,6 +57,7 @@ function isQuotaError(error) {
|
|
|
57
57
|
|
|
58
58
|
// src/streaming/websocket.ts
|
|
59
59
|
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
60
|
+
var DEFAULT_ACK_TIMEOUT_MS = 5e3;
|
|
60
61
|
var WebSocketClient = class {
|
|
61
62
|
ws = null;
|
|
62
63
|
options;
|
|
@@ -65,11 +66,16 @@ var WebSocketClient = class {
|
|
|
65
66
|
isConnecting = false;
|
|
66
67
|
isClosed = false;
|
|
67
68
|
pingInterval = null;
|
|
69
|
+
pendingAcks = /* @__PURE__ */ new Map();
|
|
70
|
+
ackTimeout = DEFAULT_ACK_TIMEOUT_MS;
|
|
71
|
+
debug;
|
|
68
72
|
constructor(options) {
|
|
73
|
+
this.debug = options.debug ?? false;
|
|
69
74
|
this.options = {
|
|
70
75
|
sessionId: "",
|
|
71
76
|
maxRetries: 5,
|
|
72
77
|
retryDelay: 1e3,
|
|
78
|
+
debug: false,
|
|
73
79
|
onConnected: () => {
|
|
74
80
|
},
|
|
75
81
|
onDisconnected: () => {
|
|
@@ -100,9 +106,11 @@ var WebSocketClient = class {
|
|
|
100
106
|
let serverReady = false;
|
|
101
107
|
const handshakeTimeout = setTimeout(() => {
|
|
102
108
|
if (!serverReady) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
if (this.debug) {
|
|
110
|
+
console.warn(
|
|
111
|
+
`\u26A0\uFE0F TestDino: WebSocket handshake timeout \u2014 server did not send 'connected' within ${HANDSHAKE_TIMEOUT_MS}ms. Resolving anyway.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
106
114
|
serverReady = true;
|
|
107
115
|
this.isConnecting = false;
|
|
108
116
|
this.options.onConnected();
|
|
@@ -173,6 +181,7 @@ var WebSocketClient = class {
|
|
|
173
181
|
}
|
|
174
182
|
/**
|
|
175
183
|
* Send multiple events in batch (parallel for speed)
|
|
184
|
+
* Note: Uses Promise.all intentionally - if one send fails, connection is broken
|
|
176
185
|
*/
|
|
177
186
|
async sendBatch(events) {
|
|
178
187
|
if (!this.ws || this.ws.readyState !== WebSocket__default.default.OPEN) {
|
|
@@ -180,6 +189,36 @@ var WebSocketClient = class {
|
|
|
180
189
|
}
|
|
181
190
|
await Promise.all(events.map((event) => this.send(event)));
|
|
182
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Send event and wait for server ACK
|
|
194
|
+
* Use this for critical events like run:end where delivery must be confirmed
|
|
195
|
+
*/
|
|
196
|
+
async sendAndWaitForAck(event, timeout = this.ackTimeout) {
|
|
197
|
+
if (!this.ws || this.ws.readyState !== WebSocket__default.default.OPEN) {
|
|
198
|
+
throw new Error("WebSocket is not connected");
|
|
199
|
+
}
|
|
200
|
+
const sequence = event.sequence;
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
const timer = setTimeout(() => {
|
|
203
|
+
this.pendingAcks.delete(sequence);
|
|
204
|
+
reject(new Error(`ACK timeout for sequence ${sequence} after ${timeout}ms`));
|
|
205
|
+
}, timeout);
|
|
206
|
+
this.pendingAcks.set(sequence, { resolve, reject, timer });
|
|
207
|
+
this.ws.send(JSON.stringify(event), (error) => {
|
|
208
|
+
if (error) {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
this.pendingAcks.delete(sequence);
|
|
211
|
+
reject(error);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Set ACK timeout for sendAndWaitForAck
|
|
218
|
+
*/
|
|
219
|
+
setAckTimeout(timeout) {
|
|
220
|
+
this.ackTimeout = timeout;
|
|
221
|
+
}
|
|
183
222
|
/**
|
|
184
223
|
* Check if WebSocket is connected
|
|
185
224
|
*/
|
|
@@ -193,11 +232,22 @@ var WebSocketClient = class {
|
|
|
193
232
|
this.isClosed = true;
|
|
194
233
|
this.stopPing();
|
|
195
234
|
this.clearReconnectTimer();
|
|
235
|
+
this.clearPendingAcks();
|
|
196
236
|
if (this.ws) {
|
|
197
237
|
this.ws.close();
|
|
198
238
|
this.ws = null;
|
|
199
239
|
}
|
|
200
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Clear all pending ACKs (reject them with connection closed error)
|
|
243
|
+
*/
|
|
244
|
+
clearPendingAcks() {
|
|
245
|
+
for (const [sequence, pending] of this.pendingAcks) {
|
|
246
|
+
clearTimeout(pending.timer);
|
|
247
|
+
pending.reject(new Error(`Connection closed while waiting for ACK on sequence ${sequence}`));
|
|
248
|
+
}
|
|
249
|
+
this.pendingAcks.clear();
|
|
250
|
+
}
|
|
201
251
|
/**
|
|
202
252
|
* Handle incoming messages
|
|
203
253
|
*/
|
|
@@ -206,6 +256,16 @@ var WebSocketClient = class {
|
|
|
206
256
|
const message = JSON.parse(data);
|
|
207
257
|
if (message.type === "connected") {
|
|
208
258
|
} else if (message.type === "ack") {
|
|
259
|
+
const sequence = typeof message.sequence === "number" ? message.sequence : void 0;
|
|
260
|
+
if (sequence === void 0) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (this.pendingAcks.has(sequence)) {
|
|
264
|
+
const pending = this.pendingAcks.get(sequence);
|
|
265
|
+
clearTimeout(pending.timer);
|
|
266
|
+
this.pendingAcks.delete(sequence);
|
|
267
|
+
pending.resolve();
|
|
268
|
+
}
|
|
209
269
|
} else if (message.type === "nack") {
|
|
210
270
|
const nack = message;
|
|
211
271
|
if (isServerError(nack.error)) {
|
|
@@ -247,7 +307,9 @@ var WebSocketClient = class {
|
|
|
247
307
|
}
|
|
248
308
|
}
|
|
249
309
|
} catch (error) {
|
|
250
|
-
|
|
310
|
+
if (this.debug) {
|
|
311
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
312
|
+
}
|
|
251
313
|
}
|
|
252
314
|
}
|
|
253
315
|
/**
|
|
@@ -255,6 +317,7 @@ var WebSocketClient = class {
|
|
|
255
317
|
*/
|
|
256
318
|
handleClose(_code, _reason) {
|
|
257
319
|
this.stopPing();
|
|
320
|
+
this.clearPendingAcks();
|
|
258
321
|
this.options.onDisconnected();
|
|
259
322
|
if (this.isClosed) {
|
|
260
323
|
return;
|
|
@@ -274,7 +337,9 @@ var WebSocketClient = class {
|
|
|
274
337
|
this.reconnectAttempts++;
|
|
275
338
|
this.reconnectTimer = setTimeout(() => {
|
|
276
339
|
this.connect().catch((error) => {
|
|
277
|
-
|
|
340
|
+
if (this.debug) {
|
|
341
|
+
console.error("Reconnection failed:", error);
|
|
342
|
+
}
|
|
278
343
|
});
|
|
279
344
|
}, delay);
|
|
280
345
|
}
|
|
@@ -308,6 +373,16 @@ var WebSocketClient = class {
|
|
|
308
373
|
}
|
|
309
374
|
}
|
|
310
375
|
};
|
|
376
|
+
|
|
377
|
+
// src/utils/index.ts
|
|
378
|
+
function sleep(ms) {
|
|
379
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
380
|
+
}
|
|
381
|
+
function isDebugEnabled() {
|
|
382
|
+
return process.env.TESTDINO_DEBUG === "true" || process.env.TESTDINO_DEBUG === "1" || process.env.DEBUG === "true";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/streaming/http.ts
|
|
311
386
|
var HttpClient = class {
|
|
312
387
|
client;
|
|
313
388
|
options;
|
|
@@ -354,7 +429,7 @@ var HttpClient = class {
|
|
|
354
429
|
lastError = new Error(this.getErrorMessage(error));
|
|
355
430
|
if (attempt < this.options.maxRetries - 1) {
|
|
356
431
|
const delay = this.options.retryDelay * Math.pow(2, attempt);
|
|
357
|
-
await
|
|
432
|
+
await sleep(delay);
|
|
358
433
|
}
|
|
359
434
|
}
|
|
360
435
|
}
|
|
@@ -378,12 +453,6 @@ var HttpClient = class {
|
|
|
378
453
|
}
|
|
379
454
|
return String(error);
|
|
380
455
|
}
|
|
381
|
-
/**
|
|
382
|
-
* Sleep utility for retry delays
|
|
383
|
-
*/
|
|
384
|
-
sleep(ms) {
|
|
385
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
386
|
-
}
|
|
387
456
|
};
|
|
388
457
|
|
|
389
458
|
// src/streaming/buffer.ts
|
|
@@ -571,7 +640,7 @@ var GitMetadataCollector = class extends BaseMetadataCollector {
|
|
|
571
640
|
if (!isGitRepo) {
|
|
572
641
|
return this.getEmptyMetadata();
|
|
573
642
|
}
|
|
574
|
-
const results = await Promise.
|
|
643
|
+
const results = await Promise.allSettled([
|
|
575
644
|
this.getBranch(),
|
|
576
645
|
this.getCommitHash(),
|
|
577
646
|
this.getCommitMessage(),
|
|
@@ -581,9 +650,15 @@ var GitMetadataCollector = class extends BaseMetadataCollector {
|
|
|
581
650
|
this.getRepoUrl(),
|
|
582
651
|
this.isDirtyWorkingTree()
|
|
583
652
|
]);
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
653
|
+
const extractedValues = results.map((r) => r.status === "fulfilled" ? r.value : void 0);
|
|
654
|
+
let branch = extractedValues[0];
|
|
655
|
+
let hash = extractedValues[1];
|
|
656
|
+
let message = extractedValues[2];
|
|
657
|
+
let author = extractedValues[3];
|
|
658
|
+
let email = extractedValues[4];
|
|
659
|
+
let timestamp = extractedValues[5];
|
|
660
|
+
const repoUrl = extractedValues[6];
|
|
661
|
+
const isDirty = extractedValues[7];
|
|
587
662
|
let prMetadata;
|
|
588
663
|
if (process.env.GITHUB_EVENT_NAME === "pull_request") {
|
|
589
664
|
const eventData = await this.readGitHubEventFile();
|
|
@@ -1249,9 +1324,13 @@ var PlaywrightMetadataCollector = class extends BaseMetadataCollector {
|
|
|
1249
1324
|
metadata.workers = config.workers;
|
|
1250
1325
|
}
|
|
1251
1326
|
if (Array.isArray(config.projects) && config.projects.length > 0) {
|
|
1252
|
-
const
|
|
1253
|
-
if (
|
|
1254
|
-
metadata.projects =
|
|
1327
|
+
const projectConfigs = config.projects.filter((project) => this.isNonEmptyString(project.name)).map((project) => this.extractProjectConfig(project));
|
|
1328
|
+
if (projectConfigs.length > 0) {
|
|
1329
|
+
metadata.projects = projectConfigs;
|
|
1330
|
+
const browsers = this.extractBrowsersFromProjects(config.projects);
|
|
1331
|
+
if (browsers.length > 0) {
|
|
1332
|
+
metadata.browsers = browsers;
|
|
1333
|
+
}
|
|
1255
1334
|
}
|
|
1256
1335
|
}
|
|
1257
1336
|
if (config.reportSlowTests && typeof config.reportSlowTests === "object") {
|
|
@@ -1277,6 +1356,151 @@ var PlaywrightMetadataCollector = class extends BaseMetadataCollector {
|
|
|
1277
1356
|
}
|
|
1278
1357
|
return metadata;
|
|
1279
1358
|
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Extract project configuration from FullProject
|
|
1361
|
+
*/
|
|
1362
|
+
extractProjectConfig(project) {
|
|
1363
|
+
const config = {
|
|
1364
|
+
name: project.name || ""
|
|
1365
|
+
};
|
|
1366
|
+
if (this.isNonEmptyString(project.testDir)) {
|
|
1367
|
+
config.testDir = project.testDir;
|
|
1368
|
+
}
|
|
1369
|
+
if (typeof project.timeout === "number") {
|
|
1370
|
+
config.timeout = project.timeout;
|
|
1371
|
+
}
|
|
1372
|
+
if (typeof project.retries === "number") {
|
|
1373
|
+
config.retries = project.retries;
|
|
1374
|
+
}
|
|
1375
|
+
if (typeof project.repeatEach === "number" && project.repeatEach > 1) {
|
|
1376
|
+
config.repeatEach = project.repeatEach;
|
|
1377
|
+
}
|
|
1378
|
+
if (Array.isArray(project.dependencies) && project.dependencies.length > 0) {
|
|
1379
|
+
config.dependencies = project.dependencies;
|
|
1380
|
+
}
|
|
1381
|
+
if (project.grep) {
|
|
1382
|
+
const patterns = Array.isArray(project.grep) ? project.grep : [project.grep];
|
|
1383
|
+
const grepStrings = patterns.map((p) => p.source);
|
|
1384
|
+
if (grepStrings.length > 0) {
|
|
1385
|
+
config.grep = grepStrings;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (project.use) {
|
|
1389
|
+
const useOptions = this.extractUseOptions(project.use);
|
|
1390
|
+
if (Object.keys(useOptions).length > 0) {
|
|
1391
|
+
config.use = useOptions;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return config;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Extract use options from project.use
|
|
1398
|
+
*/
|
|
1399
|
+
extractUseOptions(use) {
|
|
1400
|
+
const options = {};
|
|
1401
|
+
if (!use) return options;
|
|
1402
|
+
if (this.isNonEmptyString(use.channel)) {
|
|
1403
|
+
options.channel = use.channel;
|
|
1404
|
+
}
|
|
1405
|
+
const browserName = this.resolveBrowserName(use.browserName, use.defaultBrowserType, use.channel);
|
|
1406
|
+
if (browserName) {
|
|
1407
|
+
options.browserName = browserName;
|
|
1408
|
+
}
|
|
1409
|
+
if (typeof use.headless === "boolean") {
|
|
1410
|
+
options.headless = use.headless;
|
|
1411
|
+
}
|
|
1412
|
+
if (use.viewport && typeof use.viewport === "object") {
|
|
1413
|
+
options.viewport = {
|
|
1414
|
+
width: use.viewport.width,
|
|
1415
|
+
height: use.viewport.height
|
|
1416
|
+
};
|
|
1417
|
+
} else if (use.viewport === null) {
|
|
1418
|
+
options.viewport = null;
|
|
1419
|
+
}
|
|
1420
|
+
if (this.isNonEmptyString(use.baseURL)) {
|
|
1421
|
+
options.baseURL = use.baseURL;
|
|
1422
|
+
}
|
|
1423
|
+
const trace = this.normalizeArtifactMode(use.trace);
|
|
1424
|
+
if (trace) {
|
|
1425
|
+
options.trace = trace;
|
|
1426
|
+
}
|
|
1427
|
+
const screenshot = this.normalizeArtifactMode(use.screenshot);
|
|
1428
|
+
if (screenshot) {
|
|
1429
|
+
options.screenshot = screenshot;
|
|
1430
|
+
}
|
|
1431
|
+
const video = this.normalizeArtifactMode(use.video);
|
|
1432
|
+
if (video) {
|
|
1433
|
+
options.video = video;
|
|
1434
|
+
}
|
|
1435
|
+
if (typeof use.isMobile === "boolean") {
|
|
1436
|
+
options.isMobile = use.isMobile;
|
|
1437
|
+
}
|
|
1438
|
+
if (this.isNonEmptyString(use.locale)) {
|
|
1439
|
+
options.locale = use.locale;
|
|
1440
|
+
}
|
|
1441
|
+
return options;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Normalize artifact mode (trace/screenshot/video can be string or { mode: string })
|
|
1445
|
+
*/
|
|
1446
|
+
normalizeArtifactMode(value) {
|
|
1447
|
+
if (!value) return void 0;
|
|
1448
|
+
if (typeof value === "string" && value !== "off") {
|
|
1449
|
+
return value;
|
|
1450
|
+
}
|
|
1451
|
+
if (typeof value === "object" && value !== null && "mode" in value) {
|
|
1452
|
+
const mode = value.mode;
|
|
1453
|
+
if (mode && mode !== "off") {
|
|
1454
|
+
return mode;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return void 0;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Resolve browserName from explicit value, defaultBrowserType (device presets), or channel
|
|
1461
|
+
* Priority: browserName > defaultBrowserType > channel inference
|
|
1462
|
+
*/
|
|
1463
|
+
resolveBrowserName(browserName, defaultBrowserType, channel) {
|
|
1464
|
+
const validBrowsers = ["chromium", "firefox", "webkit"];
|
|
1465
|
+
if (browserName && validBrowsers.includes(browserName)) {
|
|
1466
|
+
return browserName;
|
|
1467
|
+
}
|
|
1468
|
+
if (defaultBrowserType && validBrowsers.includes(defaultBrowserType)) {
|
|
1469
|
+
return defaultBrowserType;
|
|
1470
|
+
}
|
|
1471
|
+
if (channel) {
|
|
1472
|
+
if ([
|
|
1473
|
+
"chrome",
|
|
1474
|
+
"chrome-beta",
|
|
1475
|
+
"chrome-dev",
|
|
1476
|
+
"chrome-canary",
|
|
1477
|
+
"msedge",
|
|
1478
|
+
"msedge-beta",
|
|
1479
|
+
"msedge-dev",
|
|
1480
|
+
"msedge-canary"
|
|
1481
|
+
].includes(channel)) {
|
|
1482
|
+
return "chromium";
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return void 0;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Extract unique browsers from all projects
|
|
1489
|
+
*/
|
|
1490
|
+
extractBrowsersFromProjects(projects) {
|
|
1491
|
+
const browsers = /* @__PURE__ */ new Set();
|
|
1492
|
+
for (const project of projects) {
|
|
1493
|
+
const browserName = this.resolveBrowserName(
|
|
1494
|
+
project.use?.browserName,
|
|
1495
|
+
project.use?.defaultBrowserType,
|
|
1496
|
+
project.use?.channel
|
|
1497
|
+
);
|
|
1498
|
+
if (browserName) {
|
|
1499
|
+
browsers.add(browserName);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return Array.from(browsers);
|
|
1503
|
+
}
|
|
1280
1504
|
/**
|
|
1281
1505
|
* Build skeleton from Suite
|
|
1282
1506
|
*/
|
|
@@ -1383,9 +1607,6 @@ var MetadataAggregator = class {
|
|
|
1383
1607
|
*/
|
|
1384
1608
|
async collectAll() {
|
|
1385
1609
|
const startTime = Date.now();
|
|
1386
|
-
if (this.options.debug) {
|
|
1387
|
-
console.log(`\u{1F50D} TestDino: Starting metadata collection with ${this.collectors.length} collectors`);
|
|
1388
|
-
}
|
|
1389
1610
|
const settledResults = await Promise.allSettled(
|
|
1390
1611
|
this.collectors.map(
|
|
1391
1612
|
(collector) => this.withTimeout(collector.collectWithResult(), this.options.timeout, "Metadata collection")
|
|
@@ -1413,11 +1634,6 @@ var MetadataAggregator = class {
|
|
|
1413
1634
|
const totalDuration = Date.now() - startTime;
|
|
1414
1635
|
const successCount = results.filter((r) => r.success).length;
|
|
1415
1636
|
const failureCount = results.length - successCount;
|
|
1416
|
-
if (this.options.debug) {
|
|
1417
|
-
console.log(
|
|
1418
|
-
`\u2705 TestDino: Metadata collection completed in ${totalDuration}ms (${successCount}/${results.length} successful)`
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
1637
|
return {
|
|
1422
1638
|
metadata,
|
|
1423
1639
|
results,
|
|
@@ -1516,7 +1732,7 @@ var SASTokenClient = class {
|
|
|
1516
1732
|
lastError = new Error(this.getErrorMessage(error));
|
|
1517
1733
|
if (attempt < this.options.maxRetries - 1) {
|
|
1518
1734
|
const delay = this.options.retryDelay * Math.pow(2, attempt);
|
|
1519
|
-
await
|
|
1735
|
+
await sleep(delay);
|
|
1520
1736
|
}
|
|
1521
1737
|
}
|
|
1522
1738
|
}
|
|
@@ -1543,12 +1759,6 @@ var SASTokenClient = class {
|
|
|
1543
1759
|
}
|
|
1544
1760
|
return String(error);
|
|
1545
1761
|
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Sleep utility for retry delays
|
|
1548
|
-
*/
|
|
1549
|
-
sleep(ms) {
|
|
1550
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1551
|
-
}
|
|
1552
1762
|
};
|
|
1553
1763
|
var ArtifactUploader = class {
|
|
1554
1764
|
sasToken;
|
|
@@ -1683,7 +1893,7 @@ var ArtifactUploader = class {
|
|
|
1683
1893
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1684
1894
|
if (attempt < this.options.maxRetries - 1) {
|
|
1685
1895
|
const delay = 1e3 * Math.pow(2, attempt);
|
|
1686
|
-
await
|
|
1896
|
+
await sleep(delay);
|
|
1687
1897
|
}
|
|
1688
1898
|
}
|
|
1689
1899
|
}
|
|
@@ -1706,12 +1916,6 @@ var ArtifactUploader = class {
|
|
|
1706
1916
|
maxBodyLength: Infinity
|
|
1707
1917
|
});
|
|
1708
1918
|
}
|
|
1709
|
-
/**
|
|
1710
|
-
* Sleep utility for retry delays
|
|
1711
|
-
*/
|
|
1712
|
-
sleep(ms) {
|
|
1713
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1714
|
-
}
|
|
1715
1919
|
/**
|
|
1716
1920
|
* Check if SAS token is still valid
|
|
1717
1921
|
*/
|
|
@@ -1728,6 +1932,19 @@ var ArtifactUploader = class {
|
|
|
1728
1932
|
}
|
|
1729
1933
|
};
|
|
1730
1934
|
|
|
1935
|
+
// src/reporter/log.ts
|
|
1936
|
+
var createReporterLog = (options) => ({
|
|
1937
|
+
success: (msg) => console.log(`\u2705 TestDino: ${msg}`),
|
|
1938
|
+
warn: (msg) => console.warn(`\u26A0\uFE0F TestDino: ${msg}`),
|
|
1939
|
+
error: (msg) => console.error(`\u274C TestDino: ${msg}`),
|
|
1940
|
+
info: (msg) => console.log(`\u2139\uFE0F TestDino: ${msg}`),
|
|
1941
|
+
debug: (msg) => {
|
|
1942
|
+
if (options.debug) {
|
|
1943
|
+
console.log(`\u{1F50D} TestDino: ${msg}`);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1731
1948
|
// src/reporter/index.ts
|
|
1732
1949
|
var MAX_CONSOLE_CHUNK_SIZE = 1e4;
|
|
1733
1950
|
var MAX_BUFFER_SIZE = 10;
|
|
@@ -1759,10 +1976,13 @@ var TestdinoReporter = class {
|
|
|
1759
1976
|
initFailed = false;
|
|
1760
1977
|
// Promises for onTestEnd; must be awaited in onEnd to prevent data loss
|
|
1761
1978
|
pendingTestEndPromises = /* @__PURE__ */ new Set();
|
|
1979
|
+
// Logger for consistent output
|
|
1980
|
+
log;
|
|
1762
1981
|
constructor(config = {}) {
|
|
1763
1982
|
const cliConfig = this.loadCliConfig();
|
|
1764
1983
|
this.config = { ...config, ...cliConfig };
|
|
1765
1984
|
this.runId = crypto.randomUUID();
|
|
1985
|
+
this.log = createReporterLog({ debug: this.config.debug ?? false });
|
|
1766
1986
|
this.buffer = new EventBuffer({
|
|
1767
1987
|
maxSize: MAX_BUFFER_SIZE,
|
|
1768
1988
|
onFlush: async (events) => {
|
|
@@ -1806,7 +2026,7 @@ var TestdinoReporter = class {
|
|
|
1806
2026
|
}
|
|
1807
2027
|
return mappedConfig;
|
|
1808
2028
|
} catch (error) {
|
|
1809
|
-
if (
|
|
2029
|
+
if (isDebugEnabled()) {
|
|
1810
2030
|
console.warn(
|
|
1811
2031
|
"\u26A0\uFE0F TestDino: Failed to load CLI config:",
|
|
1812
2032
|
error instanceof Error ? error.message : String(error)
|
|
@@ -1821,7 +2041,7 @@ var TestdinoReporter = class {
|
|
|
1821
2041
|
async onBegin(config, suite) {
|
|
1822
2042
|
if (config && this.isDuplicateInstance(config.reporter)) {
|
|
1823
2043
|
if (this.config.debug) {
|
|
1824
|
-
|
|
2044
|
+
this.log.debug("Reporter already configured in playwright.config, skipping duplicate instance");
|
|
1825
2045
|
}
|
|
1826
2046
|
return;
|
|
1827
2047
|
}
|
|
@@ -1861,19 +2081,20 @@ var TestdinoReporter = class {
|
|
|
1861
2081
|
this.httpClient = new HttpClient({ token, serverUrl });
|
|
1862
2082
|
const auth = await this.httpClient.authenticate();
|
|
1863
2083
|
this.sessionId = auth.sessionId;
|
|
1864
|
-
|
|
2084
|
+
this.log.success("Authenticated successfully");
|
|
1865
2085
|
if (this.config.debug) {
|
|
1866
|
-
|
|
2086
|
+
this.log.debug(`Session ${this.sessionId} \u2014 reusing for WebSocket`);
|
|
1867
2087
|
}
|
|
1868
2088
|
this.wsClient = new WebSocketClient({
|
|
1869
2089
|
token,
|
|
1870
2090
|
sessionId: this.sessionId ?? void 0,
|
|
1871
2091
|
serverUrl: this.getWebSocketUrl(),
|
|
2092
|
+
debug: this.config.debug,
|
|
1872
2093
|
onConnected: () => {
|
|
1873
|
-
|
|
2094
|
+
this.log.debug("WebSocket connected");
|
|
1874
2095
|
},
|
|
1875
2096
|
onDisconnected: () => {
|
|
1876
|
-
|
|
2097
|
+
this.log.debug("WebSocket disconnected");
|
|
1877
2098
|
},
|
|
1878
2099
|
onError: (error) => {
|
|
1879
2100
|
if (isQuotaError(error)) {
|
|
@@ -1883,14 +2104,14 @@ var TestdinoReporter = class {
|
|
|
1883
2104
|
this.printQuotaError(error);
|
|
1884
2105
|
}
|
|
1885
2106
|
} else {
|
|
1886
|
-
|
|
2107
|
+
this.log.error(`WebSocket error: ${error.message}`);
|
|
1887
2108
|
}
|
|
1888
2109
|
}
|
|
1889
2110
|
});
|
|
1890
2111
|
try {
|
|
1891
2112
|
await this.wsClient.connect();
|
|
1892
2113
|
} catch {
|
|
1893
|
-
|
|
2114
|
+
this.log.warn("WebSocket connection failed, using HTTP fallback");
|
|
1894
2115
|
this.useHttpFallback = true;
|
|
1895
2116
|
}
|
|
1896
2117
|
this.artifactsEnabled = this.config.artifacts !== false;
|
|
@@ -1906,18 +2127,13 @@ var TestdinoReporter = class {
|
|
|
1906
2127
|
...this.getEventMetadata()
|
|
1907
2128
|
};
|
|
1908
2129
|
if (this.config.debug) {
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
console.log(` skeleton.totalTests: ${metadata.skeleton.totalTests}`);
|
|
1915
|
-
console.log(` skeleton.suites: ${metadata.skeleton.suites?.length ?? 0}`);
|
|
1916
|
-
console.log(` skeleton: ${JSON.stringify(metadata.skeleton, null, 2)}`);
|
|
1917
|
-
} else {
|
|
1918
|
-
console.log(` skeleton: (not available)`);
|
|
1919
|
-
}
|
|
2130
|
+
const shardInfo = config?.shard ? `${config.shard.current}/${config.shard.total}` : "none";
|
|
2131
|
+
const skeletonInfo = metadata.skeleton ? `${metadata.skeleton.totalTests} tests, ${metadata.skeleton.suites?.length ?? 0} suites` : "not available";
|
|
2132
|
+
this.log.debug(
|
|
2133
|
+
`run:begin runId=${this.runId} ciRunId=${this.config.ciRunId ?? "none"} shard=${shardInfo} skeleton=(${skeletonInfo})`
|
|
2134
|
+
);
|
|
1920
2135
|
}
|
|
2136
|
+
console.log("[TEMP DEBUG] run:begin metadata:", JSON.stringify(metadata, null, 2));
|
|
1921
2137
|
await this.sendEvents([beginEvent]);
|
|
1922
2138
|
this.registerSignalHandlers();
|
|
1923
2139
|
return true;
|
|
@@ -2058,10 +2274,9 @@ var TestdinoReporter = class {
|
|
|
2058
2274
|
await this.buffer.add(event);
|
|
2059
2275
|
}
|
|
2060
2276
|
/**
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
*/
|
|
2277
|
+
* Called after each test.
|
|
2278
|
+
* Playwright does not await onTestEnd promises—pending work is awaited in onEnd.
|
|
2279
|
+
*/
|
|
2065
2280
|
onTestEnd(test, result) {
|
|
2066
2281
|
if (!this.initPromise || this.initFailed) return;
|
|
2067
2282
|
const workPromise = this.processTestEnd(test, result);
|
|
@@ -2110,10 +2325,7 @@ var TestdinoReporter = class {
|
|
|
2110
2325
|
};
|
|
2111
2326
|
await this.buffer.add(event);
|
|
2112
2327
|
} catch (error) {
|
|
2113
|
-
|
|
2114
|
-
"\u274C TestDino: Failed to process test:end event:",
|
|
2115
|
-
error instanceof Error ? error.message : String(error)
|
|
2116
|
-
);
|
|
2328
|
+
this.log.error(`Failed to process test:end event: ${error instanceof Error ? error.message : String(error)}`);
|
|
2117
2329
|
}
|
|
2118
2330
|
}
|
|
2119
2331
|
/**
|
|
@@ -2124,7 +2336,7 @@ var TestdinoReporter = class {
|
|
|
2124
2336
|
if (this.pendingTestEndPromises.size > 0) {
|
|
2125
2337
|
await Promise.allSettled(Array.from(this.pendingTestEndPromises));
|
|
2126
2338
|
}
|
|
2127
|
-
|
|
2339
|
+
this.log.success("Tests completed (quota limit reached; not streamed to TestDino)");
|
|
2128
2340
|
this.wsClient?.close();
|
|
2129
2341
|
this.removeSignalHandlers();
|
|
2130
2342
|
return;
|
|
@@ -2139,9 +2351,7 @@ var TestdinoReporter = class {
|
|
|
2139
2351
|
return;
|
|
2140
2352
|
}
|
|
2141
2353
|
if (this.pendingTestEndPromises.size > 0) {
|
|
2142
|
-
|
|
2143
|
-
console.log(`\u{1F50D} TestDino: Waiting for ${this.pendingTestEndPromises.size} pending test:end events...`);
|
|
2144
|
-
}
|
|
2354
|
+
this.log.debug(`Waiting for ${this.pendingTestEndPromises.size} pending test:end events...`);
|
|
2145
2355
|
await Promise.allSettled(Array.from(this.pendingTestEndPromises));
|
|
2146
2356
|
}
|
|
2147
2357
|
const event = {
|
|
@@ -2156,12 +2366,43 @@ var TestdinoReporter = class {
|
|
|
2156
2366
|
// Shard information
|
|
2157
2367
|
shard: this.shardInfo
|
|
2158
2368
|
};
|
|
2159
|
-
await this.buffer.add(event);
|
|
2160
2369
|
try {
|
|
2161
2370
|
await this.buffer.flush();
|
|
2162
|
-
console.log("\u2705 TestDino: All events sent successfully");
|
|
2163
2371
|
} catch (error) {
|
|
2164
|
-
|
|
2372
|
+
this.log.error(`Failed to flush buffered events: ${error}`);
|
|
2373
|
+
}
|
|
2374
|
+
let delivered = false;
|
|
2375
|
+
try {
|
|
2376
|
+
if (this.wsClient?.isConnected()) {
|
|
2377
|
+
await this.wsClient.sendAndWaitForAck(event);
|
|
2378
|
+
this.log.success("All events delivered (server acknowledged)");
|
|
2379
|
+
delivered = true;
|
|
2380
|
+
} else if (this.httpClient) {
|
|
2381
|
+
await this.httpClient.sendEvents([event]);
|
|
2382
|
+
this.log.success("All events sent");
|
|
2383
|
+
delivered = true;
|
|
2384
|
+
} else {
|
|
2385
|
+
this.log.warn("No connection available to send run:end event");
|
|
2386
|
+
}
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2389
|
+
if (errorMessage.includes("ACK timeout") || errorMessage.includes("Connection closed")) {
|
|
2390
|
+
if (this.httpClient && !delivered) {
|
|
2391
|
+
try {
|
|
2392
|
+
this.log.warn("WebSocket ACK timeout, retrying via HTTP...");
|
|
2393
|
+
await this.httpClient.sendEvents([event]);
|
|
2394
|
+
this.log.success("All events sent (HTTP fallback)");
|
|
2395
|
+
delivered = true;
|
|
2396
|
+
} catch (httpError) {
|
|
2397
|
+
const httpErrorMessage = httpError instanceof Error ? httpError.message : String(httpError);
|
|
2398
|
+
this.log.error(`HTTP fallback also failed: ${httpErrorMessage}`);
|
|
2399
|
+
}
|
|
2400
|
+
} else if (!delivered) {
|
|
2401
|
+
this.log.warn("Server did not acknowledge run:end in time, events may be pending");
|
|
2402
|
+
}
|
|
2403
|
+
} else {
|
|
2404
|
+
this.log.error(`Failed to send run:end event: ${errorMessage}`);
|
|
2405
|
+
}
|
|
2165
2406
|
}
|
|
2166
2407
|
this.wsClient?.close();
|
|
2167
2408
|
this.removeSignalHandlers();
|
|
@@ -2236,16 +2477,16 @@ var TestdinoReporter = class {
|
|
|
2236
2477
|
for (const event of events) {
|
|
2237
2478
|
if (event.type === "test:begin") {
|
|
2238
2479
|
const testBeginEvent = event;
|
|
2239
|
-
|
|
2240
|
-
|
|
2480
|
+
this.log.debug(
|
|
2481
|
+
`Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId} testId=${testBeginEvent.testId} retry=${testBeginEvent.retry} parallelIndex=${testBeginEvent.parallelIndex} title=${testBeginEvent.title}`
|
|
2241
2482
|
);
|
|
2242
2483
|
} else if (event.type === "test:end") {
|
|
2243
2484
|
const testEndEvent = event;
|
|
2244
|
-
|
|
2245
|
-
|
|
2485
|
+
this.log.debug(
|
|
2486
|
+
`Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId} testId=${testEndEvent.testId} retry=${testEndEvent.retry} parallelIndex=${testEndEvent.parallelIndex}`
|
|
2246
2487
|
);
|
|
2247
2488
|
} else {
|
|
2248
|
-
|
|
2489
|
+
this.log.debug(`Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId}`);
|
|
2249
2490
|
}
|
|
2250
2491
|
}
|
|
2251
2492
|
}
|
|
@@ -2254,7 +2495,7 @@ var TestdinoReporter = class {
|
|
|
2254
2495
|
await this.wsClient.sendBatch(events);
|
|
2255
2496
|
return;
|
|
2256
2497
|
} catch {
|
|
2257
|
-
|
|
2498
|
+
this.log.warn("WebSocket send failed, switching to HTTP fallback");
|
|
2258
2499
|
this.useHttpFallback = true;
|
|
2259
2500
|
}
|
|
2260
2501
|
}
|
|
@@ -2262,7 +2503,7 @@ var TestdinoReporter = class {
|
|
|
2262
2503
|
try {
|
|
2263
2504
|
await this.httpClient.sendEvents(events);
|
|
2264
2505
|
} catch (error) {
|
|
2265
|
-
|
|
2506
|
+
this.log.error(`Failed to send events via HTTP: ${error}`);
|
|
2266
2507
|
throw error;
|
|
2267
2508
|
}
|
|
2268
2509
|
}
|
|
@@ -2293,7 +2534,7 @@ var TestdinoReporter = class {
|
|
|
2293
2534
|
const metadataCollector = createMetadataCollector(playwrightConfig, playwrightSuite);
|
|
2294
2535
|
const result = await metadataCollector.collectAll();
|
|
2295
2536
|
if (result.failureCount > 0) {
|
|
2296
|
-
|
|
2537
|
+
this.log.warn(`${result.failureCount}/${result.results.length} metadata collectors failed`);
|
|
2297
2538
|
}
|
|
2298
2539
|
const skeleton = metadataCollector.buildSkeleton(playwrightSuite);
|
|
2299
2540
|
return {
|
|
@@ -2301,7 +2542,7 @@ var TestdinoReporter = class {
|
|
|
2301
2542
|
skeleton
|
|
2302
2543
|
};
|
|
2303
2544
|
} catch (error) {
|
|
2304
|
-
|
|
2545
|
+
this.log.warn(`Metadata collection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2305
2546
|
return {};
|
|
2306
2547
|
}
|
|
2307
2548
|
}
|
|
@@ -2603,8 +2844,7 @@ var TestdinoReporter = class {
|
|
|
2603
2844
|
handleInterruption(signal, exitCode) {
|
|
2604
2845
|
if (this.isShuttingDown) return;
|
|
2605
2846
|
this.isShuttingDown = true;
|
|
2606
|
-
|
|
2607
|
-
\u26A0\uFE0F TestDino: Received ${signal}, sending interruption event...`);
|
|
2847
|
+
this.log.warn(`Received ${signal}, sending interruption event...`);
|
|
2608
2848
|
if (!this.initPromise) {
|
|
2609
2849
|
process.exit(exitCode);
|
|
2610
2850
|
}
|
|
@@ -2632,7 +2872,7 @@ var TestdinoReporter = class {
|
|
|
2632
2872
|
}, 100);
|
|
2633
2873
|
const forceExitTimer = setTimeout(() => {
|
|
2634
2874
|
clearInterval(keepAlive);
|
|
2635
|
-
|
|
2875
|
+
this.log.error("Force exit - send timeout exceeded");
|
|
2636
2876
|
this.wsClient?.close();
|
|
2637
2877
|
process.exit(exitCode);
|
|
2638
2878
|
}, 3e3);
|
|
@@ -2640,10 +2880,10 @@ var TestdinoReporter = class {
|
|
|
2640
2880
|
try {
|
|
2641
2881
|
await waitForPending();
|
|
2642
2882
|
await Promise.race([this.sendInterruptionEvent(event), this.timeoutPromise(2500, "Send timeout")]);
|
|
2643
|
-
|
|
2883
|
+
this.log.success("Interruption event sent");
|
|
2644
2884
|
} catch (error) {
|
|
2645
2885
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2646
|
-
|
|
2886
|
+
this.log.error(`Failed to send interruption event: ${errorMsg}`);
|
|
2647
2887
|
} finally {
|
|
2648
2888
|
clearTimeout(forceExitTimer);
|
|
2649
2889
|
clearInterval(keepAlive);
|
|
@@ -2667,7 +2907,7 @@ var TestdinoReporter = class {
|
|
|
2667
2907
|
await this.wsClient.send(event);
|
|
2668
2908
|
return;
|
|
2669
2909
|
} catch {
|
|
2670
|
-
|
|
2910
|
+
this.log.warn("WebSocket send failed, trying HTTP");
|
|
2671
2911
|
}
|
|
2672
2912
|
}
|
|
2673
2913
|
if (this.httpClient) {
|
|
@@ -2703,11 +2943,9 @@ var TestdinoReporter = class {
|
|
|
2703
2943
|
this.artifactUploader = new ArtifactUploader(sasToken, {
|
|
2704
2944
|
debug: this.config.debug
|
|
2705
2945
|
});
|
|
2706
|
-
|
|
2707
|
-
console.log("\u{1F4E4} TestDino: Artifact uploads enabled");
|
|
2708
|
-
}
|
|
2946
|
+
this.log.debug("Artifact uploads enabled");
|
|
2709
2947
|
} catch (error) {
|
|
2710
|
-
|
|
2948
|
+
this.log.warn(`Artifact uploads disabled - ${error instanceof Error ? error.message : String(error)}`);
|
|
2711
2949
|
this.artifactsEnabled = false;
|
|
2712
2950
|
this.artifactUploader = null;
|
|
2713
2951
|
}
|