@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/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
- console.warn(
97
- `\u26A0\uFE0F TestDino: WebSocket handshake timeout \u2014 server did not send 'connected' within ${HANDSHAKE_TIMEOUT_MS}ms. Resolving anyway.`
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
- console.error("Failed to parse WebSocket message:", error);
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
- console.error("Reconnection failed:", error);
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 this.sleep(delay);
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.all([
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
- let [branch, hash, message, author, email, timestamp] = results;
578
- const repoUrl = results[6];
579
- const isDirty = results[7];
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 projectNames = config.projects.map((project) => project.name).filter((name) => this.isNonEmptyString(name));
1246
- if (projectNames.length > 0) {
1247
- metadata.projects = projectNames;
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 this.sleep(delay);
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 this.sleep(delay);
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 (process.env.TESTDINO_DEBUG === "true" || process.env.TESTDINO_DEBUG === "1") {
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
- console.log("\u26A0\uFE0F TestDino: Reporter already configured in playwright.config, skipping duplicate instance");
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
- console.log("\u2705 TestDino: Authenticated successfully");
2077
+ this.log.success("Authenticated successfully");
1858
2078
  if (this.config.debug) {
1859
- console.log(`\u{1F50C} TestDino: Session ${this.sessionId} \u2014 reusing for WebSocket`);
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
- console.log("\u{1F50C} TestDino: WebSocket connected");
2087
+ this.log.debug("WebSocket connected");
1867
2088
  },
1868
2089
  onDisconnected: () => {
1869
- console.log("\u{1F50C} TestDino: WebSocket disconnected");
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
- console.error("\u274C TestDino: WebSocket error:", error.message);
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
- console.warn("\u26A0\uFE0F TestDino: WebSocket connection failed, using HTTP fallback");
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
- console.log(`\u{1F50D} TestDino: run:begin event details:`);
1903
- console.log(` runId: ${this.runId}`);
1904
- console.log(` ciRunId: ${this.config.ciRunId ?? "(not set)"}`);
1905
- console.log(` shard: ${config?.shard ? `${config.shard.current}/${config.shard.total}` : "(not sharded)"}`);
1906
- if (metadata.skeleton) {
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
- * Called after each test.
2056
- * Playwright does not await onTestEnd promises—pending work is awaited in onEnd.
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
- console.error(
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
- console.log("\u2705 TestDino: Tests completed (quota limit reached; not streamed to TestDino)");
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
- if (this.config.debug) {
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
- console.error("\u274C TestDino: Failed to flush final events:", error);
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
- console.log(
2233
- `\u{1F50D} TestDino: Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId} testId=${testBeginEvent.testId} retry=${testBeginEvent.retry} parallelIndex=${testBeginEvent.parallelIndex} title=${testBeginEvent.title}`
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
- console.log(
2238
- `\u{1F50D} TestDino: Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId} testId=${testEndEvent.testId} retry=${testEndEvent.retry} parallelIndex=${testEndEvent.parallelIndex}`
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
- console.log(`\u{1F50D} TestDino: Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId}`);
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
- console.warn("\u26A0\uFE0F TestDino: WebSocket send failed, switching to HTTP fallback");
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
- console.error("\u274C TestDino: Failed to send events via HTTP:", error);
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
- console.warn(`\u26A0\uFE0F TestDino: ${result.failureCount}/${result.results.length} metadata collectors failed`);
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
- console.warn("\u26A0\uFE0F TestDino: Metadata collection failed:", error instanceof Error ? error.message : String(error));
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
- console.log(`
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
- console.error("\u274C TestDino: Force exit - send timeout exceeded");
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
- console.log("\u2705 TestDino: Interruption event sent");
2876
+ this.log.success("Interruption event sent");
2637
2877
  } catch (error) {
2638
2878
  const errorMsg = error instanceof Error ? error.message : String(error);
2639
- console.error(`\u274C TestDino: Failed to send interruption event: ${errorMsg}`);
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
- console.warn("\u26A0\uFE0F WebSocket send failed, trying HTTP");
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
- if (this.config.debug) {
2700
- console.log("\u{1F4E4} TestDino: Artifact uploads enabled");
2701
- }
2939
+ this.log.debug("Artifact uploads enabled");
2702
2940
  } catch (error) {
2703
- console.warn("\u26A0\uFE0F TestDino: Artifact uploads disabled -", error instanceof Error ? error.message : String(error));
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
  }