testdriverai 7.0.0 → 7.1.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 (112) hide show
  1. package/AGENTS.md +550 -0
  2. package/CODEOWNERS +0 -1
  3. package/README.md +126 -0
  4. package/agent/index.js +43 -18
  5. package/agent/lib/commands.js +794 -135
  6. package/agent/lib/redraw.js +124 -39
  7. package/agent/lib/sandbox.js +10 -1
  8. package/agent/lib/sdk.js +21 -0
  9. package/docs/MIGRATION.md +425 -0
  10. package/docs/PRESETS.md +210 -0
  11. package/docs/docs.json +91 -37
  12. package/docs/guide/best-practices-polling.mdx +154 -0
  13. package/docs/v7/api/dashcam.mdx +497 -0
  14. package/docs/v7/api/doubleClick.mdx +102 -0
  15. package/docs/v7/api/mouseDown.mdx +161 -0
  16. package/docs/v7/api/mouseUp.mdx +164 -0
  17. package/docs/v7/api/rightClick.mdx +123 -0
  18. package/docs/v7/getting-started/configuration.mdx +380 -0
  19. package/docs/v7/getting-started/quickstart.mdx +273 -140
  20. package/docs/v7/guides/best-practices.mdx +486 -0
  21. package/docs/v7/guides/caching-ai.mdx +215 -0
  22. package/docs/v7/guides/caching-selectors.mdx +292 -0
  23. package/docs/v7/guides/caching.mdx +366 -0
  24. package/docs/v7/guides/ci-cd/azure.mdx +587 -0
  25. package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
  26. package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
  27. package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
  28. package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
  29. package/docs/v7/guides/ci-cd/travis.mdx +438 -0
  30. package/docs/v7/guides/debugging.mdx +349 -0
  31. package/docs/v7/guides/faq.mdx +393 -0
  32. package/docs/v7/guides/performance.mdx +517 -0
  33. package/docs/v7/guides/troubleshooting.mdx +526 -0
  34. package/docs/v7/guides/vitest-plugin.mdx +477 -0
  35. package/docs/v7/guides/vitest.mdx +535 -0
  36. package/docs/v7/platforms/linux.mdx +308 -0
  37. package/docs/v7/platforms/macos.mdx +433 -0
  38. package/docs/v7/platforms/windows.mdx +430 -0
  39. package/docs/v7/presets/chrome-extension.mdx +223 -0
  40. package/docs/v7/presets/chrome.mdx +287 -0
  41. package/docs/v7/presets/electron.mdx +435 -0
  42. package/docs/v7/presets/vscode.mdx +398 -0
  43. package/docs/v7/presets/webapp.mdx +396 -0
  44. package/docs/v7/progressive-apis/CORE.md +459 -0
  45. package/docs/v7/progressive-apis/HOOKS.md +360 -0
  46. package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
  47. package/docs/v7/progressive-apis/PROVISION.md +266 -0
  48. package/interfaces/vitest-plugin.mjs +186 -100
  49. package/package.json +12 -1
  50. package/sdk.d.ts +335 -42
  51. package/sdk.js +756 -95
  52. package/src/core/Dashcam.js +469 -0
  53. package/src/core/index.d.ts +150 -0
  54. package/src/core/index.js +12 -0
  55. package/src/presets/index.mjs +331 -0
  56. package/src/vitest/extended.mjs +108 -0
  57. package/src/vitest/hooks.d.ts +119 -0
  58. package/src/vitest/hooks.mjs +298 -0
  59. package/src/vitest/index.mjs +64 -0
  60. package/src/vitest/lifecycle.mjs +277 -0
  61. package/src/vitest/utils.mjs +150 -0
  62. package/test/dashcam.test.js +137 -0
  63. package/testdriver/acceptance-sdk/assert.test.mjs +13 -31
  64. package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
  65. package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
  66. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +7 -19
  67. package/testdriver/acceptance-sdk/element-not-found.test.mjs +6 -19
  68. package/testdriver/acceptance-sdk/exec-js.test.mjs +6 -18
  69. package/testdriver/acceptance-sdk/exec-output.test.mjs +8 -20
  70. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +13 -25
  71. package/testdriver/acceptance-sdk/focus-window.test.mjs +8 -20
  72. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +5 -20
  73. package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
  74. package/testdriver/acceptance-sdk/hover-image.test.mjs +10 -19
  75. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +7 -19
  76. package/testdriver/acceptance-sdk/hover-text.test.mjs +5 -19
  77. package/testdriver/acceptance-sdk/match-image.test.mjs +7 -19
  78. package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
  79. package/testdriver/acceptance-sdk/press-keys.test.mjs +5 -19
  80. package/testdriver/acceptance-sdk/prompt.test.mjs +6 -18
  81. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +6 -20
  82. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +6 -18
  83. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +9 -23
  84. package/testdriver/acceptance-sdk/scroll.test.mjs +12 -21
  85. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +124 -352
  86. package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
  87. package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
  88. package/testdriver/acceptance-sdk/type.test.mjs +19 -58
  89. package/vitest.config.mjs +1 -0
  90. package/.vscode/mcp.json +0 -9
  91. package/MIGRATION.md +0 -389
  92. package/PLUGIN_MIGRATION.md +0 -222
  93. package/PROMPT_CACHE.md +0 -200
  94. package/SDK_LOGGING.md +0 -222
  95. package/SDK_MIGRATION.md +0 -474
  96. package/SDK_README.md +0 -1122
  97. package/debug-screenshot-1763401388589.png +0 -0
  98. package/examples/run-tests-with-recording.sh +0 -70
  99. package/examples/screenshot-example.js +0 -63
  100. package/examples/sdk-awesome-logs-demo.js +0 -177
  101. package/examples/sdk-cache-thresholds.js +0 -96
  102. package/examples/sdk-element-properties.js +0 -155
  103. package/examples/sdk-simple-example.js +0 -65
  104. package/examples/test-recording-example.test.js +0 -166
  105. package/mcp-server/AI_GUIDELINES.md +0 -57
  106. package/test-find-api.js +0 -73
  107. package/test-prompt-cache.js +0 -96
  108. package/test-sandbox-render.js +0 -28
  109. package/test-sdk-methods.js +0 -15
  110. package/test-sdk-refactor.js +0 -53
  111. package/test-stack-trace.mjs +0 -57
  112. package/testdriver/acceptance-sdk/setup/lifecycleHelpers.mjs +0 -239
package/sdk.js CHANGED
@@ -3,8 +3,69 @@
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
  const os = require("os");
6
+ const crypto = require("crypto");
6
7
  const { formatter } = require("./sdk-log-formatter");
7
8
 
