testdriverai 7.1.4 → 7.2.0

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.
Files changed (70) hide show
  1. package/.github/workflows/acceptance.yaml +81 -0
  2. package/.github/workflows/publish.yaml +44 -0
  3. package/agent/index.js +18 -19
  4. package/agent/lib/commands.js +321 -121
  5. package/agent/lib/redraw.js +99 -39
  6. package/agent/lib/sandbox.js +98 -6
  7. package/agent/lib/sdk.js +25 -0
  8. package/agent/lib/system.js +2 -1
  9. package/agent/lib/validation.js +6 -6
  10. package/docs/docs.json +211 -101
  11. package/docs/snippets/tests/type-repeated-replay.mdx +1 -1
  12. package/docs/v7/_drafts/caching-selectors.mdx +24 -0
  13. package/docs/v7/api/act.mdx +1 -1
  14. package/docs/v7/api/assert.mdx +1 -1
  15. package/docs/v7/api/assertions.mdx +7 -7
  16. package/docs/v7/api/elements.mdx +78 -0
  17. package/docs/v7/api/find.mdx +38 -0
  18. package/docs/v7/api/focusApplication.mdx +2 -2
  19. package/docs/v7/api/hover.mdx +2 -2
  20. package/docs/v7/features/ai-native.mdx +57 -71
  21. package/docs/v7/features/application-logs.mdx +353 -0
  22. package/docs/v7/features/browser-logs.mdx +414 -0
  23. package/docs/v7/features/cache-management.mdx +402 -0
  24. package/docs/v7/features/continuous-testing.mdx +346 -0
  25. package/docs/v7/features/coverage.mdx +508 -0
  26. package/docs/v7/features/data-driven-testing.mdx +441 -0
  27. package/docs/v7/features/easy-to-write.mdx +2 -73
  28. package/docs/v7/features/enterprise.mdx +155 -39
  29. package/docs/v7/features/fast.mdx +63 -81
  30. package/docs/v7/features/managed-sandboxes.mdx +384 -0
  31. package/docs/v7/features/network-monitoring.mdx +568 -0
  32. package/docs/v7/features/observable.mdx +3 -22
  33. package/docs/v7/features/parallel-execution.mdx +381 -0
  34. package/docs/v7/features/powerful.mdx +1 -1
  35. package/docs/v7/features/reports.mdx +414 -0
  36. package/docs/v7/features/sandbox-customization.mdx +229 -0
  37. package/docs/v7/features/scalable.mdx +217 -2
  38. package/docs/v7/features/stable.mdx +106 -147
  39. package/docs/v7/features/system-performance.mdx +616 -0
  40. package/docs/v7/features/test-analytics.mdx +373 -0
  41. package/docs/v7/features/test-cases.mdx +393 -0
  42. package/docs/v7/features/test-replays.mdx +408 -0
  43. package/docs/v7/features/test-reports.mdx +308 -0
  44. package/docs/v7/getting-started/{running-and-debugging.mdx → debugging-tests.mdx} +12 -142
  45. package/docs/v7/getting-started/quickstart.mdx +22 -305
  46. package/docs/v7/getting-started/running-tests.mdx +173 -0
  47. package/docs/v7/overview/what-is-testdriver.mdx +2 -14
  48. package/docs/v7/presets/chrome-extension.mdx +147 -122
  49. package/interfaces/cli/commands/init.js +3 -3
  50. package/interfaces/cli/lib/base.js +3 -2
  51. package/interfaces/logger.js +0 -2
  52. package/interfaces/shared-test-state.mjs +0 -5
  53. package/interfaces/vitest-plugin.mjs +69 -42
  54. package/lib/core/Dashcam.js +65 -66
  55. package/lib/vitest/hooks.mjs +42 -50
  56. package/package.json +1 -1
  57. package/sdk-log-formatter.js +350 -175
  58. package/sdk.js +431 -116
  59. package/setup/aws/cloudformation.yaml +2 -2
  60. package/setup/aws/self-hosted.yml +1 -1
  61. package/test/testdriver/chrome-extension.test.mjs +55 -72
  62. package/test/testdriver/element-not-found.test.mjs +2 -1
  63. package/test/testdriver/hover-image.test.mjs +1 -1
  64. package/test/testdriver/scroll-until-text.test.mjs +10 -6
  65. package/test/testdriver/setup/lifecycleHelpers.mjs +19 -24
  66. package/test/testdriver/setup/testHelpers.mjs +18 -23
  67. package/vitest.config.mjs +3 -3
  68. package/.github/workflows/linux-tests.yml +0 -28
  69. package/docs/v7/getting-started/generating-tests.mdx +0 -525
  70. package/test/testdriver/auto-cache-key-demo.test.mjs +0 -56
