@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.mjs
CHANGED
|
@@ -50,6 +50,7 @@ function isQuotaError(error) {
|
|
|
50
50
|
|
|
51
51
|
// src/streaming/websocket.ts
|
|
52
52
|
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
53
|
+
var DEFAULT_ACK_TIMEOUT_MS = 5e3;
|
|
53
54
|
var WebSocketClient = class {
|
|
54
55
|
ws = null;
|
|
55
56
|
options;
|
|
@@ -58,11 +59,16 @@ var WebSocketClient = class {
|
|
|
58
59
|
isConnecting = false;
|
|
59
60
|
isClosed = false;
|
|
60
61
|
pingInterval = null;
|
|
62
|
+
pendingAcks = /* @__PURE__ */ new Map();
|
|
63
|
+
ackTimeout = DEFAULT_ACK_TIMEOUT_MS;
|
|
64
|
+
debug;
|
|
61
65
|
constructor(options) {
|
|
66
|
+
this.debug = options.debug ?? false;
|
|
62
67
|
this.options = {
|
|
63
68
|
sessionId: "",
|
|
64
69
|
maxRetries: 5,
|
|
65
70
|
retryDelay: 1e3,
|
|
71
|
+
debug: false,
|
|
66
72
|
onConnected: () => {
|
|
67
73
|
},
|
|
68
74
|
onDisconnected: () => {
|
|
@@ -93,9 +99,11 @@ var WebSocketClient = class {
|
|
|
93
99
|
let serverReady = false;
|
|
94
100
|
const handshakeTimeout = setTimeout(() => {
|
|
95
101
|
if (!serverReady) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
if (this.debug) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`\u26A0\uFE0F TestDino: WebSocket handshake timeout \u2014 server did not send 'connected' within ${HANDSHAKE_TIMEOUT_MS}ms. Resolving anyway.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
99
107
|
serverReady = true;
|
|
100
108
|
this.isConnecting = false;
|
|
101
109
|
this.options.onConnected();
|
|
@@ -166,6 +174,7 @@ var WebSocketClient = class {
|
|
|
166
174
|
}
|
|
167
175
|
/**
|
|
168
176
|
* Send multiple events in batch (parallel for speed)
|
|
177
|
+
* Note: Uses Promise.all intentionally - if one send fails, connection is broken
|
|
169
178
|
*/
|
|
170
179
|
async sendBatch(events) {
|
|
171
180
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -173,6 +182,36 @@ var WebSocketClient = class {
|
|
|
173
182
|
}
|
|
174
183
|
await Promise.all(events.map((event) => this.send(event)));
|
|
175
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Send event and wait for server ACK
|
|
187
|
+
* Use this for critical events like run:end where delivery must be confirmed
|
|
188
|
+
*/
|
|
189
|
+
async sendAndWaitForAck(event, timeout = this.ackTimeout) {
|
|
190
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
191
|
+
throw new Error("WebSocket is not connected");
|
|
192
|
+
}
|
|
193
|
+
const sequence = event.sequence;
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const timer = setTimeout(() => {
|
|
196
|
+
this.pendingAcks.delete(sequence);
|
|
197
|
+
reject(new Error(`ACK timeout for sequence ${sequence} after ${timeout}ms`));
|
|
198
|
+
}, timeout);
|
|
199
|
+
this.pendingAcks.set(sequence, { resolve, reject, timer });
|
|
200
|
+
this.ws.send(JSON.stringify(event), (error) => {
|
|
201
|
+
if (error) {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
this.pendingAcks.delete(sequence);
|
|
204
|
+
reject(error);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Set ACK timeout for sendAndWaitForAck
|
|
211
|
+
*/
|
|
212
|
+
setAckTimeout(timeout) {
|
|
213
|
+
this.ackTimeout = timeout;
|
|
214
|
+
}
|
|
176
215
|
/**
|
|
177
216
|
* Check if WebSocket is connected
|
|
178
217
|
*/
|
|
@@ -186,11 +225,22 @@ var WebSocketClient = class {
|
|
|
186
225
|
this.isClosed = true;
|
|
187
226
|
this.stopPing();
|
|
188
227
|
this.clearReconnectTimer();
|
|
228
|
+
this.clearPendingAcks();
|
|
189
229
|
if (this.ws) {
|
|
190
230
|
this.ws.close();
|
|
191
231
|
this.ws = null;
|
|
192
232
|
}
|
|
193
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Clear all pending ACKs (reject them with connection closed error)
|
|
236
|
+
*/
|
|
237
|
+
clearPendingAcks() {
|
|
238
|
+
for (const [sequence, pending] of this.pendingAcks) {
|
|
239
|
+
clearTimeout(pending.timer);
|
|
240
|
+
pending.reject(new Error(`Connection closed while waiting for ACK on sequence ${sequence}`));
|
|
241
|
+
}
|
|
242
|
+
this.pendingAcks.clear();
|
|
243
|
+
}
|
|
194
244
|
/**
|
|
195
245
|
* Handle incoming messages
|
|
196
246
|
*/
|
|
@@ -199,6 +249,16 @@ var WebSocketClient = class {
|
|
|
199
249
|
const message = JSON.parse(data);
|
|
200
250
|
if (message.type === "connected") {
|
|
201
251
|
} else if (message.type === "ack") {
|
|
252
|
+
const sequence = typeof message.sequence === "number" ? message.sequence : void 0;
|
|
253
|
+
if (sequence === void 0) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (this.pendingAcks.has(sequence)) {
|
|
257
|
+
const pending = this.pendingAcks.get(sequence);
|
|
258
|
+
clearTimeout(pending.timer);
|
|
259
|
+
this.pendingAcks.delete(sequence);
|
|
260
|
+
pending.resolve();
|
|
261
|
+
}
|
|
202
262
|
} else if (message.type === "nack") {
|
|
203
263
|
const nack = message;
|
|
204
264
|
if (isServerError(nack.error)) {
|
|
@@ -240,7 +300,9 @@ var WebSocketClient = class {
|
|
|
240
300
|
}
|
|
241
301
|
}
|
|
242
302
|
} catch (error) {
|
|
243
|
-
|
|
303
|
+
if (this.debug) {
|
|
304
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
305
|
+
}
|
|
244
306
|
}
|
|
245
307
|
}
|
|
246
308
|
/**
|
|
@@ -248,6 +310,7 @@ var WebSocketClient = class {
|
|
|
248
310
|
*/
|
|
249
311
|
handleClose(_code, _reason) {
|
|
250
312
|
this.stopPing();
|
|
313
|
+
this.clearPendingAcks();
|
|
251
314
|
this.options.onDisconnected();
|
|
252
315
|
if (this.isClosed) {
|
|
253
316
|
return;
|
|
@@ -267,7 +330,9 @@ var WebSocketClient = class {
|
|
|
267
330
|
this.reconnectAttempts++;
|
|
268
331
|
this.reconnectTimer = setTimeout(() => {
|
|
269
332
|
this.connect().catch((error) => {
|
|
270
|
-
|
|
333
|
+
if (this.debug) {
|
|
334
|
+
console.error("Reconnection failed:", error);
|
|
335
|
+
}
|
|
271
336
|
});
|
|
272
337
|
}, delay);
|
|
273
338
|
}
|
|
@@ -301,6 +366,16 @@ var WebSocketClient = class {
|
|
|
301
366
|
}
|
|
302
367
|
}
|
|
303
368
|
};
|
|
369
|
+
|
|
370
|
+
// src/utils/index.ts
|
|
371
|
+
function sleep(ms) {
|
|
372
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
373
|
+
}
|
|
374
|
+
function isDebugEnabled() {
|
|
375
|
+
return process.env.TESTDINO_DEBUG === "true" || process.env.TESTDINO_DEBUG === "1" || process.env.DEBUG === "true";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/streaming/http.ts
|
|
304
379
|
var HttpClient = class {
|
|
305
380
|
client;
|
|
306
381
|
options;
|
|
@@ -347,7 +422,7 @@ var HttpClient = class {
|
|
|
347
422
|
lastError = new Error(this.getErrorMessage(error));
|
|
348
423
|
if (attempt < this.options.maxRetries - 1) {
|
|
349
424
|
const delay = this.options.retryDelay * Math.pow(2, attempt);
|
|
350
|
-
await
|
|
425
|
+
await sleep(delay);
|
|
351
426
|
}
|
|
352
427
|
}
|
|
353
428
|
}
|
|
@@ -371,12 +446,6 @@ var HttpClient = class {
|
|
|
371
446
|
}
|
|
372
447
|
return String(error);
|
|
373
448
|
}
|
|
374
|
-
/**
|
|
375
|
-
* Sleep utility for retry delays
|
|
376
|
-
*/
|
|
377
|
-
sleep(ms) {
|
|
378
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
379
|
-
}
|
|
380
449
|
};
|
|
381
450
|
|
|
382
451
|
// src/streaming/buffer.ts
|
|
@@ -564,7 +633,7 @@ var GitMetadataCollector = class extends BaseMetadataCollector {
|
|
|
564
633
|
if (!isGitRepo) {
|
|
565
634
|
return this.getEmptyMetadata();
|
|
566
635
|
}
|
|
567
|
-
const results = await Promise.
|
|
636
|
+
const results = await Promise.allSettled([
|
|
568
637
|
this.getBranch(),
|
|
569
638
|
this.getCommitHash(),
|
|
570
639
|
this.getCommitMessage(),
|
|
@@ -574,9 +643,15 @@ var GitMetadataCollector = class extends BaseMetadataCollector {
|
|
|
574
643
|
this.getRepoUrl(),
|
|
575
644
|
this.isDirtyWorkingTree()
|
|
576
645
|
]);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
646
|
+
const extractedValues = results.map((r) => r.status === "fulfilled" ? r.value : void 0);
|
|
647
|
+
let branch = extractedValues[0];
|
|
648
|
+
let hash = extractedValues[1];
|
|
649
|
+
let message = extractedValues[2];
|
|
650
|
+
let author = extractedValues[3];
|
|
651
|
+
let email = extractedValues[4];
|
|
652
|
+
let timestamp = extractedValues[5];
|
|
653
|
+
const repoUrl = extractedValues[6];
|
|
654
|
+
const isDirty = extractedValues[7];
|
|
580
655
|
let prMetadata;
|
|
581
656
|
if (process.env.GITHUB_EVENT_NAME === "pull_request") {
|
|
582
657
|
const eventData = await this.readGitHubEventFile();
|
|
@@ -1242,9 +1317,13 @@ var PlaywrightMetadataCollector = class extends BaseMetadataCollector {
|
|
|
1242
1317
|
metadata.workers = config.workers;
|
|
1243
1318
|
}
|
|
1244
1319
|
if (Array.isArray(config.projects) && config.projects.length > 0) {
|
|
1245
|
-
const
|
|
1246
|
-
if (
|
|
1247
|
-
metadata.projects =
|
|
1320
|
+
const projectConfigs = config.projects.filter((project) => this.isNonEmptyString(project.name)).map((project) => this.extractProjectConfig(project));
|
|
1321
|
+
if (projectConfigs.length > 0) {
|
|
1322
|
+
metadata.projects = projectConfigs;
|
|
1323
|
+
const browsers = this.extractBrowsersFromProjects(config.projects);
|
|
1324
|
+
if (browsers.length > 0) {
|
|
1325
|
+
metadata.browsers = browsers;
|
|
1326
|
+
}
|
|
1248
1327
|
}
|
|
1249
1328
|
}
|
|
1250
1329
|
if (config.reportSlowTests && typeof config.reportSlowTests === "object") {
|
|
@@ -1270,6 +1349,151 @@ var PlaywrightMetadataCollector = class extends BaseMetadataCollector {
|
|
|
1270
1349
|
}
|
|
1271
1350
|
return metadata;
|
|
1272
1351
|
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Extract project configuration from FullProject
|
|
1354
|
+
*/
|
|
1355
|
+
extractProjectConfig(project) {
|
|
1356
|
+
const config = {
|
|
1357
|
+
name: project.name || ""
|
|
1358
|
+
};
|
|
1359
|
+
if (this.isNonEmptyString(project.testDir)) {
|
|
1360
|
+
config.testDir = project.testDir;
|
|
1361
|
+
}
|
|
1362
|
+
if (typeof project.timeout === "number") {
|
|
1363
|
+
config.timeout = project.timeout;
|
|
1364
|
+
}
|
|
1365
|
+
if (typeof project.retries === "number") {
|
|
1366
|
+
config.retries = project.retries;
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof project.repeatEach === "number" && project.repeatEach > 1) {
|
|
1369
|
+
config.repeatEach = project.repeatEach;
|
|
1370
|
+
}
|
|
1371
|
+
if (Array.isArray(project.dependencies) && project.dependencies.length > 0) {
|
|
1372
|
+
config.dependencies = project.dependencies;
|
|
1373
|
+
}
|
|
1374
|
+
if (project.grep) {
|
|
1375
|
+
const patterns = Array.isArray(project.grep) ? project.grep : [project.grep];
|
|
1376
|
+
const grepStrings = patterns.map((p) => p.source);
|
|
1377
|
+
if (grepStrings.length > 0) {
|
|
1378
|
+
config.grep = grepStrings;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (project.use) {
|
|
1382
|
+
const useOptions = this.extractUseOptions(project.use);
|
|
1383
|
+
if (Object.keys(useOptions).length > 0) {
|
|
1384
|
+
config.use = useOptions;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return config;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Extract use options from project.use
|
|
1391
|
+
*/
|
|
1392
|
+
extractUseOptions(use) {
|
|
1393
|
+
const options = {};
|
|
1394
|
+
if (!use) return options;
|
|
1395
|
+
if (this.isNonEmptyString(use.channel)) {
|
|
1396
|
+
options.channel = use.channel;
|
|
1397
|
+
}
|
|
1398
|
+
const browserName = this.resolveBrowserName(use.browserName, use.defaultBrowserType, use.channel);
|
|
1399
|
+
if (browserName) {
|
|
1400
|
+
options.browserName = browserName;
|
|
1401
|
+
}
|
|
1402
|
+
if (typeof use.headless === "boolean") {
|
|
1403
|
+
options.headless = use.headless;
|
|
1404
|
+
}
|
|
1405
|
+
if (use.viewport && typeof use.viewport === "object") {
|
|
1406
|
+
options.viewport = {
|
|
1407
|
+
width: use.viewport.width,
|
|
1408
|
+
height: use.viewport.height
|
|
1409
|
+
};
|
|
1410
|
+
} else if (use.viewport === null) {
|
|
1411
|
+
options.viewport = null;
|
|
1412
|
+
}
|
|
1413
|
+
if (this.isNonEmptyString(use.baseURL)) {
|
|
1414
|
+
options.baseURL = use.baseURL;
|
|
1415
|
+
}
|
|
1416
|
+
const trace = this.normalizeArtifactMode(use.trace);
|
|
1417
|
+
if (trace) {
|
|
1418
|
+
options.trace = trace;
|
|
1419
|
+
}
|
|
1420
|
+
const screenshot = this.normalizeArtifactMode(use.screenshot);
|
|
1421
|
+
if (screenshot) {
|
|
1422
|
+
options.screenshot = screenshot;
|
|
1423
|
+
}
|
|
1424
|
+
const video = this.normalizeArtifactMode(use.video);
|
|
1425
|
+
if (video) {
|
|
1426
|
+
options.video = video;
|
|
1427
|
+
}
|
|
1428
|
+
if (typeof use.isMobile === "boolean") {
|
|
1429
|
+
options.isMobile = use.isMobile;
|
|
1430
|
+
}
|
|
1431
|
+
if (this.isNonEmptyString(use.locale)) {
|
|
1432
|
+
options.locale = use.locale;
|
|
1433
|
+
}
|
|
1434
|
+
return options;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Normalize artifact mode (trace/screenshot/video can be string or { mode: string })
|
|
1438
|
+
*/
|
|
1439
|
+
normalizeArtifactMode(value) {
|
|
1440
|
+
if (!value) return void 0;
|
|
1441
|
+
if (typeof value === "string" && value !== "off") {
|
|
1442
|
+
return value;
|
|
1443
|
+
}
|
|
1444
|
+
if (typeof value === "object" && value !== null && "mode" in value) {
|
|
1445
|
+
const mode = value.mode;
|
|
1446
|
+
if (mode && mode !== "off") {
|
|
1447
|
+
return mode;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return void 0;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Resolve browserName from explicit value, defaultBrowserType (device presets), or channel
|
|
1454
|
+
* Priority: browserName > defaultBrowserType > channel inference
|
|
1455
|
+
*/
|
|
1456
|
+
resolveBrowserName(browserName, defaultBrowserType, channel) {
|
|
1457
|
+
const validBrowsers = ["chromium", "firefox", "webkit"];
|
|
1458
|
+
if (browserName && validBrowsers.includes(browserName)) {
|
|
1459
|
+
return browserName;
|
|
1460
|
+
}
|
|
1461
|
+
if (defaultBrowserType && validBrowsers.includes(defaultBrowserType)) {
|
|
1462
|
+
return defaultBrowserType;
|
|
1463
|
+
}
|
|
1464
|
+
if (channel) {
|
|
1465
|
+
if ([
|
|
1466
|
+
"chrome",
|
|
1467
|
+
"chrome-beta",
|
|
1468
|
+
"chrome-dev",
|
|
1469
|
+
"chrome-canary",
|
|
1470
|
+
"msedge",
|
|
1471
|
+
"msedge-beta",
|
|
1472
|
+
"msedge-dev",
|
|
1473
|
+
"msedge-canary"
|
|
1474
|
+
].includes(channel)) {
|
|
1475
|
+
return "chromium";
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return void 0;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Extract unique browsers from all projects
|
|
1482
|
+
*/
|
|
1483
|
+
extractBrowsersFromProjects(projects) {
|
|
1484
|
+
const browsers = /* @__PURE__ */ new Set();
|
|
1485
|
+
for (const project of projects) {
|
|
1486
|
+
const browserName = this.resolveBrowserName(
|
|
1487
|
+
project.use?.browserName,
|
|
1488
|
+
project.use?.defaultBrowserType,
|
|
1489
|
+
project.use?.channel
|
|
1490
|
+
);
|
|
1491
|
+
if (browserName) {
|
|
1492
|
+
browsers.add(browserName);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return Array.from(browsers);
|
|
1496
|
+
}
|
|
1273
1497
|
/**
|
|
1274
1498
|
* Build skeleton from Suite
|
|
1275
1499
|
*/
|
|
@@ -1376,9 +1600,6 @@ var MetadataAggregator = class {
|
|
|
1376
1600
|
*/
|
|
1377
1601
|
async collectAll() {
|
|
1378
1602
|
const startTime = Date.now();
|
|
1379
|
-
if (this.options.debug) {
|
|
1380
|
-
console.log(`\u{1F50D} TestDino: Starting metadata collection with ${this.collectors.length} collectors`);
|
|
1381
|
-
}
|
|
1382
1603
|
const settledResults = await Promise.allSettled(
|
|
1383
1604
|
this.collectors.map(
|
|
1384
1605
|
(collector) => this.withTimeout(collector.collectWithResult(), this.options.timeout, "Metadata collection")
|
|
@@ -1406,11 +1627,6 @@ var MetadataAggregator = class {
|
|
|
1406
1627
|
const totalDuration = Date.now() - startTime;
|
|
1407
1628
|
const successCount = results.filter((r) => r.success).length;
|
|
1408
1629
|
const failureCount = results.length - successCount;
|
|
1409
|
-
if (this.options.debug) {
|
|
1410
|
-
console.log(
|
|
1411
|
-
`\u2705 TestDino: Metadata collection completed in ${totalDuration}ms (${successCount}/${results.length} successful)`
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1414
1630
|
return {
|
|
1415
1631
|
metadata,
|
|
1416
1632
|
results,
|
|
@@ -1509,7 +1725,7 @@ var SASTokenClient = class {
|
|
|
1509
1725
|
lastError = new Error(this.getErrorMessage(error));
|
|
1510
1726
|
if (attempt < this.options.maxRetries - 1) {
|
|
1511
1727
|
const delay = this.options.retryDelay * Math.pow(2, attempt);
|
|
1512
|
-
await
|
|
1728
|
+
await sleep(delay);
|
|
1513
1729
|
}
|
|
1514
1730
|
}
|
|
1515
1731
|
}
|
|
@@ -1536,12 +1752,6 @@ var SASTokenClient = class {
|
|
|
1536
1752
|
}
|
|
1537
1753
|
return String(error);
|
|
1538
1754
|
}
|
|
1539
|
-
/**
|
|
1540
|
-
* Sleep utility for retry delays
|
|
1541
|
-
*/
|
|
1542
|
-
sleep(ms) {
|
|
1543
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1544
|
-
}
|
|
1545
1755
|
};
|
|
1546
1756
|
var ArtifactUploader = class {
|
|
1547
1757
|
sasToken;
|
|
@@ -1676,7 +1886,7 @@ var ArtifactUploader = class {
|
|
|
1676
1886
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1677
1887
|
if (attempt < this.options.maxRetries - 1) {
|
|
1678
1888
|
const delay = 1e3 * Math.pow(2, attempt);
|
|
1679
|
-
await
|
|
1889
|
+
await sleep(delay);
|
|
1680
1890
|
}
|
|
1681
1891
|
}
|
|
1682
1892
|
}
|
|
@@ -1699,12 +1909,6 @@ var ArtifactUploader = class {
|
|
|
1699
1909
|
maxBodyLength: Infinity
|
|
1700
1910
|
});
|
|
1701
1911
|
}
|
|
1702
|
-
/**
|
|
1703
|
-
* Sleep utility for retry delays
|
|
1704
|
-
*/
|
|
1705
|
-
sleep(ms) {
|
|
1706
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1707
|
-
}
|
|
1708
1912
|
/**
|
|
1709
1913
|
* Check if SAS token is still valid
|
|
1710
1914
|
*/
|
|
@@ -1721,6 +1925,19 @@ var ArtifactUploader = class {
|
|
|
1721
1925
|
}
|
|
1722
1926
|
};
|
|
1723
1927
|
|
|
1928
|
+
// src/reporter/log.ts
|
|
1929
|
+
var createReporterLog = (options) => ({
|
|
1930
|
+
success: (msg) => console.log(`\u2705 TestDino: ${msg}`),
|
|
1931
|
+
warn: (msg) => console.warn(`\u26A0\uFE0F TestDino: ${msg}`),
|
|
1932
|
+
error: (msg) => console.error(`\u274C TestDino: ${msg}`),
|
|
1933
|
+
info: (msg) => console.log(`\u2139\uFE0F TestDino: ${msg}`),
|
|
1934
|
+
debug: (msg) => {
|
|
1935
|
+
if (options.debug) {
|
|
1936
|
+
console.log(`\u{1F50D} TestDino: ${msg}`);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1724
1941
|
// src/reporter/index.ts
|
|
1725
1942
|
var MAX_CONSOLE_CHUNK_SIZE = 1e4;
|
|
1726
1943
|
var MAX_BUFFER_SIZE = 10;
|
|
@@ -1752,10 +1969,13 @@ var TestdinoReporter = class {
|
|
|
1752
1969
|
initFailed = false;
|
|
1753
1970
|
// Promises for onTestEnd; must be awaited in onEnd to prevent data loss
|
|
1754
1971
|
pendingTestEndPromises = /* @__PURE__ */ new Set();
|
|
1972
|
+
// Logger for consistent output
|
|
1973
|
+
log;
|
|
1755
1974
|
constructor(config = {}) {
|
|
1756
1975
|
const cliConfig = this.loadCliConfig();
|
|
1757
1976
|
this.config = { ...config, ...cliConfig };
|
|
1758
1977
|
this.runId = randomUUID();
|
|
1978
|
+
this.log = createReporterLog({ debug: this.config.debug ?? false });
|
|
1759
1979
|
this.buffer = new EventBuffer({
|
|
1760
1980
|
maxSize: MAX_BUFFER_SIZE,
|
|
1761
1981
|
onFlush: async (events) => {
|
|
@@ -1799,7 +2019,7 @@ var TestdinoReporter = class {
|
|
|
1799
2019
|
}
|
|
1800
2020
|
return mappedConfig;
|
|
1801
2021
|
} catch (error) {
|
|
1802
|
-
if (
|
|
2022
|
+
if (isDebugEnabled()) {
|
|
1803
2023
|
console.warn(
|
|
1804
2024
|
"\u26A0\uFE0F TestDino: Failed to load CLI config:",
|
|
1805
2025
|
error instanceof Error ? error.message : String(error)
|
|
@@ -1814,7 +2034,7 @@ var TestdinoReporter = class {
|
|
|
1814
2034
|
async onBegin(config, suite) {
|
|
1815
2035
|
if (config && this.isDuplicateInstance(config.reporter)) {
|
|
1816
2036
|
if (this.config.debug) {
|
|
1817
|
-
|
|
2037
|
+
this.log.debug("Reporter already configured in playwright.config, skipping duplicate instance");
|
|
1818
2038
|
}
|
|
1819
2039
|
return;
|
|
1820
2040
|
}
|
|
@@ -1854,19 +2074,20 @@ var TestdinoReporter = class {
|
|
|
1854
2074
|
this.httpClient = new HttpClient({ token, serverUrl });
|
|
1855
2075
|
const auth = await this.httpClient.authenticate();
|
|
1856
2076
|
this.sessionId = auth.sessionId;
|
|
1857
|
-
|
|
2077
|
+
this.log.success("Authenticated successfully");
|
|
1858
2078
|
if (this.config.debug) {
|
|
1859
|
-
|
|
2079
|
+
this.log.debug(`Session ${this.sessionId} \u2014 reusing for WebSocket`);
|
|
1860
2080
|
}
|
|
1861
2081
|
this.wsClient = new WebSocketClient({
|
|
1862
2082
|
token,
|
|
1863
2083
|
sessionId: this.sessionId ?? void 0,
|
|
1864
2084
|
serverUrl: this.getWebSocketUrl(),
|
|
2085
|
+
debug: this.config.debug,
|
|
1865
2086
|
onConnected: () => {
|
|
1866
|
-
|
|
2087
|
+
this.log.debug("WebSocket connected");
|
|
1867
2088
|
},
|
|
1868
2089
|
onDisconnected: () => {
|
|
1869
|
-
|
|
2090
|
+
this.log.debug("WebSocket disconnected");
|
|
1870
2091
|
},
|
|
1871
2092
|
onError: (error) => {
|
|
1872
2093
|
if (isQuotaError(error)) {
|
|
@@ -1876,14 +2097,14 @@ var TestdinoReporter = class {
|
|
|
1876
2097
|
this.printQuotaError(error);
|
|
1877
2098
|
}
|
|
1878
2099
|
} else {
|
|
1879
|
-
|
|
2100
|
+
this.log.error(`WebSocket error: ${error.message}`);
|
|
1880
2101
|
}
|
|
1881
2102
|
}
|
|
1882
2103
|
});
|
|
1883
2104
|
try {
|
|
1884
2105
|
await this.wsClient.connect();
|
|
1885
2106
|
} catch {
|
|
1886
|
-
|
|
2107
|
+
this.log.warn("WebSocket connection failed, using HTTP fallback");
|
|
1887
2108
|
this.useHttpFallback = true;
|
|
1888
2109
|
}
|
|
1889
2110
|
this.artifactsEnabled = this.config.artifacts !== false;
|
|
@@ -1899,18 +2120,13 @@ var TestdinoReporter = class {
|
|
|
1899
2120
|
...this.getEventMetadata()
|
|
1900
2121
|
};
|
|
1901
2122
|
if (this.config.debug) {
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
console.log(` skeleton.totalTests: ${metadata.skeleton.totalTests}`);
|
|
1908
|
-
console.log(` skeleton.suites: ${metadata.skeleton.suites?.length ?? 0}`);
|
|
1909
|
-
console.log(` skeleton: ${JSON.stringify(metadata.skeleton, null, 2)}`);
|
|
1910
|
-
} else {
|
|
1911
|
-
console.log(` skeleton: (not available)`);
|
|
1912
|
-
}
|
|
2123
|
+
const shardInfo = config?.shard ? `${config.shard.current}/${config.shard.total}` : "none";
|
|
2124
|
+
const skeletonInfo = metadata.skeleton ? `${metadata.skeleton.totalTests} tests, ${metadata.skeleton.suites?.length ?? 0} suites` : "not available";
|
|
2125
|
+
this.log.debug(
|
|
2126
|
+
`run:begin runId=${this.runId} ciRunId=${this.config.ciRunId ?? "none"} shard=${shardInfo} skeleton=(${skeletonInfo})`
|
|
2127
|
+
);
|
|
1913
2128
|
}
|
|
2129
|
+
console.log("[TEMP DEBUG] run:begin metadata:", JSON.stringify(metadata, null, 2));
|
|
1914
2130
|
await this.sendEvents([beginEvent]);
|
|
1915
2131
|
this.registerSignalHandlers();
|
|
1916
2132
|
return true;
|
|
@@ -2051,10 +2267,9 @@ var TestdinoReporter = class {
|
|
|
2051
2267
|
await this.buffer.add(event);
|
|
2052
2268
|
}
|
|
2053
2269
|
/**
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
*/
|
|
2270
|
+
* Called after each test.
|
|
2271
|
+
* Playwright does not await onTestEnd promises—pending work is awaited in onEnd.
|
|
2272
|
+
*/
|
|
2058
2273
|
onTestEnd(test, result) {
|
|
2059
2274
|
if (!this.initPromise || this.initFailed) return;
|
|
2060
2275
|
const workPromise = this.processTestEnd(test, result);
|
|
@@ -2103,10 +2318,7 @@ var TestdinoReporter = class {
|
|
|
2103
2318
|
};
|
|
2104
2319
|
await this.buffer.add(event);
|
|
2105
2320
|
} catch (error) {
|
|
2106
|
-
|
|
2107
|
-
"\u274C TestDino: Failed to process test:end event:",
|
|
2108
|
-
error instanceof Error ? error.message : String(error)
|
|
2109
|
-
);
|
|
2321
|
+
this.log.error(`Failed to process test:end event: ${error instanceof Error ? error.message : String(error)}`);
|
|
2110
2322
|
}
|
|
2111
2323
|
}
|
|
2112
2324
|
/**
|
|
@@ -2117,7 +2329,7 @@ var TestdinoReporter = class {
|
|
|
2117
2329
|
if (this.pendingTestEndPromises.size > 0) {
|
|
2118
2330
|
await Promise.allSettled(Array.from(this.pendingTestEndPromises));
|
|
2119
2331
|
}
|
|
2120
|
-
|
|
2332
|
+
this.log.success("Tests completed (quota limit reached; not streamed to TestDino)");
|
|
2121
2333
|
this.wsClient?.close();
|
|
2122
2334
|
this.removeSignalHandlers();
|
|
2123
2335
|
return;
|
|
@@ -2132,9 +2344,7 @@ var TestdinoReporter = class {
|
|
|
2132
2344
|
return;
|
|
2133
2345
|
}
|
|
2134
2346
|
if (this.pendingTestEndPromises.size > 0) {
|
|
2135
|
-
|
|
2136
|
-
console.log(`\u{1F50D} TestDino: Waiting for ${this.pendingTestEndPromises.size} pending test:end events...`);
|
|
2137
|
-
}
|
|
2347
|
+
this.log.debug(`Waiting for ${this.pendingTestEndPromises.size} pending test:end events...`);
|
|
2138
2348
|
await Promise.allSettled(Array.from(this.pendingTestEndPromises));
|
|
2139
2349
|
}
|
|
2140
2350
|
const event = {
|
|
@@ -2149,12 +2359,43 @@ var TestdinoReporter = class {
|
|
|
2149
2359
|
// Shard information
|
|
2150
2360
|
shard: this.shardInfo
|
|
2151
2361
|
};
|
|
2152
|
-
await this.buffer.add(event);
|
|
2153
2362
|
try {
|
|
2154
2363
|
await this.buffer.flush();
|
|
2155
|
-
console.log("\u2705 TestDino: All events sent successfully");
|
|
2156
2364
|
} catch (error) {
|
|
2157
|
-
|
|
2365
|
+
this.log.error(`Failed to flush buffered events: ${error}`);
|
|
2366
|
+
}
|
|
2367
|
+
let delivered = false;
|
|
2368
|
+
try {
|
|
2369
|
+
if (this.wsClient?.isConnected()) {
|
|
2370
|
+
await this.wsClient.sendAndWaitForAck(event);
|
|
2371
|
+
this.log.success("All events delivered (server acknowledged)");
|
|
2372
|
+
delivered = true;
|
|
2373
|
+
} else if (this.httpClient) {
|
|
2374
|
+
await this.httpClient.sendEvents([event]);
|
|
2375
|
+
this.log.success("All events sent");
|
|
2376
|
+
delivered = true;
|
|
2377
|
+
} else {
|
|
2378
|
+
this.log.warn("No connection available to send run:end event");
|
|
2379
|
+
}
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2382
|
+
if (errorMessage.includes("ACK timeout") || errorMessage.includes("Connection closed")) {
|
|
2383
|
+
if (this.httpClient && !delivered) {
|
|
2384
|
+
try {
|
|
2385
|
+
this.log.warn("WebSocket ACK timeout, retrying via HTTP...");
|
|
2386
|
+
await this.httpClient.sendEvents([event]);
|
|
2387
|
+
this.log.success("All events sent (HTTP fallback)");
|
|
2388
|
+
delivered = true;
|
|
2389
|
+
} catch (httpError) {
|
|
2390
|
+
const httpErrorMessage = httpError instanceof Error ? httpError.message : String(httpError);
|
|
2391
|
+
this.log.error(`HTTP fallback also failed: ${httpErrorMessage}`);
|
|
2392
|
+
}
|
|
2393
|
+
} else if (!delivered) {
|
|
2394
|
+
this.log.warn("Server did not acknowledge run:end in time, events may be pending");
|
|
2395
|
+
}
|
|
2396
|
+
} else {
|
|
2397
|
+
this.log.error(`Failed to send run:end event: ${errorMessage}`);
|
|
2398
|
+
}
|
|
2158
2399
|
}
|
|
2159
2400
|
this.wsClient?.close();
|
|
2160
2401
|
this.removeSignalHandlers();
|
|
@@ -2229,16 +2470,16 @@ var TestdinoReporter = class {
|
|
|
2229
2470
|
for (const event of events) {
|
|
2230
2471
|
if (event.type === "test:begin") {
|
|
2231
2472
|
const testBeginEvent = event;
|
|
2232
|
-
|
|
2233
|
-
|
|
2473
|
+
this.log.debug(
|
|
2474
|
+
`Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId} testId=${testBeginEvent.testId} retry=${testBeginEvent.retry} parallelIndex=${testBeginEvent.parallelIndex} title=${testBeginEvent.title}`
|
|
2234
2475
|
);
|
|
2235
2476
|
} else if (event.type === "test:end") {
|
|
2236
2477
|
const testEndEvent = event;
|
|
2237
|
-
|
|
2238
|
-
|
|
2478
|
+
this.log.debug(
|
|
2479
|
+
`Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId} testId=${testEndEvent.testId} retry=${testEndEvent.retry} parallelIndex=${testEndEvent.parallelIndex}`
|
|
2239
2480
|
);
|
|
2240
2481
|
} else {
|
|
2241
|
-
|
|
2482
|
+
this.log.debug(`Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId}`);
|
|
2242
2483
|
}
|
|
2243
2484
|
}
|
|
2244
2485
|
}
|
|
@@ -2247,7 +2488,7 @@ var TestdinoReporter = class {
|
|
|
2247
2488
|
await this.wsClient.sendBatch(events);
|
|
2248
2489
|
return;
|
|
2249
2490
|
} catch {
|
|
2250
|
-
|
|
2491
|
+
this.log.warn("WebSocket send failed, switching to HTTP fallback");
|
|
2251
2492
|
this.useHttpFallback = true;
|
|
2252
2493
|
}
|
|
2253
2494
|
}
|
|
@@ -2255,7 +2496,7 @@ var TestdinoReporter = class {
|
|
|
2255
2496
|
try {
|
|
2256
2497
|
await this.httpClient.sendEvents(events);
|
|
2257
2498
|
} catch (error) {
|
|
2258
|
-
|
|
2499
|
+
this.log.error(`Failed to send events via HTTP: ${error}`);
|
|
2259
2500
|
throw error;
|
|
2260
2501
|
}
|
|
2261
2502
|
}
|
|
@@ -2286,7 +2527,7 @@ var TestdinoReporter = class {
|
|
|
2286
2527
|
const metadataCollector = createMetadataCollector(playwrightConfig, playwrightSuite);
|
|
2287
2528
|
const result = await metadataCollector.collectAll();
|
|
2288
2529
|
if (result.failureCount > 0) {
|
|
2289
|
-
|
|
2530
|
+
this.log.warn(`${result.failureCount}/${result.results.length} metadata collectors failed`);
|
|
2290
2531
|
}
|
|
2291
2532
|
const skeleton = metadataCollector.buildSkeleton(playwrightSuite);
|
|
2292
2533
|
return {
|
|
@@ -2294,7 +2535,7 @@ var TestdinoReporter = class {
|
|
|
2294
2535
|
skeleton
|
|
2295
2536
|
};
|
|
2296
2537
|
} catch (error) {
|
|
2297
|
-
|
|
2538
|
+
this.log.warn(`Metadata collection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2298
2539
|
return {};
|
|
2299
2540
|
}
|
|
2300
2541
|
}
|
|
@@ -2596,8 +2837,7 @@ var TestdinoReporter = class {
|
|
|
2596
2837
|
handleInterruption(signal, exitCode) {
|
|
2597
2838
|
if (this.isShuttingDown) return;
|
|
2598
2839
|
this.isShuttingDown = true;
|
|
2599
|
-
|
|
2600
|
-
\u26A0\uFE0F TestDino: Received ${signal}, sending interruption event...`);
|
|
2840
|
+
this.log.warn(`Received ${signal}, sending interruption event...`);
|
|
2601
2841
|
if (!this.initPromise) {
|
|
2602
2842
|
process.exit(exitCode);
|
|
2603
2843
|
}
|
|
@@ -2625,7 +2865,7 @@ var TestdinoReporter = class {
|
|
|
2625
2865
|
}, 100);
|
|
2626
2866
|
const forceExitTimer = setTimeout(() => {
|
|
2627
2867
|
clearInterval(keepAlive);
|
|
2628
|
-
|
|
2868
|
+
this.log.error("Force exit - send timeout exceeded");
|
|
2629
2869
|
this.wsClient?.close();
|
|
2630
2870
|
process.exit(exitCode);
|
|
2631
2871
|
}, 3e3);
|
|
@@ -2633,10 +2873,10 @@ var TestdinoReporter = class {
|
|
|
2633
2873
|
try {
|
|
2634
2874
|
await waitForPending();
|
|
2635
2875
|
await Promise.race([this.sendInterruptionEvent(event), this.timeoutPromise(2500, "Send timeout")]);
|
|
2636
|
-
|
|
2876
|
+
this.log.success("Interruption event sent");
|
|
2637
2877
|
} catch (error) {
|
|
2638
2878
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2639
|
-
|
|
2879
|
+
this.log.error(`Failed to send interruption event: ${errorMsg}`);
|
|
2640
2880
|
} finally {
|
|
2641
2881
|
clearTimeout(forceExitTimer);
|
|
2642
2882
|
clearInterval(keepAlive);
|
|
@@ -2660,7 +2900,7 @@ var TestdinoReporter = class {
|
|
|
2660
2900
|
await this.wsClient.send(event);
|
|
2661
2901
|
return;
|
|
2662
2902
|
} catch {
|
|
2663
|
-
|
|
2903
|
+
this.log.warn("WebSocket send failed, trying HTTP");
|
|
2664
2904
|
}
|
|
2665
2905
|
}
|
|
2666
2906
|
if (this.httpClient) {
|
|
@@ -2696,11 +2936,9 @@ var TestdinoReporter = class {
|
|
|
2696
2936
|
this.artifactUploader = new ArtifactUploader(sasToken, {
|
|
2697
2937
|
debug: this.config.debug
|
|
2698
2938
|
});
|
|
2699
|
-
|
|
2700
|
-
console.log("\u{1F4E4} TestDino: Artifact uploads enabled");
|
|
2701
|
-
}
|
|
2939
|
+
this.log.debug("Artifact uploads enabled");
|
|
2702
2940
|
} catch (error) {
|
|
2703
|
-
|
|
2941
|
+
this.log.warn(`Artifact uploads disabled - ${error instanceof Error ? error.message : String(error)}`);
|
|
2704
2942
|
this.artifactsEnabled = false;
|
|
2705
2943
|
this.artifactUploader = null;
|
|
2706
2944
|
}
|