9
+ /**
10
+ * Get the file path of the caller (the file that called TestDriver)
11
+ * @returns {string|null} File path or null if not found
12
+ */
13
+ function getCallerFilePath() {
14
+ const originalPrepareStackTrace = Error.prepareStackTrace;
15
+ try {
16
+ const err = new Error();
17
+ Error.prepareStackTrace = (_, stack) => stack;
18
+ const stack = err.stack;
19
+ Error.prepareStackTrace = originalPrepareStackTrace;
20
+
21
+ // Look for the first file that's not sdk.js, hooks.mjs, or node internals
22
+ for (const callSite of stack) {
23
+ const fileName = callSite.getFileName();
24
+ if (fileName &&
25
+ !fileName.includes('sdk.js') &&
26
+ !fileName.includes('hooks.mjs') &&
27
+ !fileName.includes('hooks.js') &&
28
+ !fileName.includes('node_modules') &&
29
+ !fileName.includes('node:internal') &&
30
+ fileName !== 'evalmachine.<anonymous>') {
31
+ return fileName;
32
+ }
33
+ }
34
+ } catch (error) {
35
+ // Silently fail and return null
36
+ } finally {
37
+ Error.prepareStackTrace = originalPrepareStackTrace;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * Generate a hash of the caller file for use as a cache key
44
+ * @returns {string|null} Hash of the file or null if file not found
45
+ */
46
+ function getCallerFileHash() {
47
+ const filePath = getCallerFilePath();
48
+ if (!filePath) {
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ // Handle file:// URLs by converting to file system path
54
+ let fsPath = filePath;
55
+ if (filePath.startsWith('file://')) {
56
+ fsPath = filePath.replace('file://', '');
57
+ }
58
+
59
+ const fileContent = fs.readFileSync(fsPath, 'utf-8');
60
+ const hash = crypto.createHash('sha256').update(fileContent).digest('hex');
61
+ // Return first 16 chars of hash for brevity
62
+ return hash.substring(0, 16);
63
+ } catch (error) {
64
+ // If we can't read the file, return null
65
+ return null;
66
+ }
67
+ }
68
+
8
69
  /**
9
70
  * Custom error class for element operation failures
10
71
  * Includes debugging information like screenshots and AI responses
@@ -13,8 +74,8 @@ class ElementNotFoundError extends Error {
13
74
  constructor(message, debugInfo = {}) {
14
75
  super(message);
15
76
  this.name = "ElementNotFoundError";
16
- this.screenshot = debugInfo.screenshot;
17
- this.aiResponse = debugInfo.aiResponse;
77
+ // Sanitize aiResponse to remove base64 images before storing
78
+ this.aiResponse = this._sanitizeAiResponse(debugInfo.aiResponse);
18
79
  this.description = debugInfo.description;
19
80
  this.timestamp = new Date().toISOString();
20
81
  this.screenshotPath = null;
@@ -24,8 +85,9 @@ class ElementNotFoundError extends Error {
24
85
  Error.captureStackTrace(this, ElementNotFoundError);
25
86
  }
26
87
 
27
- // Write screenshot to temp directory
28
- if (this.screenshot) {
88
+ // Write screenshot to temp directory immediately (don't store on error object)
89
+ // This prevents vitest from serializing huge base64 strings
90
+ if (debugInfo.screenshot) {
29
91
  try {
30
92
  const tempDir = path.join(os.tmpdir(), "testdriver-debug");
31
93
  if (!fs.existsSync(tempDir)) {
@@ -36,7 +98,7 @@ class ElementNotFoundError extends Error {
36
98
  this.screenshotPath = path.join(tempDir, filename);
37
99
 
38
100
  // Remove data:image/png;base64, prefix if present
39
- const base64Data = this.screenshot.replace(
101
+ const base64Data = debugInfo.screenshot.replace(
40
102
  /^data:image\/\w+;base64,/,
41
103
  "",
42
104
  );
@@ -182,6 +244,25 @@ class ElementNotFoundError extends Error {
182
244
  this.stack = filteredLines.join("\n");
183
245
  }
184
246
  }
247
+
248
+ /**
249
+ * Sanitize AI response by removing large base64 data to prevent serialization issues
250
+ * @private
251
+ * @param {Object} response - AI response
252
+ * @returns {Object} Sanitized response
253
+ */
254
+ _sanitizeAiResponse(response) {
255
+ if (!response) return null;
256
+
257
+ // Create shallow copy and remove large base64 fields
258
+ const sanitized = { ...response };
259
+ delete sanitized.croppedImage;
260
+ delete sanitized.screenshot;
261
+ delete sanitized.pixelDiffImage;
262
+ // Keep cachedImageUrl as it's just a URL string, not base64 data
263
+
264
+ return sanitized;
265
+ }
185
266
  }
186
267
 
187
268
  /**
@@ -213,16 +294,18 @@ class Element {
213
294
  /**
214
295
  * Find the element on screen
215
296
  * @param {string} [newDescription] - Optional new description to search for
216
- * @param {number} [cacheThreshold] - Cache threshold for this specific find (overrides global setting)
297
+ * @param {Object} [options] - Optional options object with cacheThreshold and/or cacheKey
217
298
  * @returns {Promise<Element>} This element instance
218
299
  */
219
- async find(newDescription, cacheThreshold) {
300
+ async find(newDescription, options) {
220
301
  const description = newDescription || this.description;
221
302
  if (newDescription) {
222
303
  this.description = newDescription;
223
304
  }
224
305
 
225
306
  const startTime = Date.now();
307
+ let response = null;
308
+ let findError = null;
226
309
 
227
310
  const debugMode =
228
311
  process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
@@ -239,9 +322,38 @@ class Element {
239
322
  this._screenshot = screenshot;
240
323
  }
241
324
 
242
- // Use per-command threshold if provided, otherwise fall back to global threshold
243
- const threshold =
244
- cacheThreshold ?? this.sdk.cacheThresholds?.find ?? 0.05;
325
+ // Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
326
+ let cacheKey = null;
327
+ let cacheThreshold = null;
328
+
329
+ if (typeof options === 'number') {
330
+ // Legacy: options is just a number threshold
331
+ cacheThreshold = options;
332
+ } else if (typeof options === 'object' && options !== null) {
333
+ // New: options is an object with cacheKey and/or cacheThreshold
334
+ cacheKey = options.cacheKey || null;
335
+ cacheThreshold = options.cacheThreshold ?? null;
336
+ }
337
+
338
+ // Use default cacheKey from SDK constructor if not provided in find() options
339
+ if (!cacheKey && this.sdk.options?.cacheKey) {
340
+ cacheKey = this.sdk.options.cacheKey;
341
+ }
342
+
343
+ // Determine threshold:
344
+ // - If cacheKey is provided, enable cache (threshold = 0.05 or custom)
345
+ // - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
346
+ let threshold;
347
+ if (cacheKey) {
348
+ // cacheKey provided - enable cache with threshold
349
+ threshold = cacheThreshold ?? 0.05;
350
+ } else if (cacheThreshold !== null) {
351
+ // Explicit threshold provided without cacheKey
352
+ threshold = cacheThreshold;
353
+ } else {
354
+ // No cacheKey, no explicit threshold - use global default (which is -1 now)
355
+ threshold = this.sdk.cacheThresholds?.find ?? -1;
356
+ }
245
357
 
246
358
  // Store the threshold for debugging
247
359
  this._threshold = threshold;
@@ -249,16 +361,21 @@ class Element {
249
361
  // Debug log threshold
250
362
  if (debugMode) {
251
363
  const { events } = require("./agent/events.js");
364
+ const autoGenMsg = (this.sdk._autoGeneratedCacheKey && cacheKey === this.sdk.options.cacheKey)
365
+ ? ' (auto-generated from file hash)'
366
+ : '';
252
367
  this.sdk.emitter.emit(
253
368
  events.log.debug,
254
- `🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"})`,
369
+ `🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
255
370
  );
256
371
  }
257
372
 
258
- const response = await this.sdk.apiClient.req("find", {
373
+ response = await this.sdk.apiClient.req("find", {
374
+ session: this.sdk.getSessionId(),
259
375
  element: description,
260
376
  image: screenshot,
261
377
  threshold: threshold,
378
+ cacheKey: cacheKey,
262
379
  os: this.sdk.os,
263
380
  resolution: this.sdk.resolution,
264
381
  });
@@ -278,12 +395,36 @@ class Element {
278
395
  } else {
279
396
  this._response = this._sanitizeResponse(response);
280
397
  this._found = false;
398
+ findError = "Element not found";
281
399
  }
282
400
  } catch (error) {
283
401
  this._response = error.response
284
402
  ? this._sanitizeResponse(error.response)
285
403
  : null;
286
404
  this._found = false;
405
+ findError = error.message;
406
+ response = error.response;
407
+ }
408
+
409
+ // Track find interaction once at the end
410
+ const sessionId = this.sdk.getSessionId();
411
+ if (sessionId && this.sdk.sandbox?.send) {
412
+ try {
413
+ await this.sdk.sandbox.send({
414
+ type: "trackInteraction",
415
+ interactionType: "find",
416
+ session: sessionId,
417
+ prompt: description,
418
+ timestamp: startTime,
419
+ success: this._found,
420
+ error: findError,
421
+ cacheHit: response?.cacheHit || response?.cache_hit || response?.cached || false,
422
+ selector: response?.selector,
423
+ selectorUsed: !!response?.selector,
424
+ });
425
+ } catch (err) {
426
+ console.warn("Failed to track find interaction:", err.message);
427
+ }
287
428
  }
288
429
 
289
430
  return this;
@@ -544,11 +685,8 @@ class Element {
544
685
  `Element "${this.description}" not found.`,
545
686
  {
546
687
  description: this.description,
547
- screenshot: this._screenshot,
548
688
  aiResponse: this._response,
549
689
  threshold: this._threshold,
550
- cachedImageUrl: this._response?.cachedImageUrl,
551
- pixelDiffImage: this._response?.pixelDiffImage,
552
690
  },
553
691
  );
554
692
  }
@@ -562,10 +700,22 @@ class Element {
562
700
  );
563
701
  this.sdk.emitter.emit(events.log.log, formattedMessage);
564
702
 
703
+ // Prepare element metadata for interaction tracking
704
+ const elementData = {
705
+ prompt: this.description,
706
+ elementType: this._response?.elementType,
707
+ elementBounds: this._response?.elementBounds,
708
+ croppedImageUrl: this._response?.savedImagePath,
709
+ edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
710
+ cacheHit: this._response?.cacheHit,
711
+ selectorUsed: !!this._response?.selector,
712
+ selector: this._response?.selector
713
+ };
714
+
565
715
  if (action === "hover") {
566
- await this.commands.hover(this.coordinates.x, this.coordinates.y);
716
+ await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
567
717
  } else {
568
- await this.commands.click(this.coordinates.x, this.coordinates.y, action);
718
+ await this.commands.click(this.coordinates.x, this.coordinates.y, action, elementData);
569
719
  }
570
720
  }
571
721
 
@@ -579,11 +729,8 @@ class Element {
579
729
  `Element "${this.description}" not found.`,
580
730
  {
581
731
  description: this.description,
582
- screenshot: this._screenshot,
583
732
  aiResponse: this._response,
584
733
  threshold: this._threshold,
585
- cachedImageUrl: this._response?.cachedImageUrl,
586
- pixelDiffImage: this._response?.pixelDiffImage,
587
734
  },
588
735
  );
589
736
  }
@@ -593,7 +740,19 @@ class Element {
593
740
  const formattedMessage = formatter.formatAction("hover", this.description);
594
741
  this.sdk.emitter.emit(events.log.log, formattedMessage);
595
742
 
596
- await this.commands.hover(this.coordinates.x, this.coordinates.y);
743
+ // Prepare element metadata for interaction tracking
744
+ const elementData = {
745
+ prompt: this.description,
746
+ elementType: this._response?.elementType,
747
+ elementBounds: this._response?.elementBounds,
748
+ croppedImageUrl: this._response?.savedImagePath,
749
+ edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
750
+ cacheHit: this._response?.cacheHit,
751
+ selectorUsed: !!this._response?.selector,
752
+ selector: this._response?.selector
753
+ };
754
+
755
+ await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
597
756
  }
598
757
 
599
758
  /**
@@ -785,6 +944,60 @@ class Element {
785
944
  }
786
945
  }
787
946
 
947
+ /**
948
+ * Creates a chainable promise that allows method chaining on find() results
949
+ * This enables syntax like: await testdriver.find("button").click()
950
+ *
951
+ * @param {Promise<Element>} promise - The promise that resolves to an Element
952
+ * @returns {Promise<Element> & ChainableElement} A promise with chainable element methods
953
+ */
954
+ function createChainablePromise(promise) {
955
+ // Define the chainable methods that should be available
956
+ const chainableMethods = ['click', 'hover', 'doubleClick', 'rightClick', 'mouseDown', 'mouseUp'];
957
+
958
+ // Create a new promise that wraps the original
959
+ const chainablePromise = promise.then(element => element);
960
+
961
+ // Add chainable methods to the promise
962
+ for (const method of chainableMethods) {
963
+ chainablePromise[method] = function(...args) {
964
+ // Return a promise that waits for the element, then calls the method
965
+ return promise.then(element => element[method](...args));
966
+ };
967
+ }
968
+
969
+ // Add getters for element properties (these return promises)
970
+ Object.defineProperty(chainablePromise, 'x', {
971
+ get() { return promise.then(el => el.x); }
972
+ });
973
+ Object.defineProperty(chainablePromise, 'y', {
974
+ get() { return promise.then(el => el.y); }
975
+ });
976
+ Object.defineProperty(chainablePromise, 'centerX', {
977
+ get() { return promise.then(el => el.centerX); }
978
+ });
979
+ Object.defineProperty(chainablePromise, 'centerY', {
980
+ get() { return promise.then(el => el.centerY); }
981
+ });
982
+
983
+ // Add found() method
984
+ chainablePromise.found = function() {
985
+ return promise.then(el => el.found());
986
+ };
987
+
988
+ // Add getCoordinates() method
989
+ chainablePromise.getCoordinates = function() {
990
+ return promise.then(el => el.getCoordinates());
991
+ };
992
+
993
+ // Add getResponse() method
994
+ chainablePromise.getResponse = function() {
995
+ return promise.then(el => el.getResponse());
996
+ };
997
+
998
+ return chainablePromise;
999
+ }
1000
+
788
1001
  /**
789
1002
  * TestDriver SDK
790
1003
  *
@@ -838,6 +1051,17 @@ class TestDriverSDK {
838
1051
  },
839
1052
  });
840
1053
 
1054
+ // Auto-generate cache key from caller file hash if not explicitly provided
1055
+ // This allows caching to be tied to the specific test file
1056
+ if (!options.cacheKey) {
1057
+ const autoGeneratedKey = getCallerFileHash();
1058
+ if (autoGeneratedKey) {
1059
+ options.cacheKey = autoGeneratedKey;
1060
+ // Store flag to indicate this was auto-generated
1061
+ this._autoGeneratedCacheKey = true;
1062
+ }
1063
+ }
1064
+
841
1065
  // Store options for later use
842
1066
  this.options = options;
843
1067
 
@@ -847,7 +1071,7 @@ class TestDriverSDK {
847
1071
 
848
1072
  // Store newSandbox preference from options
849
1073
  this.newSandbox =
850
- options.newSandbox !== undefined ? options.newSandbox : false;
1074
+ options.newSandbox !== undefined ? options.newSandbox : true;
851
1075
 
852
1076
  // Store headless preference from options
853
1077
  this.headless = options.headless !== undefined ? options.headless : false;
@@ -862,31 +1086,48 @@ class TestDriverSDK {
862
1086
 
863
1087
  // Cache threshold configuration
864
1088
  // threshold = pixel difference allowed (0.05 = 5% difference, 95% similarity)
865
- // cache: false option disables cache completely by setting threshold to -1
866
- // Also support TD_NO_CACHE environment variable
867
- const useCache =
868
- options.cache !== false && process.env.TD_NO_CACHE !== "true";
869
-
870
- // Note: Cannot emit events here as emitter is not yet available
871
- // Logging will be done after connection
872
-
873
- if (!useCache) {
874
- // If cache is disabled, use -1 to bypass cache entirely
1089
+ // By default, cache is DISABLED (threshold = -1) to avoid unnecessary AI costs
1090
+ // To enable cache, provide a cacheKey when calling find() or findAll()
1091
+ // Also support TD_NO_CACHE environment variable and cache: false option for backwards compatibility
1092
+ const cacheDisabled =
1093
+ options.cache === false || process.env.TD_NO_CACHE === "true";
1094
+
1095
+ if (cacheDisabled) {
1096
+ // Explicit cache disabled via option or env var
875
1097
  this.cacheThresholds = {
876
1098
  find: -1,
877
1099
  findAll: -1,
878
1100
  };
879
1101
  } else {
880
- // Use configured thresholds or defaults
1102
+ // Cache disabled by default, enabled only when cacheKey is provided
1103
+ // Note: The threshold value here is the fallback when cacheKey is NOT provided
881
1104
  this.cacheThresholds = {
882
- find: options.cacheThreshold?.find ?? 0.05,
883
- findAll: options.cacheThreshold?.findAll ?? 0.05,
1105
+ find: options.cacheThreshold?.find ?? -1, // Default: cache disabled
1106
+ findAll: options.cacheThreshold?.findAll ?? -1, // Default: cache disabled
884
1107
  };
885
1108
  }
886
1109
 
887
- // Redraw threshold configuration
888
- // threshold = percentage of pixels that must change to consider screen redrawn (0.1 = 0.1%)
889
- this.redrawThreshold = options.redrawThreshold ?? 0.1;
1110
+ // Redraw configuration
1111
+ // Supports both:
1112
+ // - redraw: { enabled: true, diffThreshold: 0.1, screenRedraw: true, networkMonitor: true }
1113
+ // - redrawThreshold: 0.1 (legacy, sets diffThreshold)
1114
+ // The `redraw` option takes precedence and matches the per-command API
1115
+ if (options.redraw !== undefined) {
1116
+ // New unified API: redraw object (matches per-command options)
1117
+ this.redrawOptions = typeof options.redraw === 'object'
1118
+ ? options.redraw
1119
+ : { enabled: options.redraw }; // Support redraw: false as shorthand
1120
+ } else if (options.redrawThreshold !== undefined) {
1121
+ // Legacy API: redrawThreshold number or object
1122
+ this.redrawOptions = typeof options.redrawThreshold === 'object'
1123
+ ? options.redrawThreshold
1124
+ : { diffThreshold: options.redrawThreshold };
1125
+ } else {
1126
+ // Default: disabled
1127
+ this.redrawOptions = { enabled: false };
1128
+ }
1129
+ // Keep redrawThreshold for backwards compatibility in connect()
1130
+ this.redrawThreshold = this.redrawOptions;
890
1131
 
891
1132
  // Track connection state
892
1133
  this.connected = false;
@@ -910,6 +1151,242 @@ class TestDriverSDK {
910
1151
 
911
1152
  // Set up event listeners once (they live for the lifetime of the SDK instance)
912
1153
  this._setupLogging();
1154
+
1155
+ // Set up provision API
1156
+ this.provision = this._createProvisionAPI();
1157
+
1158
+ // Set up dashcam API lazily
1159
+ this._dashcam = null;
1160
+ }
1161
+
1162
+ /**
1163
+ * Wait for the sandbox connection to complete
1164
+ * @returns {Promise<void>}
1165
+ */
1166
+ async ready() {
1167
+ if (this.__connectionPromise) {
1168
+ await this.__connectionPromise;
1169
+ }
1170
+ if (!this.connected) {
1171
+ throw new Error('Not connected to sandbox. Call connect() first or use autoConnect option.');
1172
+ }
1173
+ }
1174
+
1175
+ /**
1176
+ * Get or create the Dashcam instance
1177
+ * @returns {Dashcam} Dashcam instance
1178
+ */
1179
+ get dashcam() {
1180
+ if (!this._dashcam) {
1181
+ const { Dashcam } = require("./src/core/index.js");
1182
+ // Don't pass apiKey - let Dashcam use its default key
1183
+ this._dashcam = new Dashcam(this);
1184
+ }
1185
+ return this._dashcam;
1186
+ }
1187
+
1188
+ /**
1189
+ * Get milliseconds elapsed since dashcam started recording
1190
+ * @returns {number|null} Milliseconds since dashcam start, or null if not recording
1191
+ */
1192
+ getDashcamElapsedTime() {
1193
+ if (this._dashcam) {
1194
+ return this._dashcam.getElapsedTime();
1195
+ }
1196
+ return null;
1197
+ }
1198
+
1199
+ /**
1200
+ * Create the provision API with methods for launching applications
1201
+ * @private
1202
+ */
1203
+ _createProvisionAPI() {
1204
+ return {
1205
+ /**
1206
+ * Launch Chrome browser
1207
+ * @param {Object} options - Chrome launch options
1208
+ * @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
1209
+ * @param {boolean} [options.maximized=true] - Start maximized
1210
+ * @param {boolean} [options.guest=false] - Use guest mode
1211
+ * @returns {Promise<void>}
1212
+ */
1213
+ chrome: async (options = {}) => {
1214
+ // Automatically wait for connection to be ready
1215
+ await this.ready();
1216
+
1217
+ const {
1218
+ url = 'http://testdriver-sandbox.vercel.app/',
1219
+ maximized = true,
1220
+ guest = false,
1221
+ } = options;
1222
+
1223
+ // If dashcam is available and recording, add web logs for this domain
1224
+ if (this._dashcam) {
1225
+ console.log('[provision.chrome] Adding web logs to dashcam...');
1226
+ try {
1227
+ const urlObj = new URL(url);
1228
+ const domain = urlObj.hostname;
1229
+ const pattern = `*${domain}*`;
1230
+ await this._dashcam.addWebLog(pattern, 'Web Logs');
1231
+ console.log(`[provision.chrome] ✅ Web logs added to dashcam (pattern: ${pattern})`);
1232
+ } catch (error) {
1233
+ console.warn('[provision.chrome] ⚠️ Failed to add web logs:', error.message);
1234
+ }
1235
+ }
1236
+
1237
+ // Automatically start dashcam if not already recording
1238
+ if (!this._dashcam || !this._dashcam.recording) {
1239
+ console.log('[provision.chrome] Starting dashcam...');
1240
+ await this.dashcam.start();
1241
+ console.log('[provision.chrome] ✅ Dashcam started');
1242
+ }
1243
+
1244
+ // Build Chrome launch command
1245
+ const chromeArgs = [];
1246
+ if (maximized) chromeArgs.push('--start-maximized');
1247
+ if (guest) chromeArgs.push('--guest');
1248
+ chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run');
1249
+
1250
+ // Add dashcam-chrome extension on Linux
1251
+ if (this.os === 'linux') {
1252
+ chromeArgs.push('--load-extension=/usr/lib/node_modules/dashcam-chrome/build');
1253
+ }
1254
+
1255
+ // Launch Chrome
1256
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1257
+
1258
+ if (this.os === 'windows') {
1259
+ const argsString = chromeArgs.map(arg => `"${arg}"`).join(', ');
1260
+ await this.exec(
1261
+ shell,
1262
+ `Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList ${argsString}, "${url}"`,
1263
+ 30000
1264
+ );
1265
+ } else {
1266
+ const argsString = chromeArgs.join(' ');
1267
+ await this.exec(
1268
+ shell,
1269
+ `chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
1270
+ 30000
1271
+ );
1272
+ }
1273
+
1274
+ // Wait for Chrome to be ready
1275
+ await this.focusApplication('Google Chrome');
1276
+
1277
+
1278
+ // Wait for URL to load
1279
+ try {
1280
+ const urlObj = new URL(url);
1281
+ const domain = urlObj.hostname;
1282
+
1283
+ console.log(`[provision.chrome] Waiting for domain "${domain}" to appear in URL bar...`);
1284
+
1285
+ for (let attempt = 0; attempt < 30; attempt++) {
1286
+ try {
1287
+ const result = await this.find(`${domain}`);
1288
+ if (result.found()) {
1289
+ console.log(`[provision.chrome] ✅ Chrome ready at ${url}`);
1290
+ break;
1291
+ }
1292
+ } catch (e) {
1293
+ // Not found yet, continue polling
1294
+ }
1295
+ await new Promise(resolve => setTimeout(resolve, 1000));
1296
+ }
1297
+
1298
+ await this.focusApplication('Google Chrome');
1299
+ } catch (e) {
1300
+ console.warn(`[provision.chrome] ⚠️ Could not parse URL "${url}":`, e.message);
1301
+ }
1302
+ },
1303
+
1304
+ /**
1305
+ * Launch VS Code
1306
+ * @param {Object} options - VS Code launch options
1307
+ * @param {string} [options.workspace] - Workspace/folder to open
1308
+ * @param {string[]} [options.extensions=[]] - Extensions to install
1309
+ * @returns {Promise<void>}
1310
+ */
1311
+ vscode: async (options = {}) => {
1312
+ this._ensureConnected();
1313
+
1314
+ const {
1315
+ workspace = null,
1316
+ extensions = [],
1317
+ } = options;
1318
+
1319
+ // Install extensions if provided
1320
+ for (const extension of extensions) {
1321
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1322
+ await this.exec(
1323
+ shell,
1324
+ `code --install-extension ${extension}`,
1325
+ 60000,
1326
+ true
1327
+ );
1328
+ }
1329
+
1330
+ // Launch VS Code
1331
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1332
+ const workspaceArg = workspace ? `"${workspace}"` : '';
1333
+
1334
+ if (this.os === 'windows') {
1335
+ await this.exec(
1336
+ shell,
1337
+ `Start-Process code -ArgumentList ${workspaceArg}`,
1338
+ 30000
1339
+ );
1340
+ } else {
1341
+ await this.exec(
1342
+ shell,
1343
+ `code ${workspaceArg} >/dev/null 2>&1 &`,
1344
+ 30000
1345
+ );
1346
+ }
1347
+
1348
+ // Wait for VS Code to be ready
1349
+ await this.focusApplication('Visual Studio Code');
1350
+ console.log('[provision.vscode] ✅ VS Code ready');
1351
+ },
1352
+
1353
+ /**
1354
+ * Launch Electron app
1355
+ * @param {Object} options - Electron launch options
1356
+ * @param {string} options.appPath - Path to Electron app (required)
1357
+ * @param {string[]} [options.args=[]] - Additional electron args
1358
+ * @returns {Promise<void>}
1359
+ */
1360
+ electron: async (options = {}) => {
1361
+ this._ensureConnected();
1362
+
1363
+ const { appPath, args = [] } = options;
1364
+
1365
+ if (!appPath) {
1366
+ throw new Error('provision.electron requires appPath option');
1367
+ }
1368
+
1369
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1370
+ const argsString = args.join(' ');
1371
+
1372
+ if (this.os === 'windows') {
1373
+ await this.exec(
1374
+ shell,
1375
+ `Start-Process electron -ArgumentList "${appPath}", ${argsString}`,
1376
+ 30000
1377
+ );
1378
+ } else {
1379
+ await this.exec(
1380
+ shell,
1381
+ `electron "${appPath}" ${argsString} >/dev/null 2>&1 &`,
1382
+ 30000
1383
+ );
1384
+ }
1385
+
1386
+ await this.focusApplication('Electron');
1387
+ console.log('[provision.electron] ✅ Electron app ready');
1388
+ },
1389
+ };
913
1390
  }
914
1391
 
915
1392
  /**
@@ -1009,12 +1486,33 @@ class TestDriverSDK {
1009
1486
  // Expose the agent's commands, parser, and commander
1010
1487
  this.commands = this.agent.commands;
1011
1488
 
1489
+ // Recreate commands with dashcam elapsed time support
1490
+ const { createCommands } = require("./agent/lib/commands.js");
1491
+ const commandsResult = createCommands(
1492
+ this.agent.emitter,
1493
+ this.agent.system,
1494
+ this.agent.sandbox,
1495
+ this.agent.config,
1496
+ this.agent.session,
1497
+ () => this.agent.sourceMapper?.currentFilePath || this.agent.thisFile,
1498
+ this.agent.cliArgs.options.redrawThreshold,
1499
+ () => this.getDashcamElapsedTime(), // Pass dashcam elapsed time function
1500
+ );
1501
+ this.commands = commandsResult.commands;
1502
+ this.agent.commands = commandsResult.commands;
1503
+ this.agent.redraw = commandsResult.redraw;
1504
+
1012
1505
  // Dynamically create command methods based on available commands
1013
1506
  this._setupCommandMethods();
1014
1507
 
1015
1508
  this.connected = true;
1509
+
1510
+ // Expose whether we reconnected to an existing sandbox or created a new one
1511
+ this.isReconnected = this.agent.isReconnected || false;
1512
+
1016
1513
  this.analytics.track("sdk.connect", {
1017
1514
  sandboxId: this.instance?.instanceId,
1515
+ isReconnected: this.isReconnected,
1018
1516
  });
1019
1517
 
1020
1518
  return this.instance;
@@ -1054,16 +1552,24 @@ class TestDriverSDK {
1054
1552
  * Automatically locates the element and returns it
1055
1553
  *
1056
1554
  * @param {string} description - Description of the element to find
1057
- * @param {number} [cacheThreshold] - Cache threshold for this specific find (overrides global setting)
1058
- * @returns {Promise<Element>} Element instance that has been located
1555
+ * @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
1556
+ * @returns {Promise<Element> & ChainableElement} Element instance that has been located, with chainable methods
1059
1557
  *
1060
1558
  * @example
1061
- * // Find and click immediately
1559
+ * // Find and click immediately (chainable)
1560
+ * await client.find('the sign in button').click();
1561
+ *
1562
+ * @example
1563
+ * // Find and click (traditional)
1062
1564
  * const element = await client.find('the sign in button');
1063
1565
  * await element.click();
1064
1566
  *
1065
1567
  * @example
1066
- * // Find with custom cache threshold
1568
+ * // Find with cache key to enable caching
1569
+ * const element = await client.find('login button', { cacheKey: 'my-test-run' });
1570
+ *
1571
+ * @example
1572
+ * // Find with custom cache threshold (legacy)
1067
1573
  * const element = await client.find('login button', 0.01);
1068
1574
  *
1069
1575
  * @example
@@ -1077,10 +1583,14 @@ class TestDriverSDK {
1077
1583
  * }
1078
1584
  * await element.click();
1079
1585
  */
1080
- async find(description, cacheThreshold) {
1586
+ find(description, options) {
1081
1587
  this._ensureConnected();
1082
1588
  const element = new Element(description, this, this.system, this.commands);
1083
- return await element.find(null, cacheThreshold);
1589
+ const findPromise = element.find(null, options);
1590
+
1591
+ // Create a chainable promise that allows direct method chaining
1592
+ // e.g., await testdriver.find("button").click()
1593
+ return createChainablePromise(findPromise);
1084
1594
  }
1085
1595
 
1086
1596
  /**
@@ -1088,7 +1598,7 @@ class TestDriverSDK {
1088
1598
  * Automatically locates all matching elements and returns them as an array
1089
1599
  *
1090
1600
  * @param {string} description - Description of the elements to find
1091
- * @param {number} [cacheThreshold] - Cache threshold for this specific findAll (overrides global setting)
1601
+ * @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
1092
1602
  * @returns {Promise<Element[]>} Array of Element instances that have been located
1093
1603
  *
1094
1604
  * @example
@@ -1099,13 +1609,13 @@ class TestDriverSDK {
1099
1609
  * }
1100
1610
  *
1101
1611
  * @example
1102
- * // Find all list items with custom cache threshold
1103
- * const items = await client.findAll('list item', 0.01);
1612
+ * // Find all list items with cache key to enable caching
1613
+ * const items = await client.findAll('list item', { cacheKey: 'my-test-run' });
1104
1614
  * for (const item of items) {
1105
1615
  * console.log(`Found item at (${item.x}, ${item.y})`);
1106
1616
  * }
1107
1617
  */
1108
- async findAll(description, cacheThreshold) {
1618
+ async findAll(description, options) {
1109
1619
  this._ensureConnected();
1110
1620
 
1111
1621
  const startTime = Date.now();
@@ -1118,15 +1628,59 @@ class TestDriverSDK {
1118
1628
  try {
1119
1629
  const screenshot = await this.system.captureScreenBase64();
1120
1630
 
1121
- // Use per-command threshold if provided, otherwise fall back to global threshold
1122
- const threshold = cacheThreshold ?? this.cacheThresholds?.findAll ?? 0.05;
1631
+ // Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
1632
+ let cacheKey = null;
1633
+ let cacheThreshold = null;
1634
+
1635
+ if (typeof options === 'number') {
1636
+ // Legacy: options is just a number threshold
1637
+ cacheThreshold = options;
1638
+ } else if (typeof options === 'object' && options !== null) {
1639
+ // New: options is an object with cacheKey and/or cacheThreshold
1640
+ cacheKey = options.cacheKey || null;
1641
+ cacheThreshold = options.cacheThreshold ?? null;
1642
+ }
1643
+
1644
+ // Use default cacheKey from SDK constructor if not provided in findAll() options
1645
+ if (!cacheKey && this.options?.cacheKey) {
1646
+ cacheKey = this.options.cacheKey;
1647
+ }
1648
+
1649
+ // Determine threshold:
1650
+ // - If cacheKey is provided, enable cache (threshold = 0.05 or custom)
1651
+ // - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
1652
+ let threshold;
1653
+ if (cacheKey) {
1654
+ // cacheKey provided - enable cache with threshold
1655
+ threshold = cacheThreshold ?? 0.05;
1656
+ } else if (cacheThreshold !== null) {
1657
+ // Explicit threshold provided without cacheKey
1658
+ threshold = cacheThreshold;
1659
+ } else {
1660
+ // No cacheKey, no explicit threshold - use global default (which is -1 now)
1661
+ threshold = this.cacheThresholds?.findAll ?? -1;
1662
+ }
1663
+
1664
+ // Debug log threshold
1665
+ const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
1666
+ if (debugMode) {
1667
+ const autoGenMsg = (this._autoGeneratedCacheKey && cacheKey === this.options.cacheKey)
1668
+ ? ' (auto-generated from file hash)'
1669
+ : '';
1670
+ this.emitter.emit(
1671
+ events.log.debug,
1672
+ `🔍 findAll() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
1673
+ );
1674
+ }
1123
1675
 
1124
1676
  const response = await this.apiClient.req(
1125
1677
  "/api/v7.0.0/testdriver-agent/testdriver-find-all",
1126
1678
  {
1679
+ session: this.getSessionId(),
1127
1680
  element: description,
1128
1681
  image: screenshot,
1129
1682
  threshold: threshold,
1683
+ cacheKey: cacheKey,
1130
1684
  os: this.os,
1131
1685
  resolution: this.resolution,
1132
1686
  },
@@ -1173,6 +1727,27 @@ class TestDriverSDK {
1173
1727
  return element;
1174
1728
  });
1175
1729
 
1730
+ // Track successful findAll interaction
1731
+ const sessionId = this.getSessionId();
1732
+ if (sessionId && this.sandbox?.send) {
1733
+ try {
1734
+ await this.sandbox.send({
1735
+ type: "trackInteraction",
1736
+ interactionType: "findAll",
1737
+ session: sessionId,
1738
+ prompt: description,
1739
+ timestamp: startTime,
1740
+ success: true,
1741
+ input: { count: elements.length },
1742
+ cacheHit: response.cached || false,
1743
+ selector: response.selector,
1744
+ selectorUsed: !!response.selector,
1745
+ });
1746
+ } catch (err) {
1747
+ console.warn("Failed to track findAll interaction:", err.message);
1748
+ }
1749
+ }
1750
+
1176
1751
  // Log debug information when elements are found
1177
1752
  if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
1178
1753
  const { events } = require("./agent/events.js");
@@ -1189,10 +1764,51 @@ class TestDriverSDK {
1189
1764
 
1190
1765
  return elements;
1191
1766
  } else {
1767
+ // No elements found - track interaction
1768
+ const sessionId = this.getSessionId();
1769
+ if (sessionId && this.sandbox?.send) {
1770
+ try {
1771
+ await this.sandbox.send({
1772
+ type: "trackInteraction",
1773
+ interactionType: "findAll",
1774
+ session: sessionId,
1775
+ prompt: description,
1776
+ timestamp: startTime,
1777
+ success: false,
1778
+ error: "No elements found",
1779
+ input: { count: 0 },
1780
+ cacheHit: response?.cached || false,
1781
+ selector: response?.selector,
1782
+ selectorUsed: !!response?.selector,
1783
+ });
1784
+ } catch (err) {
1785
+ console.warn("Failed to track findAll interaction:", err.message);
1786
+ }
1787
+ }
1788
+
1192
1789
  // No elements found - return empty array
1193
1790
  return [];
1194
1791
  }
1195
1792
  } catch (error) {
1793
+ // Track findAll error interaction
1794
+ const sessionId = this.getSessionId();
1795
+ if (sessionId && this.sandbox?.send) {
1796
+ try {
1797
+ await this.sandbox.send({
1798
+ type: "trackInteraction",
1799
+ interactionType: "findAll",
1800
+ session: sessionId,
1801
+ prompt: description,
1802
+ timestamp: startTime,
1803
+ success: false,
1804
+ error: error.message,
1805
+ input: { count: 0 },
1806
+ });
1807
+ } catch (err) {
1808
+ console.warn("Failed to track findAll interaction:", err.message);
1809
+ }
1810
+ }
1811
+
1196
1812
  const { events } = require("./agent/events.js");
1197
1813
  this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
1198
1814
  return [];
@@ -1245,17 +1861,18 @@ class TestDriverSDK {
1245
1861
  */
1246
1862
  _setupCommandMethods() {
1247
1863
  // Mapping from command names to SDK method names with type definitions
1864
+ // Each command supports both positional args (legacy) and object args (new)
1248
1865
  const commandMapping = {
1249
1866
  "hover-text": {
1250
1867
  name: "hoverText",
1251
1868
  /**
1252
1869
  * Hover over text on screen
1253
1870
  * @deprecated Use find() and element.click() instead
1254
- * @param {string} text - Text to find and hover over
1255
- * @param {string | null} [description] - Optional description of the element
1256
- * @param {ClickAction} [action='click'] - Action to perform
1257
- * @param {TextMatchMethod} [method='turbo'] - Text matching method
1258
- * @param {number} [timeout=5000] - Timeout in milliseconds
1871
+ * @param {Object|string} options - Options object or text (legacy positional)
1872
+ * @param {string} options.text - Text to find and hover over
1873
+ * @param {string|null} [options.description] - Optional description of the element
1874
+ * @param {ClickAction} [options.action='click'] - Action to perform
1875
+ * @param {number} [options.timeout=5000] - Timeout in milliseconds
1259
1876
  * @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
1260
1877
  */
1261
1878
  doc: "Hover over text on screen (deprecated - use find() instead)",
@@ -1265,8 +1882,9 @@ class TestDriverSDK {
1265
1882
  /**
1266
1883
  * Hover over an image on screen
1267
1884
  * @deprecated Use find() and element.click() instead
1268
- * @param {string} description - Description of the image to find
1269
- * @param {ClickAction} [action='click'] - Action to perform
1885
+ * @param {Object|string} options - Options object or description (legacy positional)
1886
+ * @param {string} options.description - Description of the image to find
1887
+ * @param {ClickAction} [options.action='click'] - Action to perform
1270
1888
  * @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
1271
1889
  */
1272
1890
  doc: "Hover over an image on screen (deprecated - use find() instead)",
@@ -1275,9 +1893,10 @@ class TestDriverSDK {
1275
1893
  name: "matchImage",
1276
1894
  /**
1277
1895
  * Match and interact with an image template
1278
- * @param {string} imagePath - Path to the image template
1279
- * @param {ClickAction} [action='click'] - Action to perform
1280
- * @param {boolean} [invert=false] - Invert the match
1896
+ * @param {Object|string} options - Options object or path (legacy positional)
1897
+ * @param {string} options.path - Path to the image template
1898
+ * @param {ClickAction} [options.action='click'] - Action to perform
1899
+ * @param {boolean} [options.invert=false] - Invert the match
1281
1900
  * @returns {Promise<boolean>}
1282
1901
  */
1283
1902
  doc: "Match and interact with an image template",
@@ -1286,17 +1905,20 @@ class TestDriverSDK {
1286
1905
  name: "type",
1287
1906
  /**
1288
1907
  * Type text
1289
- * @param {string | number} text - Text to type
1290
- * @param {number} [delay=250] - Delay between keystrokes in milliseconds
1908
+ * @param {string|number} text - Text to type
1909
+ * @param {Object} [options] - Additional options
1910
+ * @param {number} [options.delay=250] - Delay between keystrokes in milliseconds
1911
+ * @param {boolean} [options.secret=false] - If true, text is treated as sensitive (not logged or stored)
1291
1912
  * @returns {Promise<void>}
1292
1913
  */
1293
- doc: "Type text",
1914
+ doc: "Type text (use { secret: true } for passwords)",
1294
1915
  },
1295
1916
  "press-keys": {
1296
1917
  name: "pressKeys",
1297
1918
  /**
1298
1919
  * Press keyboard keys
1299
1920
  * @param {KeyboardKey[]} keys - Array of keys to press
1921
+ * @param {Object} [options] - Additional options (reserved for future use)
1300
1922
  * @returns {Promise<void>}
1301
1923
  */
1302
1924
  doc: "Press keyboard keys",
@@ -1305,9 +1927,10 @@ class TestDriverSDK {
1305
1927
  name: "click",
1306
1928
  /**
1307
1929
  * Click at coordinates
1308
- * @param {number} x - X coordinate
1309
- * @param {number} y - Y coordinate
1310
- * @param {ClickAction} [action='click'] - Type of click action
1930
+ * @param {Object|number} options - Options object or x coordinate (legacy positional)
1931
+ * @param {number} options.x - X coordinate
1932
+ * @param {number} options.y - Y coordinate
1933
+ * @param {ClickAction} [options.action='click'] - Type of click action
1311
1934
  * @returns {Promise<void>}
1312
1935
  */
1313
1936
  doc: "Click at coordinates",
@@ -1316,8 +1939,9 @@ class TestDriverSDK {
1316
1939
  name: "hover",
1317
1940
  /**
1318
1941
  * Hover at coordinates
1319
- * @param {number} x - X coordinate
1320
- * @param {number} y - Y coordinate
1942
+ * @param {Object|number} options - Options object or x coordinate (legacy positional)
1943
+ * @param {number} options.x - X coordinate
1944
+ * @param {number} options.y - Y coordinate
1321
1945
  * @returns {Promise<void>}
1322
1946
  */
1323
1947
  doc: "Hover at coordinates",
@@ -1327,7 +1951,8 @@ class TestDriverSDK {
1327
1951
  /**
1328
1952
  * Scroll the page
1329
1953
  * @param {ScrollDirection} [direction='down'] - Direction to scroll
1330
- * @param {number} [amount=300] - Amount to scroll in pixels
1954
+ * @param {Object} [options] - Additional options
1955
+ * @param {number} [options.amount=300] - Amount to scroll in pixels
1331
1956
  * @returns {Promise<void>}
1332
1957
  */
1333
1958
  doc: "Scroll the page",
@@ -1338,6 +1963,7 @@ class TestDriverSDK {
1338
1963
  * Wait for specified time
1339
1964
  * @deprecated Consider using element polling with find() instead of arbitrary waits
1340
1965
  * @param {number} [timeout=3000] - Time to wait in milliseconds
1966
+ * @param {Object} [options] - Additional options (reserved for future use)
1341
1967
  * @returns {Promise<void>}
1342
1968
  */
1343
1969
  doc: "Wait for specified time (deprecated - consider element polling instead)",
@@ -1347,10 +1973,9 @@ class TestDriverSDK {
1347
1973
  /**
1348
1974
  * Wait for text to appear on screen
1349
1975
  * @deprecated Use find() in a polling loop instead
1350
- * @param {string} text - Text to wait for
1351
- * @param {number} [timeout=5000] - Timeout in milliseconds
1352
- * @param {TextMatchMethod} [method='turbo'] - Text matching method
1353
- * @param {boolean} [invert=false] - Invert the match (wait for text to disappear)
1976
+ * @param {Object|string} options - Options object or text (legacy positional)
1977
+ * @param {string} options.text - Text to wait for
1978
+ * @param {number} [options.timeout=5000] - Timeout in milliseconds
1354
1979
  * @returns {Promise<void>}
1355
1980
  */
1356
1981
  doc: "Wait for text to appear on screen (deprecated - use find() in a loop instead)",
@@ -1360,9 +1985,9 @@ class TestDriverSDK {
1360
1985
  /**
1361
1986
  * Wait for image to appear on screen
1362
1987
  * @deprecated Use find() in a polling loop instead
1363
- * @param {string} description - Description of the image
1364
- * @param {number} [timeout=10000] - Timeout in milliseconds
1365
- * @param {boolean} [invert=false] - Invert the match (wait for image to disappear)
1988
+ * @param {Object|string} options - Options object or description (legacy positional)
1989
+ * @param {string} options.description - Description of the image
1990
+ * @param {number} [options.timeout=10000] - Timeout in milliseconds
1366
1991
  * @returns {Promise<void>}
1367
1992
  */
1368
1993
  doc: "Wait for image to appear on screen (deprecated - use find() in a loop instead)",
@@ -1371,12 +1996,11 @@ class TestDriverSDK {
1371
1996
  name: "scrollUntilText",
1372
1997
  /**
1373
1998
  * Scroll until text is found
1374
- * @param {string} text - Text to find
1375
- * @param {ScrollDirection} [direction='down'] - Scroll direction
1376
- * @param {number} [maxDistance=10000] - Maximum distance to scroll in pixels
1377
- * @param {TextMatchMethod} [textMatchMethod='turbo'] - Text matching method
1378
- * @param {ScrollMethod} [method='keyboard'] - Scroll method
1379
- * @param {boolean} [invert=false] - Invert the match
1999
+ * @param {Object|string} options - Options object or text (legacy positional)
2000
+ * @param {string} options.text - Text to find
2001
+ * @param {ScrollDirection} [options.direction='down'] - Scroll direction
2002
+ * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
2003
+ * @param {boolean} [options.invert=false] - Invert the match
1380
2004
  * @returns {Promise<void>}
1381
2005
  */
1382
2006
  doc: "Scroll until text is found",
@@ -1385,12 +2009,13 @@ class TestDriverSDK {
1385
2009
  name: "scrollUntilImage",
1386
2010
  /**
1387
2011
  * Scroll until image is found
1388
- * @param {string} description - Description of the image (or use path parameter)
1389
- * @param {ScrollDirection} [direction='down'] - Scroll direction
1390
- * @param {number} [maxDistance=10000] - Maximum distance to scroll in pixels
1391
- * @param {ScrollMethod} [method='keyboard'] - Scroll method
1392
- * @param {string | null} [path=null] - Path to image template
1393
- * @param {boolean} [invert=false] - Invert the match
2012
+ * @param {Object|string} [options] - Options object or description (legacy positional)
2013
+ * @param {string} [options.description] - Description of the image
2014
+ * @param {ScrollDirection} [options.direction='down'] - Scroll direction
2015
+ * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
2016
+ * @param {string} [options.method='mouse'] - Scroll method
2017
+ * @param {string} [options.path] - Path to image template
2018
+ * @param {boolean} [options.invert=false] - Invert the match
1394
2019
  * @returns {Promise<void>}
1395
2020
  */
1396
2021
  doc: "Scroll until image is found",
@@ -1400,6 +2025,7 @@ class TestDriverSDK {
1400
2025
  /**
1401
2026
  * Focus an application by name
1402
2027
  * @param {string} name - Application name
2028
+ * @param {Object} [options] - Additional options (reserved for future use)
1403
2029
  * @returns {Promise<string>}
1404
2030
  */
1405
2031
  doc: "Focus an application by name",
@@ -1408,7 +2034,8 @@ class TestDriverSDK {
1408
2034
  name: "remember",
1409
2035
  /**
1410
2036
  * Extract and remember information from the screen using AI
1411
- * @param {string} description - What to remember
2037
+ * @param {Object|string} options - Options object or description (legacy positional)
2038
+ * @param {string} options.description - What to remember
1412
2039
  * @returns {Promise<string>}
1413
2040
  */
1414
2041
  doc: "Extract and remember information from the screen",
@@ -1418,6 +2045,7 @@ class TestDriverSDK {
1418
2045
  /**
1419
2046
  * Make an AI-powered assertion
1420
2047
  * @param {string} assertion - Assertion to check
2048
+ * @param {Object} [options] - Additional options (reserved for future use)
1421
2049
  * @returns {Promise<boolean>}
1422
2050
  */
1423
2051
  doc: "Make an AI-powered assertion",
@@ -1426,10 +2054,11 @@ class TestDriverSDK {
1426
2054
  name: "exec",
1427
2055
  /**
1428
2056
  * Execute code in the sandbox
1429
- * @param {ExecLanguage} language - Language ('js' or 'pwsh')
1430
- * @param {string} code - Code to execute
1431
- * @param {number} timeout - Timeout in milliseconds
1432
- * @param {boolean} [silent=false] - Suppress output
2057
+ * @param {Object|ExecLanguage} options - Options object or language (legacy positional)
2058
+ * @param {ExecLanguage} [options.language='pwsh'] - Language ('js', 'pwsh', or 'sh')
2059
+ * @param {string} options.code - Code to execute
2060
+ * @param {number} [options.timeout] - Timeout in milliseconds
2061
+ * @param {boolean} [options.silent=false] - Suppress output
1433
2062
  * @returns {Promise<string>}
1434
2063
  */
1435
2064
  doc: "Execute code in the sandbox",
@@ -1580,6 +2209,9 @@ class TestDriverSDK {
1580
2209
  ? `[${this.testContext}] ${message}`
1581
2210
  : message;
1582
2211
  console.log(prefixedMessage);
2212
+
2213
+ // Also forward to sandbox for dashcam
2214
+ this._forwardLogToSandbox(prefixedMessage);
1583
2215
  }
1584
2216
  });
1585
2217
 
@@ -1753,8 +2385,37 @@ class TestDriverSDK {
1753
2385
  // Auto-detect sandbox ID from the active sandbox if not provided
1754
2386
  const sandboxId = options.sandboxId || this.agent?.sandbox?.id || null;
1755
2387
 
1756
- // Get session ID from the agent's session instance
1757
- const sessionId = this.agent?.sessionInstance?.get() || null;
2388
+ // Get or create session ID using the agent's newSession method
2389
+ let sessionId = this.agent?.sessionInstance?.get() || null;
2390
+
2391
+ // If no session exists, create one using the agent's method
2392
+ if (!sessionId && this.agent?.newSession) {
2393
+ try {
2394
+ await this.agent.newSession();
2395
+ sessionId = this.agent.sessionInstance.get();
2396
+
2397
+ // Save session ID to file for reuse across test runs
2398
+ if (sessionId) {
2399
+ const sessionFile = path.join(os.homedir(), '.testdriverai-session');
2400
+ fs.writeFileSync(sessionFile, sessionId, { encoding: 'utf-8' });
2401
+ }
2402
+ } catch (error) {
2403
+ // Log but don't fail - tests can run without a session
2404
+ console.warn('Failed to create session:', error.message);
2405
+ }
2406
+ }
2407
+
2408
+ // If still no session, try reading from file (for reporter/separate processes)
2409
+ if (!sessionId) {
2410
+ try {
2411
+ const sessionFile = path.join(os.homedir(), '.testdriverai-session');
2412
+ if (fs.existsSync(sessionFile)) {
2413
+ sessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
2414
+ }
2415
+ } catch (error) {
2416
+ // Ignore file read errors
2417
+ }
2418
+ }
1758
2419
 
1759
2420
  const data = {
1760
2421
  runId: options.runId,