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
@@ -249,6 +249,12 @@ export async function createTestDriver(options = {}) {
249
249
  // Merge options: plugin global options < test-specific options
250
250
  const mergedOptions = { ...pluginOptions, ...options };
251
251
 
252
+ // Support TD_OS environment variable for specifying target OS (linux, mac, windows)
253
+ // Priority: test options > plugin options > environment variable > default (linux)
254
+ if (!mergedOptions.os && process.env.TD_OS) {
255
+ mergedOptions.os = process.env.TD_OS;
256
+ }
257
+
252
258
  // Extract TestDriver-specific options
253
259
  const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
254
260
 
@@ -264,10 +270,8 @@ export async function createTestDriver(options = {}) {
264
270
  const testdriver = new TestDriverSDK(apiKey, config);
265
271
 
266
272
  // Connect to sandbox
267
- console.log('[testdriver] Connecting to sandbox...');
268
273
  await testdriver.auth();
269
274
  await testdriver.connect();
270
- console.log('[testdriver] ✅ Connected to sandbox');
271
275
 
272
276
  return testdriver;
273
277
  }
@@ -312,9 +316,7 @@ export async function cleanupTestDriver(testdriver) {
312
316
  if (!testdriver) {
313
317
  return;
314
318
  }
315
-
316
- console.log('[testdriver] Cleaning up TestDriver client...');
317
-
319
+
318
320
  try {
319
321
  // Stop dashcam if it was started
320
322
  if (testdriver._dashcam && testdriver._dashcam.recording) {
@@ -337,7 +339,6 @@ export async function cleanupTestDriver(testdriver) {
337
339
  }
338
340
 
339
341
  await testdriver.disconnect();
340
- console.log('✅ Client disconnected');
341
342
  } catch (error) {
342
343
  console.error('Error disconnecting client:', error);
343
344
  }
@@ -351,7 +352,7 @@ async function handleProcessExit() {
351
352
  return;
352
353
  }
353
354
 
354
- logger.info("Process interrupted, marking test run as cancelled...");
355
+ logger.debug("Process interrupted, marking test run as cancelled...");
355
356
 
356
357
  try {
357
358
  const stats = {
@@ -378,7 +379,7 @@ async function handleProcessExit() {
378
379
  }
379
380
 
380
381
  await completeTestRun(completeData);
381
- logger.info("✅ Test run marked as cancelled");
382
+ logger.debug("✅ Test run marked as cancelled");
382
383
  } catch (error) {
383
384
  logger.error("Failed to mark test run as cancelled:", error.message);
384
385
  }
@@ -494,8 +495,6 @@ class TestDriverReporter {
494
495
  return;
495
496
  }
496
497
 
497
- logger.info("Starting test run initialization with API key...");
498
-
499
498
  try {
500
499
  // Exchange API key for JWT token
501
500
  logger.debug("Authenticating with API...");
@@ -544,7 +543,7 @@ class TestDriverReporter {
544
543
  startTime: pluginState.startTime,
545
544
  });
546
545
 
547
- logger.info(`Test run created: ${pluginState.testRunId}`);
546
+ logger.debug(`Test run created: ${pluginState.testRunId}`);
548
547
  } catch (error) {
549
548
  logger.error("Failed to initialize:", error.message);
550
549
  pluginState.apiKey = null;
@@ -566,7 +565,7 @@ class TestDriverReporter {
566
565
  return;
567
566
  }
568
567
 
569
- logger.info("Completing test run...");
568
+ logger.debug("Completing test run...");
570
569
 
571
570
  try {
572
571
  // Calculate statistics from testModules
@@ -574,14 +573,17 @@ class TestDriverReporter {
574
573
 
575
574
  logger.debug("Stats:", stats);
576
575
 
577
- // Determine overall status based on reason and stats
576
+ // Determine overall status based on stats (not reason, which is unreliable in parallel runs)
578
577
  let status = "passed";
579
- if (reason === "failed" || stats.failedTests > 0) {
578
+ if (stats.failedTests > 0) {
580
579
  status = "failed";
581
580
  } else if (reason === "interrupted") {
582
581
  status = "cancelled";
583
582
  } else if (stats.totalTests === 0) {
584
583
  status = "cancelled";
584
+ } else if (stats.passedTests === 0 && stats.skippedTests === 0) {
585
+ // No tests actually ran (all were filtered/excluded)
586
+ status = "cancelled";
585
587
  }
586
588
 
587
589
  // Complete test run via API
@@ -599,9 +601,12 @@ class TestDriverReporter {
599
601
 
600
602
  // Update platform if detected from test results
601
603
  const platform = getPlatform();
604
+ logger.debug(`Platform detection result: ${platform}, detectedPlatform in state: ${pluginState.detectedPlatform}`);
602
605
  if (platform) {
603
606
  completeData.platform = platform;
604
607
  logger.debug(`Updating test run with platform: ${platform}`);
608
+ } else {
609
+ logger.warn(`No platform detected, test run will keep default platform`);
605
610
  }
606
611
 
607
612
  // Wait for any pending operations (shouldn't be any, but just in case)
@@ -619,7 +624,17 @@ class TestDriverReporter {
619
624
  // Mark test run as completed to prevent duplicate completion
620
625
  pluginState.testRunCompleted = true;
621
626
 
622
- logger.info(`✅ Test run completed: ${stats.passedTests}/${stats.totalTests} passed`);
627
+ // Output the test run URL for CI to capture
628
+ const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
629
+ const consoleUrl = getConsoleUrl(pluginState.apiRoot);
630
+ if (testRunDbId) {
631
+ const testRunUrl = `${consoleUrl}/runs/${testRunDbId}`;
632
+ logger.debug(`🔗 View test run: ${testRunUrl}`);
633
+ // Output in a parseable format for CI
634
+ console.log(`TESTDRIVER_RUN_URL=${testRunUrl}`);
635
+ }
636
+
637
+ logger.debug(`✅ Test run completed: ${stats.passedTests}/${stats.totalTests} passed`);
623
638
  } catch (error) {
624
639
  logger.error("Failed to complete test run:", error.message);
625
640
  logger.debug("Error stack:", error.stack);
@@ -633,9 +648,6 @@ class TestDriverReporter {
633
648
  test,
634
649
  startTime: Date.now(),
635
650
  });
636
-
637
- // Try to detect platform from test context
638
- detectPlatformFromTest(test);
639
651
  }
640
652
 
641
653
  async onTestCaseResult(test) {
@@ -649,7 +661,7 @@ class TestDriverReporter {
649
661
  ? "skipped"
650
662
  : "failed";
651
663
 
652
- logger.info(`Test case completed: ${test.name} (${status})`);
664
+ logger.debug(`Test case completed: ${test.name} (${status})`);
653
665
 
654
666
  // Calculate duration from tracked start time
655
667
  const testCase = pluginState.testCases.get(test.id);
@@ -692,12 +704,9 @@ class TestDriverReporter {
692
704
  // Don't override duration from file - use Vitest's result.duration
693
705
  // duration is already set above from result.duration
694
706
 
695
- logger.debug(`Read from file - dashcam: ${dashcamUrl}, platform: ${platform}, sessionId: ${sessionId}, testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms`);
696
-
697
707
  // Update test run platform from first test that reports it
698
708
  if (platform && !pluginState.detectedPlatform) {
699
709
  pluginState.detectedPlatform = platform;
700
- logger.debug(`Detected platform from test: ${platform}`);
701
710
  }
702
711
 
703
712
  // Clean up the file after reading
@@ -811,8 +820,8 @@ class TestDriverReporter {
811
820
  const testCaseDbId = testCaseResponse.data?.id;
812
821
  const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
813
822
 
814
- logger.debug(`Reported test case to API${dashcamUrl ? " with dashcam URL" : ""}`);
815
- logger.info(`🔗 View test: ${pluginState.apiRoot.replace("testdriver-api.onrender.com", "console.testdriver.ai")}/runs/${testRunDbId}/${testCaseDbId}`);
823
+ console.log('');
824
+ console.log(`🔗 Test Report: ${getConsoleUrl(pluginState.apiRoot)}/runs/${testRunDbId}/${testCaseDbId}`);
816
825
  } catch (error) {
817
826
  logger.error("Failed to report test case:", error.message);
818
827
  }
@@ -823,6 +832,33 @@ class TestDriverReporter {
823
832
  // Helper Functions
824
833
  // ============================================================================
825
834
 
835
+ /**
836
+ * Maps an API root URL to its corresponding web console URL.
837
+ * The API and web console are served from different domains/ports.
838
+ *
839
+ * @param {string} apiRoot - The API root URL (e.g., https://testdriver-api.onrender.com)
840
+ * @returns {string} The corresponding web console URL
841
+ */
842
+ function getConsoleUrl(apiRoot) {
843
+
844
+ if (!apiRoot) return 'https://console.testdriver.ai';
845
+
846
+ // Production: API on render.com -> Console on testdriver.ai
847
+ if (apiRoot.includes('testdriver-api.onrender.com')) {
848
+ return 'https://console.testdriver.ai';
849
+ }
850
+
851
+ // Local development: API on localhost:1337 -> Web on localhost:3001
852
+ if (apiRoot.includes('ngrok.io')) {
853
+ return `http://localhost:3001`;
854
+ }
855
+
856
+ // Ngrok or other tunnels: assume same host, different path structure
857
+ // For ngrok, the API and web might be on same domain or user needs to configure
858
+ // Return as-is since we can't reliably determine the mapping
859
+ return apiRoot;
860
+ }
861
+
826
862
  function generateRunId() {
827
863
  return `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
828
864
  }
@@ -838,27 +874,18 @@ function getPlatform() {
838
874
  return pluginState.detectedPlatform;
839
875
  }
840
876
 
877
+ // Try to get platform from dashcam URLs (registered during test cleanup)
878
+ for (const [, data] of pluginState.dashcamUrls) {
879
+ if (data.platform) {
880
+ logger.debug(`Using platform from dashcam URL registration: ${data.platform}`);
881
+ return data.platform;
882
+ }
883
+ }
884
+
841
885
  logger.debug("Platform not yet detected from client");
842
886
  return null;
843
887
  }
844
888
 
845
- function detectPlatformFromTest(test) {
846
- // Check if testdriver client is accessible via test context
847
- const client = test.context?.testdriver || test.meta?.testdriver;
848
-
849
- if (client && client.os) {
850
- // Normalize platform value
851
- let platform = client.os.toLowerCase();
852
- if (platform === "darwin" || platform === "mac") platform = "mac";
853
- else if (platform === "win32" || platform === "windows")
854
- platform = "windows";
855
- else if (platform === "linux") platform = "linux";
856
-
857
- pluginState.detectedPlatform = platform;
858
- logger.debug(`Detected platform from test context: ${platform}`);
859
- }
860
- }
861
-
862
889
  function calculateStatsFromModules(testModules) {
863
890
  let totalTests = 0;
864
891
  let passedTests = 0;
@@ -973,7 +1000,7 @@ function getGitInfo() {
973
1000
  }
974
1001
  }
975
1002
 
976
- logger.info("Collected git info:", info);
1003
+ logger.debug("Collected git info:", info);
977
1004
  return info;
978
1005
  }
979
1006
 
@@ -9,6 +9,8 @@
9
9
  * - Retrieving replay URLs
10
10
  */
11
11
 
12
+ const { logger } = require('../../interfaces/logger');
13
+
12
14
  class Dashcam {
13
15
  /**
14
16
  * Create a Dashcam instance
@@ -74,6 +76,23 @@ class Dashcam {
74
76
  return this.client.config?.TD_API_ROOT || 'https://testdriver-api.onrender.com';
75
77
  }
76
78
 
79
+ /**
80
+ * Get console URL based on API root
81
+ * Maps API endpoints to their corresponding web console URLs
82
+ * @param {string} apiRoot - The API root URL
83
+ * @returns {string} The corresponding console URL
84
+ */
85
+ static getConsoleUrl(apiRoot = 'https://testdriver-api.onrender.com') {
86
+ // Map API roots to console URLs
87
+ const apiToConsoleMap = {
88
+ 'https://testdriver-api.onrender.com': 'https://console.testdriver.ai',
89
+ 'https://v6.testdriver.ai': 'https://console.testdriver.ai',
90
+ 'https://replayable-dev-ian-mac-m1-16.ngrok.io': 'http://localhost:3001',
91
+ };
92
+
93
+ return apiToConsoleMap[apiRoot] || 'https://console.testdriver.ai';
94
+ }
95
+
77
96
  /**
78
97
  * Get dashcam executable path
79
98
  * @private
@@ -83,7 +102,7 @@ class Dashcam {
83
102
  const npmPrefix = await this.client.exec(shell, 'npm prefix -g', 40000, true);
84
103
 
85
104
  if (this.client.os === 'windows') {
86
- return npmPrefix.trim() + '\\dashcam.cmd';
105
+ return 'dashcam';
87
106
  }
88
107
  return npmPrefix.trim() + '/bin/dashcam';
89
108
  }
@@ -98,33 +117,16 @@ class Dashcam {
98
117
  const shell = this._getShell();
99
118
  const apiRoot = this._getApiRoot();
100
119
 
101
- if (this.client.os === 'windows') {
102
- // Debug session info
103
- const debug = await this.client.exec(shell, 'query session', 40000, true);
104
- this._log('debug', 'Debug version output:', debug);
120
+ let install = await this.client.exec(
121
+ shell,
122
+ 'npm ls dashcam -g || echo "not installed"',
123
+ 40000,
124
+ true
125
+ );
126
+ this._log('debug', 'Dashcam install check:', install);
105
127
 
106
- // Uninstall and clear cache for fresh install
107
- await this.client.exec(shell, 'npm uninstall dashcam -g', 40000, true);
108
- await this.client.exec(shell, 'npm cache clean --force', 40000, true);
109
-
110
- // Install dashcam with TD_API_ROOT environment variable
111
- const installOutput = await this.client.exec(
112
- shell,
113
- `$env:TD_API_ROOT="${apiRoot}"; npm install dashcam@beta -g`,
114
- 120000,
115
- true
116
- );
117
- this._log('debug', 'Install dashcam output:', installOutput);
128
+ if (this.client.os === 'windows') {
118
129
 
119
- // Verify version
120
- const latestVersion = await this.client.exec(
121
- shell,
122
- 'npm view dashcam@beta version',
123
- 40000,
124
- true
125
- );
126
- this._log('debug', 'Latest beta version available:', latestVersion);
127
-
128
130
  const dashcamPath = await this._getDashcamPath();
129
131
  this._log('debug', 'Dashcam executable path:', dashcamPath);
130
132
 
@@ -145,16 +147,6 @@ class Dashcam {
145
147
  );
146
148
  this._log('debug', 'Dashcam version test:', versionTest);
147
149
 
148
- // Verify installation
149
- if (!installedVersion) {
150
- this._log('error', 'Dashcam version command returned null/empty');
151
- this._log('debug', 'Install output was:', installOutput);
152
- } else if (!installedVersion.includes('1.3.')) {
153
- this._log('warn', 'Dashcam version may be outdated. Expected 1.3.x, got:', installedVersion);
154
- } else {
155
- this._log('debug', 'Dashcam version verified:', installedVersion);
156
- }
157
-
158
150
  // Authenticate with TD_API_ROOT
159
151
  const authOutput = await this.client.exec(
160
152
  shell,
@@ -292,7 +284,6 @@ class Dashcam {
292
284
 
293
285
  // Auto-authenticate if not already done
294
286
  if (!this._authenticated) {
295
- this._log('info', 'Auto-authenticating dashcam...');
296
287
  await this.auth();
297
288
  }
298
289
 
@@ -300,7 +291,6 @@ class Dashcam {
300
291
  const apiRoot = this._getApiRoot();
301
292
 
302
293
  if (this.client.os === 'windows') {
303
- this._log('info', 'Starting dashcam recording on Windows...');
304
294
 
305
295
  const dashcamPath = await this._getDashcamPath();
306
296
  this._log('debug', 'Dashcam path:', dashcamPath);
@@ -316,11 +306,12 @@ class Dashcam {
316
306
 
317
307
  // Start dashcam record and redirect output with TD_API_ROOT
318
308
  const outputFile = 'C:\\Users\\testdriver\\.dashcam-cli\\dashcam-start.log';
319
- const titleArg = this.title ? ` --title="${this.title.replace(/"/g, '\"')}"` : '';
309
+ // const titleArg = this.title ? ` --title=\`"${this.title.replace(/"/g, '`"')}\`"` : '';
310
+ let titleArg = '';
320
311
  const startScript = `
321
312
  try {
322
313
  $env:TD_API_ROOT="${apiRoot}"
323
- $process = Start-Process "cmd.exe" -ArgumentList "/c", "${dashcamPath} record${titleArg} > ${outputFile} 2>&1" -PassThru
314
+ $process = Start-Process "cmd.exe" -ArgumentList "/c", "\`"${dashcamPath}\`" record${titleArg}"
324
315
  Write-Output "Process started with PID: $($process.Id)"
325
316
  Start-Sleep -Seconds 2
326
317
  if ($process.HasExited) {
@@ -332,7 +323,11 @@ class Dashcam {
332
323
  Write-Output "ERROR: $_"
333
324
  }
334
325
  `;
326
+
327
+ // add 2>&1" -PassThru
335
328
 
329
+ // Capture startTime right before issuing the dashcam command to sync with actual recording start
330
+ this.startTime = Date.now();
336
331
  const startOutput = await this.client.exec(shell, startScript, 10000, true);
337
332
  this._log('debug', 'Start-Process output:', startOutput);
338
333
 
@@ -349,23 +344,25 @@ class Dashcam {
349
344
  // Give process time to initialize
350
345
  await new Promise(resolve => setTimeout(resolve, 5000));
351
346
 
352
- this._log('info', 'Dashcam recording started');
347
+ this._log('debug', 'Dashcam recording started');
353
348
  } else {
354
349
  // Linux/Mac with TD_API_ROOT
355
- this._log('info', 'Starting dashcam recording on Linux/Mac...');
350
+ this._log('debug', 'Starting dashcam recording on Linux/Mac...');
356
351
  const titleArg = this.title ? ` --title="${this.title.replace(/"/g, '\"')}"` : '';
352
+ // Capture startTime right before issuing the dashcam command to sync with actual recording start
353
+ this.startTime = Date.now();
357
354
  await this.client.exec(shell, `TD_API_ROOT="${apiRoot}" dashcam record${titleArg} >/dev/null 2>&1 &`);
358
- this._log('info', 'Dashcam recording started');
355
+ this._log('debug', 'Dashcam recording started');
359
356
  }
360
357
 
361
358
  this.recording = true;
362
- this.startTime = Date.now(); // Record the timestamp when dashcam started
363
359
 
364
360
  // Update the session with dashcam start time for interaction timestamp synchronization
365
- if (this.client && this.client.agent && this.client.agent.session) {
361
+ const sessionId = this.client?.agent?.session?.get?.();
362
+ if (sessionId) {
366
363
  try {
367
- const apiRoot = this.apiRoot || process.env.TD_API_ROOT || 'https://console.testdriver.ai';
368
- const response = await fetch(`${apiRoot}/api/v7.0.0/testdriver/session/${this.client.agent.session}/update-dashcam-time`, {
364
+ const apiRoot = this.apiRoot || process.env.TD_API_ROOT || this._getApiRoot();
365
+ const response = await fetch(`${apiRoot}/api/v7.0.0/testdriver/session/${sessionId}/update-dashcam-time`, {
369
366
  method: 'POST',
370
367
  headers: {
371
368
  'Content-Type': 'application/json',
@@ -375,7 +372,7 @@ class Dashcam {
375
372
  });
376
373
 
377
374
  if (response.ok) {
378
- this._log('info', `Updated session ${this.client.agent.session} with dashcam start time: ${this.startTime}`);
375
+ this._log('debug', `Updated session ${sessionId} with dashcam start time: ${this.startTime}`);
379
376
  } else {
380
377
  this._log('warn', 'Failed to update session with dashcam start time:', response.statusText);
381
378
  }
@@ -406,7 +403,7 @@ class Dashcam {
406
403
  return null;
407
404
  }
408
405
 
409
- this._log('info', 'Stopping dashcam and retrieving URL...');
406
+ this._log('debug', 'Stopping dashcam and retrieving URL...');
410
407
  const shell = this._getShell();
411
408
  const apiRoot = this._getApiRoot();
412
409
  let output;
@@ -417,12 +414,12 @@ class Dashcam {
417
414
  const dashcamPath = await this._getDashcamPath();
418
415
 
419
416
  // Stop and get output with TD_API_ROOT
420
- output = await this.client.exec(shell, `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" stop`, 120000);
417
+ output = await this.client.exec(shell, `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" stop`, 300000, true);
421
418
  this._log('debug', 'Dashcam stop command output:', output);
422
419
  } else {
423
420
  // Linux/Mac with TD_API_ROOT
424
421
  const dashcamPath = await this._getDashcamPath();
425
- output = await this.client.exec(shell, `TD_API_ROOT="${apiRoot}" "${dashcamPath}" stop`, 60000, false);
422
+ output = await this.client.exec(shell, `TD_API_ROOT="${apiRoot}" "${dashcamPath}" stop`, 300000, true);
426
423
  this._log('debug', 'Dashcam command output:', output);
427
424
  }
428
425
 
@@ -437,7 +434,6 @@ class Dashcam {
437
434
  let url = replayUrlMatch[0];
438
435
  // Remove trailing punctuation but keep query params
439
436
  url = url.replace(/[.,;:!\)\]]+$/, '').trim();
440
- this._log('info', 'Found dashcam URL:', url);
441
437
  return url;
442
438
  }
443
439
 
@@ -446,7 +442,6 @@ class Dashcam {
446
442
  if (dashcamUrlMatch) {
447
443
  let url = dashcamUrlMatch[0];
448
444
  url = url.replace(/[.,;:!\?\)\]]+$/, '').trim();
449
- this._log('info', 'Found dashcam URL:', url);
450
445
  return url;
451
446
  }
452
447
 
@@ -459,7 +454,7 @@ class Dashcam {
459
454
  }
460
455
 
461
456
  /**
462
- * Internal logging - writes to testdriver log file but not user console
457
+ * Internal logging - uses TestDriver logger
463
458
  * @private
464
459
  */
465
460
  _log(level, ...args) {
@@ -467,19 +462,23 @@ class Dashcam {
467
462
  typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
468
463
  ).join(' ');
469
464
 
470
- const timestamp = new Date().toISOString();
471
- const logLine = `[${timestamp}] [DASHCAM:${level.toUpperCase()}] ${message}`;
465
+ const logMessage = `[DASHCAM] ${message}`;
472
466
 
473
- // Send to sandbox log file via output command (same as console interceptor)
474
- if (this.client?.sandbox?.instanceSocketConnected) {
475
- try {
476
- this.client.sandbox.send({
477
- type: "output",
478
- output: Buffer.from(logLine, "utf8").toString("base64"),
479
- });
480
- } catch {
481
- // Silently fail
482
- }
467
+ // Use the TestDriver logger based on level
468
+ switch (level) {
469
+ case 'error':
470
+ logger.error(logMessage);
471
+ break;
472
+ case 'warn':
473
+ logger.warn(logMessage);
474
+ break;
475
+ case 'debug':
476
+ logger.debug(logMessage);
477
+ break;
478
+ case 'info':
479
+ default:
480
+ logger.info(logMessage);
481
+ break;
483
482
  }
484
483
  }
485
484
 
@@ -19,6 +19,7 @@ import fs from 'fs';
19
19
  import os from 'os';
20
20
  import path from 'path';
21
21
  import { vi } from 'vitest';
22
+ import chalk from 'chalk';
22
23
  import TestDriverSDK from '../../sdk.js';
23
24
 
24
25
  /**
@@ -103,7 +104,6 @@ function setupConsoleSpy(client, taskId) {
103
104
  // Store spies on client for cleanup
104
105
  client._consoleSpies = { logSpy, errorSpy, warnSpy, infoSpy };
105
106
 
106
- console.log(`[testdriver] Console spy set up for task: ${taskId}`);
107
107
  }
108
108
 
109
109
  /**
@@ -162,6 +162,13 @@ export function TestDriver(context, options = {}) {
162
162
  // Merge options: plugin global options < test-specific options
163
163
  const mergedOptions = { ...pluginOptions, ...options };
164
164
 
165
+ // Support TD_OS environment variable for specifying target OS (linux, mac, windows)
166
+ // Priority: test options > plugin options > environment variable > default (linux)
167
+ if (!mergedOptions.os && process.env.TD_OS) {
168
+ mergedOptions.os = process.env.TD_OS;
169
+ console.log(`[testdriver] Set mergedOptions.os = ${mergedOptions.os} from TD_OS environment variable`);
170
+ }
171
+
165
172
  // Extract TestDriver-specific options
166
173
  const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
167
174
 
@@ -191,9 +198,7 @@ export function TestDriver(context, options = {}) {
191
198
 
192
199
  await testdriver.auth();
193
200
  await testdriver.connect();
194
-
195
- console.log('[testdriver] ✅ Connected to sandbox');
196
-
201
+
197
202
  if (debugConsoleSpy) {
198
203
  console.log('[DEBUG] After connect - sandbox.instanceSocketConnected:', testdriver.sandbox?.instanceSocketConnected);
199
204
  console.log('[DEBUG] After connect - sandbox.send:', typeof testdriver.sandbox?.send);
@@ -213,7 +218,6 @@ export function TestDriver(context, options = {}) {
213
218
  : `touch ${logPath}`;
214
219
 
215
220
  await testdriver.exec(shell, createLogCmd, 10000, true);
216
- console.log('[testdriver] ✅ Created log file:', logPath);
217
221
 
218
222
  // Add automatic log tracking when dashcam starts
219
223
  // Store original start method
@@ -226,56 +230,45 @@ export function TestDriver(context, options = {}) {
226
230
  // Register cleanup handler with dashcam.stop()
227
231
  if (!lifecycleHandlers.has(context.task)) {
228
232
  const cleanup = async () => {
229
- console.log('[testdriver] Cleaning up TestDriver client...');
230
233
  try {
231
234
  // Stop dashcam if it was started - with timeout to prevent hanging
232
235
  if (testdriver._dashcam && testdriver._dashcam.recording) {
233
236
  try {
234
- // Add a timeout wrapper to prevent dashcam.stop from hanging indefinitely
235
- const stopWithTimeout = Promise.race([
236
- testdriver.dashcam.stop(),
237
- new Promise((_, reject) =>
238
- setTimeout(() => reject(new Error('Dashcam stop timed out after 30s')), 30000)
239
- )
240
- ]);
237
+ const dashcamUrl = await testdriver.dashcam.stop();
238
+ console.log('');
239
+ console.log('🎥' + chalk.yellow(` Dashcam URL`) + `: ${dashcamUrl}`);
240
+ console.log('');
241
+ // Write test result to file for the reporter (cross-process communication)
242
+ // This should happen regardless of whether dashcam succeeded, to ensure platform info is available
243
+ const testId = context.task.id;
244
+ const platform = testdriver.os || 'linux';
245
+ const absolutePath = context.task.file?.filepath || context.task.file?.name || 'unknown';
246
+ const projectRoot = process.cwd();
247
+ const testFile = absolutePath !== 'unknown'
248
+ ? path.relative(projectRoot, absolutePath)
249
+ : absolutePath;
250
+
251
+ // Create results directory if it doesn't exist
252
+ const resultsDir = path.join(os.tmpdir(), 'testdriver-results');
253
+ if (!fs.existsSync(resultsDir)) {
254
+ fs.mkdirSync(resultsDir, { recursive: true });
255
+ }
256
+
257
+ // Write test result file
258
+ const testResultFile = path.join(resultsDir, `${testId}.json`);
259
+ const testResult = {
260
+ dashcamUrl: dashcamUrl || null,
261
+ platform,
262
+ testFile,
263
+ testOrder: 0,
264
+ sessionId: testdriver.getSessionId(),
265
+ };
241
266
 
242
- const dashcamUrl = await stopWithTimeout;
243
- console.log('🎥 Dashcam URL:', dashcamUrl);
267
+ fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
244
268
 
245
- // Write dashcam URL to file for the reporter (cross-process communication)
246
- if (dashcamUrl) {
247
- const testId = context.task.id;
248
- const platform = testdriver.os || 'linux';
249
- const absolutePath = context.task.file?.filepath || context.task.file?.name || 'unknown';
250
- const projectRoot = process.cwd();
251
- const testFile = absolutePath !== 'unknown'
252
- ? path.relative(projectRoot, absolutePath)
253
- : absolutePath;
254
-
255
- // Create results directory if it doesn't exist
256
- const resultsDir = path.join(os.tmpdir(), 'testdriver-results');
257
- if (!fs.existsSync(resultsDir)) {
258
- fs.mkdirSync(resultsDir, { recursive: true });
259
- }
260
-
261
- // Write test result file
262
- const testResultFile = path.join(resultsDir, `${testId}.json`);
263
- const testResult = {
264
- dashcamUrl,
265
- platform,
266
- testFile,
267
- testOrder: 0,
268
- sessionId: testdriver.getSessionId(),
269
- };
270
-
271
- fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
272
- console.log(`[testdriver] ✅ Wrote dashcam URL to ${testResultFile}`);
273
-
274
- // Also register in memory if plugin is available
275
- if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
276
- globalThis.__testdriverPlugin.registerDashcamUrl(testId, dashcamUrl, platform);
277
- console.log(`[testdriver] ✅ Registered dashcam URL in memory for test ${testId}`);
278
- }
269
+ // Also register in memory if plugin is available
270
+ if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
271
+ globalThis.__testdriverPlugin.registerDashcamUrl(testId, dashcamUrl, platform);
279
272
  }
280
273
  } catch (error) {
281
274
  // Log more detailed error information for debugging
@@ -305,7 +298,6 @@ export function TestDriver(context, options = {}) {
305
298
  testdriver.disconnect(),
306
299
  new Promise((resolve) => setTimeout(resolve, 5000)) // 5s timeout for disconnect
307
300
  ]);
308
- console.log('✅ Client disconnected');
309
301
  } catch (error) {
310
302
  console.error('Error disconnecting client:', error);
311
303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.1.4",
3
+ "version": "7.2.0",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "exports": {