@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.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
- console.warn(
104
- `\u26A0\uFE0F TestDino: WebSocket handshake timeout \u2014 server did not send 'connected' within ${HANDSHAKE_TIMEOUT_MS}ms. Resolving anyway.`
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
- console.error("Failed to parse WebSocket message:", error);
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
- console.error("Reconnection failed:", error);
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 this.sleep(delay);
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.all([
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
- let [branch, hash, message, author, email, timestamp] = results;
585
- const repoUrl = results[6];
586
- const isDirty = results[7];
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 projectNames = config.projects.map((project) => project.name).filter((name) => this.isNonEmptyString(name));
1253
- if (projectNames.length > 0) {
1254
- metadata.projects = projectNames;
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 this.sleep(delay);
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 this.sleep(delay);
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 (process.env.TESTDINO_DEBUG === "true" || process.env.TESTDINO_DEBUG === "1") {
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
- console.log("\u26A0\uFE0F TestDino: Reporter already configured in playwright.config, skipping duplicate instance");
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
- console.log("\u2705 TestDino: Authenticated successfully");
2084
+ this.log.success("Authenticated successfully");
1865
2085
  if (this.config.debug) {
1866
- console.log(`\u{1F50C} TestDino: Session ${this.sessionId} \u2014 reusing for WebSocket`);
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
- console.log("\u{1F50C} TestDino: WebSocket connected");
2094
+ this.log.debug("WebSocket connected");
1874
2095
  },
1875
2096
  onDisconnected: () => {
1876
- console.log("\u{1F50C} TestDino: WebSocket disconnected");
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
- console.error("\u274C TestDino: WebSocket error:", error.message);
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
- console.warn("\u26A0\uFE0F TestDino: WebSocket connection failed, using HTTP fallback");
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
- console.log(`\u{1F50D} TestDino: run:begin event details:`);
1910
- console.log(` runId: ${this.runId}`);
1911
- console.log(` ciRunId: ${this.config.ciRunId ?? "(not set)"}`);
1912
- console.log(` shard: ${config?.shard ? `${config.shard.current}/${config.shard.total}` : "(not sharded)"}`);
1913
- if (metadata.skeleton) {
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
- * Called after each test.
2063
- * Playwright does not await onTestEnd promises—pending work is awaited in onEnd.
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
- console.error(
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
- console.log("\u2705 TestDino: Tests completed (quota limit reached; not streamed to TestDino)");
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
- if (this.config.debug) {
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
- console.error("\u274C TestDino: Failed to flush final events:", error);
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
- console.log(
2240
- `\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}`
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
- console.log(
2245
- `\u{1F50D} TestDino: Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId} testId=${testEndEvent.testId} retry=${testEndEvent.retry} parallelIndex=${testEndEvent.parallelIndex}`
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
- console.log(`\u{1F50D} TestDino: Sending event type=${event.type} sequence=${event.sequence} runId=${event.runId}`);
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
- console.warn("\u26A0\uFE0F TestDino: WebSocket send failed, switching to HTTP fallback");
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
- console.error("\u274C TestDino: Failed to send events via HTTP:", error);
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
- console.warn(`\u26A0\uFE0F TestDino: ${result.failureCount}/${result.results.length} metadata collectors failed`);
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
- console.warn("\u26A0\uFE0F TestDino: Metadata collection failed:", error instanceof Error ? error.message : String(error));
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
- console.log(`
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
- console.error("\u274C TestDino: Force exit - send timeout exceeded");
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
- console.log("\u2705 TestDino: Interruption event sent");
2883
+ this.log.success("Interruption event sent");
2644
2884
  } catch (error) {
2645
2885
  const errorMsg = error instanceof Error ? error.message : String(error);
2646
- console.error(`\u274C TestDino: Failed to send interruption event: ${errorMsg}`);
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
- console.warn("\u26A0\uFE0F WebSocket send failed, trying HTTP");
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
- if (this.config.debug) {
2707
- console.log("\u{1F4E4} TestDino: Artifact uploads enabled");
2708
- }
2946
+ this.log.debug("Artifact uploads enabled");
2709
2947
  } catch (error) {
2710
- console.warn("\u26A0\uFE0F TestDino: Artifact uploads disabled -", error instanceof Error ? error.message : String(error));
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
  }