package/sdk.js CHANGED
@@ -289,6 +289,44 @@ class Element {
289
289
  return this._found;
290
290
  }
291
291
 
292
+ /**
293
+ * Serialize element to JSON safely (removes circular references)
294
+ * This is automatically called by JSON.stringify()
295
+ * @returns {Object} Serializable representation of the element
296
+ */
297
+ toJSON() {
298
+ const result = {
299
+ description: this.description,
300
+ coordinates: this.coordinates,
301
+ found: this._found,
302
+ threshold: this._threshold,
303
+ x: this.coordinates?.x,
304
+ y: this.coordinates?.y,
305
+ };
306
+
307
+ // Include response metadata if available
308
+ if (this._response) {
309
+ result.cache = {
310
+ hit: this._response.cacheHit || this._response.cache_hit || this._response.cached || false,
311
+ strategy: this._response.cacheStrategy,
312
+ createdAt: this._response.cacheCreatedAt,
313
+ diffPercent: this._response.cacheDiffPercent,
314
+ imageUrl: this._response.cachedImageUrl,
315
+ };
316
+
317
+ result.similarity = this._response.similarity;
318
+ result.confidence = this._response.confidence;
319
+ result.selector = this._response.selector;
320
+
321
+ // Include AI response text if available
322
+ if (this._response.response?.content?.[0]?.text) {
323
+ result.aiResponse = this._response.response.content[0].text;
324
+ }
325
+ }
326
+
327
+ return result;
328
+ }
329
+
292
330
  /**
293
331
  * Find the element on screen
294
332
  * @param {string} [newDescription] - Optional new description to search for
@@ -301,7 +339,10 @@ class Element {
301
339
  this.description = newDescription;
302
340
  }
303
341
 
304
- const startTime = Date.now();
342
+ // Capture absolute timestamp at the very start of the command
343
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
344
+ const absoluteTimestamp = Date.now();
345
+ const startTime = absoluteTimestamp;
305
346
  let response = null;
306
347
  let findError = null;
307
348
 
@@ -380,8 +421,6 @@ class Element {
380
421
 
381
422
  const duration = Date.now() - startTime;
382
423
 
383
- console.log("AI Response Text:", response?.response.content[0]?.text);
384
-
385
424
  if (response && response.coordinates) {
386
425
  // Store response but clear large base64 data to prevent memory leaks
387
426
  this._response = this._sanitizeResponse(response);
@@ -394,6 +433,14 @@ class Element {
394
433
  this._response = this._sanitizeResponse(response);
395
434
  this._found = false;
396
435
  findError = "Element not found";
436
+
437
+ // Log not found
438
+ const duration = Date.now() - startTime;
439
+ const { events } = require("./agent/events.js");
440
+ const notFoundMessage = formatter.formatElementNotFound(description, {
441
+ duration: `${duration}ms`,
442
+ });
443
+ this.sdk.emitter.emit(events.log.log, notFoundMessage);
397
444
  }
398
445
  } catch (error) {
399
446
  this._response = error.response
@@ -402,27 +449,36 @@ class Element {
402
449
  this._found = false;
403
450
  findError = error.message;
404
451
  response = error.response;
452
+
453
+ // Log not found with error
454
+ const duration = Date.now() - startTime;
455
+ const { events } = require("./agent/events.js");
456
+ const notFoundMessage = formatter.formatElementNotFound(description, {
457
+ duration: `${duration}ms`,
458
+ error: error.message,
459
+ });
460
+ this.sdk.emitter.emit(events.log.log, notFoundMessage);
461
+
462
+ console.error("Error during find():", error);
405
463
  }
406
464
 
407
- // Track find interaction once at the end
465
+ // Track find interaction once at the end (fire-and-forget, don't block)
408
466
  const sessionId = this.sdk.getSessionId();
409
467
  if (sessionId && this.sdk.sandbox?.send) {
410
- try {
411
- await this.sdk.sandbox.send({
412
- type: "trackInteraction",
413
- interactionType: "find",
414
- session: sessionId,
415
- prompt: description,
416
- timestamp: startTime,
417
- success: this._found,
418
- error: findError,
419
- cacheHit: response?.cacheHit || response?.cache_hit || response?.cached || false,
420
- selector: response?.selector,
421
- selectorUsed: !!response?.selector,
422
- });
423
- } catch (err) {
468
+ await this.sdk.sandbox.send({
469
+ type: "trackInteraction",
470
+ interactionType: "find",
471
+ session: sessionId,
472
+ prompt: description,
473
+ timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
474
+ success: this._found,
475
+ error: findError,
476
+ cacheHit: response?.cacheHit || response?.cache_hit || response?.cached || false,
477
+ selector: response?.selector,
478
+ selectorUsed: !!response?.selector,
479
+ }).catch(err => {
424
480
  console.warn("Failed to track find interaction:", err.message);
425
- }
481
+ });
426
482
  }
427
483
 
428
484
  return this;
@@ -470,11 +526,15 @@ class Element {
470
526
 
471
527
  // Emit element found as log:log event
472
528
  const { events } = require("./agent/events.js");
529
+ const Dashcam = require("./lib/core/Dashcam");
530
+ const consoleUrl = Dashcam.getConsoleUrl(this.sdk.config?.TD_API_ROOT);
473
531
  const formattedMessage = formatter.formatElementFound(this.description, {
474
532
  x: this.coordinates.x,
475
533
  y: this.coordinates.y,
476
534
  duration: debugInfo.duration,
477
535
  cacheHit: debugInfo.cacheHit,
536
+ selectorId: this._response?.selector,
537
+ consoleUrl: consoleUrl,
478
538
  });
479
539
  this.sdk.emitter.emit(events.log.log, formattedMessage);
480
540
 
@@ -1079,7 +1139,6 @@ class TestDriverSDK {
1079
1139
 
1080
1140
  // Store sandbox configuration options
1081
1141
  this.sandboxAmi = options.sandboxAmi || null;
1082
- this.sandboxOs = options.sandboxOs || null;
1083
1142
  this.sandboxInstance = options.sandboxInstance || null;
1084
1143
 
1085
1144
  // Cache threshold configuration
@@ -1233,26 +1292,18 @@ class TestDriverSDK {
1233
1292
 
1234
1293
  await this.exec(shell, createLogCmd, 10000, true);
1235
1294
 
1236
- console.log('[provision.chrome] Adding web logs to dashcam...');
1237
- try {
1238
1295
  const urlObj = new URL(url);
1239
1296
  const domain = urlObj.hostname;
1240
1297
  const pattern = `*${domain}*`;
1241
1298
  await this._dashcam.addWebLog(pattern, 'Web Logs');
1242
- console.log(`[provision.chrome] ✅ Web logs added to dashcam (pattern: ${pattern})`);
1243
1299
 
1244
1300
  await this._dashcam.addFileLog(logPath, "TestDriver Log");
1245
1301
 
1246
- } catch (error) {
1247
- console.warn('[provision.chrome] ⚠️ Failed to add web logs:', error.message);
1248
- }
1249
1302
  }
1250
1303
 
1251
1304
  // Automatically start dashcam if not already recording
1252
1305
  if (!this._dashcam || !this._dashcam.recording) {
1253
- console.log('[provision.chrome] Starting dashcam...');
1254
1306
  await this.dashcam.start();
1255
- console.log('[provision.chrome] ✅ Dashcam started');
1256
1307
  }
1257
1308
 
1258
1309
  // Set up Chrome profile with preferences
@@ -1303,17 +1354,17 @@ class TestDriverSDK {
1303
1354
 
1304
1355
  const prefsJson = JSON.stringify(chromePrefs, null, 2);
1305
1356
  const writePrefCmd = this.os === 'windows'
1306
- ? `Set-Content -Path "${prefsPath}" -Value '${prefsJson.replace(/'/g, "''")}'`
1357
+ // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1358
+ ? `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1307
1359
  : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1308
1360
 
1309
1361
  await this.exec(shell, writePrefCmd, 10000, true);
1310
- console.log('[provision.chrome] ✅ Chrome preferences configured');
1311
1362
 
1312
1363
  // Build Chrome launch command
1313
1364
  const chromeArgs = [];
1314
1365
  if (maximized) chromeArgs.push('--start-maximized');
1315
1366
  if (guest) chromeArgs.push('--guest');
1316
- chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run', '--disable-infobars', `--user-data-dir=${userDataDir}`);
1367
+ chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run', '--no-experiments', '--disable-infobars', `--user-data-dir=${userDataDir}`);
1317
1368
 
1318
1369
  // Add dashcam-chrome extension on Linux
1319
1370
  if (this.os === 'linux') {
@@ -1341,30 +1392,305 @@ class TestDriverSDK {
1341
1392
  // Wait for Chrome to be ready
1342
1393
  await this.focusApplication('Google Chrome');
1343
1394
 
1344
-
1345
1395
  // Wait for URL to load
1346
1396
  try {
1347
1397
  const urlObj = new URL(url);
1348
1398
  const domain = urlObj.hostname;
1399
+
1400
+ for (let attempt = 0; attempt < 30; attempt++) {
1401
+ const result = await this.find(`${domain}`);
1402
+
1403
+ if (result.found()) {
1404
+ break;
1405
+ } else {
1406
+ await new Promise(resolve => setTimeout(resolve, 1000));
1407
+ }
1408
+ }
1349
1409
 
1350
- console.log(`[provision.chrome] Waiting for domain "${domain}" to appear in URL bar...`);
1410
+ await this.focusApplication('Google Chrome');
1411
+ } catch (e) {
1412
+ console.warn(`[provision.chrome] ⚠️ Could not parse URL "${url}":`, e.message);
1413
+ }
1414
+ },
1415
+
1416
+ /**
1417
+ * Launch Chrome browser with a custom extension loaded
1418
+ * @param {Object} options - Chrome extension launch options
1419
+ * @param {string} [options.extensionPath] - Local filesystem path to the unpacked extension directory
1420
+ * @param {string} [options.extensionId] - Chrome Web Store extension ID (e.g., "cjpalhdlnbpafiamejdnhcphjbkeiagm" for uBlock Origin)
1421
+ * @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
1422
+ * @param {boolean} [options.maximized=true] - Start maximized
1423
+ * @returns {Promise<void>}
1424
+ * @example
1425
+ * // Load extension from local path
1426
+ * await testdriver.exec('sh', 'git clone https://github.com/user/extension.git /tmp/extension');
1427
+ * await testdriver.provision.chromeExtension({
1428
+ * extensionPath: '/tmp/extension',
1429
+ * url: 'https://example.com'
1430
+ * });
1431
+ *
1432
+ * @example
1433
+ * // Load extension by Chrome Web Store ID
1434
+ * await testdriver.provision.chromeExtension({
1435
+ * extensionId: 'cjpalhdlnbpafiamejdnhcphjbkeiagm', // uBlock Origin
1436
+ * url: 'https://example.com'
1437
+ * });
1438
+ */
1439
+ chromeExtension: async (options = {}) => {
1440
+ // Automatically wait for connection to be ready
1441
+ await this.ready();
1442
+
1443
+ const {
1444
+ extensionPath: providedExtensionPath,
1445
+ extensionId,
1446
+ url = 'http://testdriver-sandbox.vercel.app/',
1447
+ maximized = true,
1448
+ } = options;
1449
+
1450
+ if (!providedExtensionPath && !extensionId) {
1451
+ throw new Error('[provision.chromeExtension] Either extensionPath or extensionId is required');
1452
+ }
1453
+
1454
+ let extensionPath = providedExtensionPath;
1455
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1456
+
1457
+ // If extensionId is provided, download and extract the extension from Chrome Web Store
1458
+ if (extensionId && !extensionPath) {
1459
+ console.log(`[provision.chromeExtension] Downloading extension ${extensionId} from Chrome Web Store...`);
1460
+
1461
+ const extensionDir = this.os === 'windows'
1462
+ ? `C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Extensions\\${extensionId}`
1463
+ : `/tmp/testdriver-extensions/${extensionId}`;
1464
+
1465
+ // Create extension directory
1466
+ const mkdirCmd = this.os === 'windows'
1467
+ ? `New-Item -ItemType Directory -Path "${extensionDir}" -Force | Out-Null`
1468
+ : `mkdir -p "${extensionDir}"`;
1469
+ await this.exec(shell, mkdirCmd, 10000, true);
1470
+
1471
+ // Download CRX from Chrome Web Store
1472
+ // The CRX download URL format for Chrome Web Store
1473
+ const crxUrl = `https://clients2.google.com/service/update2/crx?response=redirect&prodversion=131.0.0.0&acceptformat=crx2,crx3&x=id%3D${extensionId}%26installsource%3Dondemand%26uc`;
1474
+ const crxPath = this.os === 'windows'
1475
+ ? `${extensionDir}\\extension.crx`
1476
+ : `${extensionDir}/extension.crx`;
1477
+
1478
+ if (this.os === 'windows') {
1479
+ await this.exec(
1480
+ 'pwsh',
1481
+ `Invoke-WebRequest -Uri "${crxUrl}" -OutFile "${crxPath}"`,
1482
+ 60000,
1483
+ true
1484
+ );
1485
+ } else {
1486
+ await this.exec(
1487
+ 'sh',
1488
+ `curl -L -o "${crxPath}" "${crxUrl}"`,
1489
+ 60000,
1490
+ true
1491
+ );
1492
+ }
1493
+
1494
+ // Extract the CRX file (CRX is a ZIP with a header)
1495
+ // Skip the CRX header and extract as ZIP
1496
+ if (this.os === 'windows') {
1497
+ // PowerShell: Read CRX, skip header, extract ZIP
1498
+ await this.exec(
1499
+ 'pwsh',
1500
+ `
1501
+ $crxBytes = [System.IO.File]::ReadAllBytes("${crxPath}")
1502
+ # CRX3 header: 4 bytes magic + 4 bytes version + 4 bytes header length + header
1503
+ $magic = [System.Text.Encoding]::ASCII.GetString($crxBytes[0..3])
1504
+ if ($magic -eq "Cr24") {
1505
+ $headerLen = [BitConverter]::ToUInt32($crxBytes, 8)
1506
+ $zipStart = 12 + $headerLen
1507
+ } else {
1508
+ # CRX2 format
1509
+ $zipStart = 16 + [BitConverter]::ToUInt32($crxBytes, 8) + [BitConverter]::ToUInt32($crxBytes, 12)
1510
+ }
1511
+ $zipBytes = $crxBytes[$zipStart..($crxBytes.Length - 1)]
1512
+ $zipPath = "${extensionDir}\\extension.zip"
1513
+ [System.IO.File]::WriteAllBytes($zipPath, $zipBytes)
1514
+ Expand-Archive -Path $zipPath -DestinationPath "${extensionDir}\\unpacked" -Force
1515
+ `,
1516
+ 30000,
1517
+ true
1518
+ );
1519
+ extensionPath = `${extensionDir}\\unpacked`;
1520
+ } else {
1521
+ // Linux: Use unzip with offset or python to extract
1522
+ await this.exec(
1523
+ 'sh',
1524
+ `
1525
+ cd "${extensionDir}"
1526
+ # Extract CRX (skip header and unzip)
1527
+ # CRX3 format: magic(4) + version(4) + header_length(4) + header + zip
1528
+ python3 -c "
1529
+ import struct
1530
+ import zipfile
1531
+ import io
1532
+ import os
1533
+
1534
+ with open('extension.crx', 'rb') as f:
1535
+ data = f.read()
1536
+
1537
+ # Check magic number
1538
+ magic = data[:4]
1539
+ if magic == b'Cr24':
1540
+ # CRX3 format
1541
+ header_len = struct.unpack('<I', data[8:12])[0]
1542
+ zip_start = 12 + header_len
1543
+ else:
1544
+ # CRX2 format
1545
+ pub_key_len = struct.unpack('<I', data[8:12])[0]
1546
+ sig_len = struct.unpack('<I', data[12:16])[0]
1547
+ zip_start = 16 + pub_key_len + sig_len
1548
+
1549
+ zip_data = data[zip_start:]
1550
+ os.makedirs('unpacked', exist_ok=True)
1551
+ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1552
+ zf.extractall('unpacked')
1553
+ "
1554
+ `,
1555
+ 30000,
1556
+ true
1557
+ );
1558
+ extensionPath = `${extensionDir}/unpacked`;
1559
+ }
1351
1560
 
1561
+ console.log(`[provision.chromeExtension] Extension ${extensionId} extracted to ${extensionPath}`);
1562
+ }
1563
+
1564
+ // If dashcam is available and recording, add web logs for this domain
1565
+ if (this._dashcam) {
1566
+ // Create the log file on the remote machine
1567
+ const logPath = this.os === "windows"
1568
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1569
+ : "/tmp/testdriver.log";
1570
+
1571
+ const createLogCmd = this.os === "windows"
1572
+ ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1573
+ : `touch ${logPath}`;
1574
+
1575
+ await this.exec(shell, createLogCmd, 10000, true);
1576
+
1577
+ const urlObj = new URL(url);
1578
+ const domain = urlObj.hostname;
1579
+ const pattern = `*${domain}*`;
1580
+ await this._dashcam.addWebLog(pattern, 'Web Logs');
1581
+ await this._dashcam.addFileLog(logPath, "TestDriver Log");
1582
+ }
1583
+
1584
+ // Automatically start dashcam if not already recording
1585
+ if (!this._dashcam || !this._dashcam.recording) {
1586
+ await this.dashcam.start();
1587
+ }
1588
+
1589
+ // Set up Chrome profile with preferences
1590
+ const userDataDir = this.os === 'windows'
1591
+ ? 'C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome'
1592
+ : '/tmp/testdriver-chrome-profile';
1593
+
1594
+ // Create user data directory and Default profile directory
1595
+ const defaultProfileDir = this.os === 'windows'
1596
+ ? `${userDataDir}\\Default`
1597
+ : `${userDataDir}/Default`;
1598
+
1599
+ const createDirCmd = this.os === 'windows'
1600
+ ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1601
+ : `mkdir -p "${defaultProfileDir}"`;
1602
+
1603
+ await this.exec(shell, createDirCmd, 10000, true);
1604
+
1605
+ // Write Chrome preferences
1606
+ const chromePrefs = {
1607
+ credentials_enable_service: false,
1608
+ profile: {
1609
+ password_manager_enabled: false,
1610
+ default_content_setting_values: {}
1611
+ },
1612
+ signin: {
1613
+ allowed: false
1614
+ },
1615
+ sync: {
1616
+ requested: false,
1617
+ first_setup_complete: true,
1618
+ sync_all_os_types: false
1619
+ },
1620
+ autofill: {
1621
+ enabled: false
1622
+ },
1623
+ local_state: {
1624
+ browser: {
1625
+ has_seen_welcome_page: true
1626
+ }
1627
+ }
1628
+ };
1629
+
1630
+ const prefsPath = this.os === 'windows'
1631
+ ? `${defaultProfileDir}\\Preferences`
1632
+ : `${defaultProfileDir}/Preferences`;
1633
+
1634
+ const prefsJson = JSON.stringify(chromePrefs, null, 2);
1635
+ const writePrefCmd = this.os === 'windows'
1636
+ // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1637
+ ? `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1638
+ : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1639
+
1640
+ await this.exec(shell, writePrefCmd, 10000, true);
1641
+
1642
+ // Build Chrome launch command
1643
+ const chromeArgs = [];
1644
+ if (maximized) chromeArgs.push('--start-maximized');
1645
+ chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run', '--no-experiments', '--disable-infobars', '--disable-features=ChromeLabs', `--user-data-dir=${userDataDir}`);
1646
+
1647
+ // Add user extension and dashcam-chrome extension
1648
+ if (this.os === 'linux') {
1649
+ // Load both user extension and dashcam-chrome for web log capture
1650
+ chromeArgs.push(`--load-extension=${extensionPath},/usr/lib/node_modules/dashcam-chrome/build`);
1651
+ } else if (this.os === 'windows') {
1652
+ // On Windows, just load the user extension (dashcam-chrome not available)
1653
+ chromeArgs.push(`--load-extension=${extensionPath}`);
1654
+ }
1655
+
1656
+ // Launch Chrome
1657
+ if (this.os === 'windows') {
1658
+ const argsString = chromeArgs.map(arg => `"${arg}"`).join(', ');
1659
+ await this.exec(
1660
+ shell,
1661
+ `Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList ${argsString}, "${url}"`,
1662
+ 30000
1663
+ );
1664
+ } else {
1665
+ const argsString = chromeArgs.join(' ');
1666
+ await this.exec(
1667
+ shell,
1668
+ `chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
1669
+ 30000
1670
+ );
1671
+ }
1672
+
1673
+ // Wait for Chrome to be ready
1674
+ await this.focusApplication('Google Chrome');
1675
+
1676
+ // Wait for URL to load
1677
+ try {
1678
+ const urlObj = new URL(url);
1679
+ const domain = urlObj.hostname;
1680
+
1352
1681
  for (let attempt = 0; attempt < 30; attempt++) {
1353
- try {
1354
- const result = await this.find(`${domain}`);
1355
- if (result.found()) {
1356
- console.log(`[provision.chrome] ✅ Chrome ready at ${url}`);
1357
- break;
1358
- }
1359
- } catch (e) {
1360
- // Not found yet, continue polling
1682
+ const result = await this.find(`${domain}`);
1683
+
1684
+ if (result.found()) {
1685
+ break;
1686
+ } else {
1687
+ await new Promise(resolve => setTimeout(resolve, 1000));
1361
1688
  }
1362
- await new Promise(resolve => setTimeout(resolve, 1000));
1363
1689
  }
1364
1690
 
1365
1691
  await this.focusApplication('Google Chrome');
1366
1692
  } catch (e) {
1367
- console.warn(`[provision.chrome] ⚠️ Could not parse URL "${url}":`, e.message);
1693
+ console.warn(`[provision.chromeExtension] ⚠️ Could not parse URL "${url}":`, e.message);
1368
1694
  }
1369
1695
  },
1370
1696
 
@@ -1414,7 +1740,6 @@ class TestDriverSDK {
1414
1740
 
1415
1741
  // Wait for VS Code to be ready
1416
1742
  await this.focusApplication('Visual Studio Code');
1417
- console.log('[provision.vscode] ✅ VS Code ready');
1418
1743
  },
1419
1744
 
1420
1745
  /**
@@ -1451,7 +1776,6 @@ class TestDriverSDK {
1451
1776
  }
1452
1777
 
1453
1778
  await this.focusApplication('Electron');
1454
- console.log('[provision.electron] ✅ Electron app ready');
1455
1779
  },
1456
1780
  };
1457
1781
  }
@@ -1534,13 +1858,11 @@ class TestDriverSDK {
1534
1858
  } else if (this.sandboxInstance) {
1535
1859
  this.agent.sandboxInstance = this.sandboxInstance;
1536
1860
  }
1537
- // Use os from connectOptions if provided, otherwise fall back to constructor value
1861
+ // Use os from connectOptions if provided, otherwise fall back to this.os
1538
1862
  if (connectOptions.os !== undefined) {
1539
1863
  this.agent.sandboxOs = connectOptions.os;
1540
- } else if (this.sandboxOs) {
1541
- this.agent.sandboxOs = this.sandboxOs;
1864
+ this.os = connectOptions.os; // Update this.os to match
1542
1865
  } else {
1543
- // Fall back to this.os (which defaults to "linux")
1544
1866
  this.agent.sandboxOs = this.os;
1545
1867
  }
1546
1868
 
@@ -1553,6 +1875,17 @@ class TestDriverSDK {
1553
1875
  // Get the instance from the agent
1554
1876
  this.instance = this.agent.instance;
1555
1877
 
1878
+ // Ensure this.os reflects the actual sandbox OS (important for vitest reporter)
1879
+ // After buildEnv, agent.sandboxOs should contain the correct OS value
1880
+ if (this.agent.sandboxOs) {
1881
+ this.os = this.agent.sandboxOs;
1882
+ }
1883
+
1884
+ // Also ensure sandbox.os is set for consistency
1885
+ if (this.agent.sandbox && this.os) {
1886
+ this.agent.sandbox.os = this.os;
1887
+ }
1888
+
1556
1889
  // Expose the agent's commands, parser, and commander
1557
1890
  this.commands = this.agent.commands;
1558
1891
 
@@ -1689,7 +2022,10 @@ class TestDriverSDK {
1689
2022
  async findAll(description, options) {
1690
2023
  this._ensureConnected();
1691
2024
 
1692
- const startTime = Date.now();
2025
+ // Capture absolute timestamp at the very start of the command
2026
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
2027
+ const absoluteTimestamp = Date.now();
2028
+ const startTime = absoluteTimestamp;
1693
2029
 
1694
2030
  // Log finding all action
1695
2031
  const { events } = require("./agent/events.js");
@@ -1798,25 +2134,23 @@ class TestDriverSDK {
1798
2134
  return element;
1799
2135
  });
1800
2136
 
1801
- // Track successful findAll interaction
2137
+ // Track successful findAll interaction (fire-and-forget, don't block)
1802
2138
  const sessionId = this.getSessionId();
1803
2139
  if (sessionId && this.sandbox?.send) {
1804
- try {
1805
- await this.sandbox.send({
1806
- type: "trackInteraction",
1807
- interactionType: "findAll",
1808
- session: sessionId,
1809
- prompt: description,
1810
- timestamp: startTime,
1811
- success: true,
1812
- input: { count: elements.length },
1813
- cacheHit: response.cached || false,
1814
- selector: response.selector,
1815
- selectorUsed: !!response.selector,
1816
- });
1817
- } catch (err) {
2140
+ this.sandbox.send({
2141
+ type: "trackInteraction",
2142
+ interactionType: "findAll",
2143
+ session: sessionId,
2144
+ prompt: description,
2145
+ timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2146
+ success: true,
2147
+ input: { count: elements.length },
2148
+ cacheHit: response.cached || false,
2149
+ selector: response.selector,
2150
+ selectorUsed: !!response.selector,
2151
+ }).catch(err => {
1818
2152
  console.warn("Failed to track findAll interaction:", err.message);
1819
- }
2153
+ });
1820
2154
  }
1821
2155
 
1822
2156
  // Log debug information when elements are found
@@ -1835,49 +2169,45 @@ class TestDriverSDK {
1835
2169
 
1836
2170
  return elements;
1837
2171
  } else {
1838
- // No elements found - track interaction
2172
+ // No elements found - track interaction (fire-and-forget, don't block)
1839
2173
  const sessionId = this.getSessionId();
1840
2174
  if (sessionId && this.sandbox?.send) {
1841
- try {
1842
- await this.sandbox.send({
1843
- type: "trackInteraction",
1844
- interactionType: "findAll",
1845
- session: sessionId,
1846
- prompt: description,
1847
- timestamp: startTime,
1848
- success: false,
1849
- error: "No elements found",
1850
- input: { count: 0 },
1851
- cacheHit: response?.cached || false,
1852
- selector: response?.selector,
1853
- selectorUsed: !!response?.selector,
1854
- });
1855
- } catch (err) {
2175
+ this.sandbox.send({
2176
+ type: "trackInteraction",
2177
+ interactionType: "findAll",
2178
+ session: sessionId,
2179
+ prompt: description,
2180
+ timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2181
+ success: false,
2182
+ error: "No elements found",
2183
+ input: { count: 0 },
2184
+ cacheHit: response?.cached || false,
2185
+ selector: response?.selector,
2186
+ selectorUsed: !!response?.selector,
2187
+ }).catch(err => {
1856
2188
  console.warn("Failed to track findAll interaction:", err.message);
1857
- }
2189
+ });
1858
2190
  }
1859
2191
 
1860
2192
  // No elements found - return empty array
1861
2193
  return [];
1862
2194
  }
1863
2195
  } catch (error) {
1864
- // Track findAll error interaction
2196
+ // Track findAll error interaction (fire-and-forget, don't block)
1865
2197
  const sessionId = this.getSessionId();
1866
2198
  if (sessionId && this.sandbox?.send) {
1867
- try {
1868
- await this.sandbox.send({
1869
- type: "trackInteraction",
1870
- interactionType: "findAll",
1871
- session: sessionId,
1872
- prompt: description,
1873
- timestamp: startTime,
1874
- success: false,
1875
- error: error.message,
1876
- input: { count: 0 },
1877
- });
1878
- } catch (err) {
2199
+ this.sandbox.send({
2200
+ type: "trackInteraction",
2201
+ interactionType: "findAll",
2202
+ session: sessionId,
2203
+ prompt: description,
2204
+ timestamp: absoluteTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
2205
+ success: false,
2206
+ error: error.message,
2207
+ input: { count: 0 },
2208
+ }).catch(err => {
1879
2209
  console.warn("Failed to track findAll interaction:", err.message);
1880
- }
2210
+ });
1881
2211
  }
1882
2212
 
1883
2213
  const { events } = require("./agent/events.js");
@@ -2270,6 +2600,7 @@ class TestDriverSDK {
2270
2600
  _setupLogging() {
2271
2601
  // Track the last fatal error message to throw on exit
2272
2602
  let lastFatalError = null;
2603
+ const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
2273
2604
 
2274
2605
  // Set up markdown logger
2275
2606
  createMarkdownLogger(this.emitter);
@@ -2277,7 +2608,7 @@ class TestDriverSDK {
2277
2608
  // Set up basic event logging
2278
2609
  this.emitter.on("log:**", (message) => {
2279
2610
  const event = this.emitter.event;
2280
- if (event === events.log.debug) return;
2611
+ if (event === events.log.debug && !debugMode) return;
2281
2612
  if (this.loggingEnabled && message) {
2282
2613
  const prefixedMessage = this.testContext
2283
2614
  ? `[${this.testContext}] ${message}`
@@ -2307,23 +2638,6 @@ class TestDriverSDK {
2307
2638
  }
2308
2639
  });
2309
2640
 
2310
- // Handle redraw status for debugging scroll and other async operations
2311
- this.emitter.on("redraw:status", (status) => {
2312
- if (this.loggingEnabled) {
2313
- console.log(
2314
- `[redraw] screen:${status.redraw.text} network:${status.network.text} timeout:${status.timeout.text}`,
2315
- );
2316
- }
2317
- });
2318
-
2319
- this.emitter.on("redraw:complete", (info) => {
2320
- if (this.loggingEnabled) {
2321
- console.log(
2322
- `[redraw complete] screen:${info.screenHasRedrawn} network:${info.networkSettled} timeout:${info.isTimeout} elapsed:${info.timeElapsed}ms`,
2323
- );
2324
- }
2325
- });
2326
-
2327
2641
  // Handle exit events - throw error with meaningful message instead of calling process.exit
2328
2642
  // This allows test frameworks like Vitest to properly catch and display the error
2329
2643
  this.emitter.on(events.exit, (exitCode) => {
@@ -2341,7 +2655,7 @@ class TestDriverSDK {
2341
2655
  this.emitter.on("show-window", async (url) => {
2342
2656
  if (this.loggingEnabled) {
2343
2657
  console.log("");
2344
- console.log("Live test execution:");
2658
+ console.log("🔗 Live test execution:");
2345
2659
  if (this.config.CI) {
2346
2660
  // In CI mode, just print the view-only URL
2347
2661
  const u = new URL(url);
@@ -2356,6 +2670,7 @@ class TestDriverSDK {
2356
2670
  console.log(url);
2357
2671
  await this._openBrowser(url);
2358
2672
  }
2673
+
2359
2674
  }
2360
2675
  });
2361
2676
  }