testdriverai 6.2.2 → 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 (300) hide show
  1. package/.github/workflows/acceptance-linux.yml +75 -0
  2. package/.github/workflows/acceptance-sdk-tests.yml +133 -0
  3. package/.vscode/settings.json +5 -1
  4. package/AGENTS.md +550 -0
  5. package/CODEOWNERS +0 -1
  6. package/README.md +126 -0
  7. package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
  8. package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
  9. package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
  10. package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
  11. package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
  12. package/_testdriver/lifecycle/prerun.yaml +15 -0
  13. package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
  14. package/agent/index.js +300 -85
  15. package/agent/interface.js +15 -0
  16. package/agent/lib/cache.js +142 -0
  17. package/agent/lib/commander.js +1 -39
  18. package/agent/lib/commands.js +910 -296
  19. package/agent/lib/redraw.js +129 -41
  20. package/agent/lib/sandbox.js +29 -6
  21. package/agent/lib/sdk.js +22 -0
  22. package/agent/lib/system.js +0 -3
  23. package/agent/lib/validation.js +1 -7
  24. package/debug-locate-response.js +82 -0
  25. package/debugger/index.html +15 -4
  26. package/docs/ARCHITECTURE.md +424 -0
  27. package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
  28. package/docs/MIGRATION.md +425 -0
  29. package/docs/PRESETS.md +210 -0
  30. package/docs/QUICK_START_TEST_RECORDING.md +215 -0
  31. package/docs/SDK_AWESOME_LOGS.md +468 -0
  32. package/docs/TEST_RECORDING.md +388 -0
  33. package/docs/docs.json +286 -152
  34. package/docs/guide/best-practices-polling.mdx +154 -0
  35. package/docs/sdk-browser-rendering.md +167 -0
  36. package/docs/v6/getting-started/self-hosting.mdx +407 -0
  37. package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
  38. package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
  39. package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
  40. package/docs/v6/overview/comparison.mdx +101 -0
  41. package/docs/v7/README.md +135 -0
  42. package/docs/v7/api/ai.mdx +205 -0
  43. package/docs/v7/api/assert.mdx +285 -0
  44. package/docs/v7/api/assertions.mdx +403 -0
  45. package/docs/v7/api/click.mdx +287 -0
  46. package/docs/v7/api/client.mdx +322 -0
  47. package/docs/v7/api/dashcam.mdx +497 -0
  48. package/docs/v7/api/doubleClick.mdx +102 -0
  49. package/docs/v7/api/elements.mdx +479 -0
  50. package/docs/v7/api/exec.mdx +346 -0
  51. package/docs/v7/api/find.mdx +316 -0
  52. package/docs/v7/api/focusApplication.mdx +294 -0
  53. package/docs/v7/api/hover.mdx +279 -0
  54. package/docs/v7/api/mouseDown.mdx +161 -0
  55. package/docs/v7/api/mouseUp.mdx +164 -0
  56. package/docs/v7/api/pressKeys.mdx +349 -0
  57. package/docs/v7/api/rightClick.mdx +123 -0
  58. package/docs/v7/api/sandbox.mdx +404 -0
  59. package/docs/v7/api/scroll.mdx +300 -0
  60. package/docs/v7/api/type.mdx +314 -0
  61. package/docs/v7/commands/assert.mdx +45 -0
  62. package/docs/v7/commands/exec.mdx +282 -0
  63. package/docs/v7/commands/focus-application.mdx +44 -0
  64. package/docs/v7/commands/hover-image.mdx +69 -0
  65. package/docs/v7/commands/hover-text.mdx +47 -0
  66. package/docs/v7/commands/if.mdx +53 -0
  67. package/docs/v7/commands/match-image.mdx +67 -0
  68. package/docs/v7/commands/press-keys.mdx +87 -0
  69. package/docs/v7/commands/remember.mdx +49 -0
  70. package/docs/v7/commands/run.mdx +44 -0
  71. package/docs/v7/commands/scroll-until-image.mdx +66 -0
  72. package/docs/v7/commands/scroll-until-text.mdx +60 -0
  73. package/docs/v7/commands/scroll.mdx +69 -0
  74. package/docs/v7/commands/type.mdx +45 -0
  75. package/docs/v7/commands/wait-for-image.mdx +54 -0
  76. package/docs/v7/commands/wait-for-text.mdx +48 -0
  77. package/docs/v7/commands/wait.mdx +45 -0
  78. package/docs/v7/getting-started/configuration.mdx +380 -0
  79. package/docs/v7/getting-started/quickstart.mdx +332 -0
  80. package/docs/v7/guides/best-practices.mdx +486 -0
  81. package/docs/v7/guides/caching-ai.mdx +215 -0
  82. package/docs/v7/guides/caching-selectors.mdx +292 -0
  83. package/docs/v7/guides/caching.mdx +366 -0
  84. package/docs/v7/guides/ci-cd/azure.mdx +587 -0
  85. package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
  86. package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
  87. package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
  88. package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
  89. package/docs/v7/guides/ci-cd/travis.mdx +438 -0
  90. package/docs/v7/guides/debugging.mdx +349 -0
  91. package/docs/v7/guides/faq.mdx +393 -0
  92. package/docs/v7/guides/migration.mdx +562 -0
  93. package/docs/v7/guides/performance.mdx +517 -0
  94. package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
  95. package/docs/v7/guides/troubleshooting.mdx +526 -0
  96. package/docs/v7/guides/vitest-plugin.mdx +477 -0
  97. package/docs/v7/guides/vitest.mdx +535 -0
  98. package/docs/v7/platforms/linux.mdx +308 -0
  99. package/docs/v7/platforms/macos.mdx +433 -0
  100. package/docs/v7/platforms/windows.mdx +430 -0
  101. package/docs/v7/playwright.mdx +342 -0
  102. package/docs/v7/presets/chrome-extension.mdx +223 -0
  103. package/docs/v7/presets/chrome.mdx +287 -0
  104. package/docs/v7/presets/electron.mdx +435 -0
  105. package/docs/v7/presets/vscode.mdx +398 -0
  106. package/docs/v7/presets/webapp.mdx +396 -0
  107. package/docs/v7/progressive-apis/CORE.md +459 -0
  108. package/docs/v7/progressive-apis/HOOKS.md +360 -0
  109. package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
  110. package/docs/v7/progressive-apis/PROVISION.md +266 -0
  111. package/eslint.config.js +19 -1
  112. package/interfaces/cli/lib/base.js +10 -4
  113. package/interfaces/logger.js +2 -1
  114. package/interfaces/shared-test-state.mjs +69 -0
  115. package/interfaces/vitest-plugin.mjs +830 -0
  116. package/package.json +29 -5
  117. package/schema.json +8 -29
  118. package/scripts/view-test-results.mjs +96 -0
  119. package/sdk-log-formatter.js +714 -0
  120. package/sdk.d.ts +1028 -0
  121. package/sdk.js +2567 -0
  122. package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
  123. package/setup/aws/cloudformation.yaml +9 -2
  124. package/src/core/Dashcam.js +469 -0
  125. package/src/core/index.d.ts +150 -0
  126. package/src/core/index.js +12 -0
  127. package/src/presets/index.mjs +331 -0
  128. package/src/vitest/extended.mjs +108 -0
  129. package/src/vitest/hooks.d.ts +119 -0
  130. package/src/vitest/hooks.mjs +298 -0
  131. package/src/vitest/index.mjs +64 -0
  132. package/src/vitest/lifecycle.mjs +277 -0
  133. package/src/vitest/utils.mjs +150 -0
  134. package/test/dashcam.test.js +137 -0
  135. package/test/mcp-example-test.yaml +27 -0
  136. package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
  137. package/testdriver/acceptance-sdk/README.md +128 -0
  138. package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
  139. package/testdriver/acceptance-sdk/assert.test.mjs +26 -0
  140. package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
  141. package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
  142. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +58 -0
  143. package/testdriver/acceptance-sdk/element-not-found.test.mjs +25 -0
  144. package/testdriver/acceptance-sdk/exec-js.test.mjs +43 -0
  145. package/testdriver/acceptance-sdk/exec-output.test.mjs +59 -0
  146. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +57 -0
  147. package/testdriver/acceptance-sdk/focus-window.test.mjs +36 -0
  148. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +26 -0
  149. package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
  150. package/testdriver/acceptance-sdk/hover-image.test.mjs +34 -0
  151. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +38 -0
  152. package/testdriver/acceptance-sdk/hover-text.test.mjs +27 -0
  153. package/testdriver/acceptance-sdk/match-image.test.mjs +36 -0
  154. package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
  155. package/testdriver/acceptance-sdk/press-keys.test.mjs +50 -0
  156. package/testdriver/acceptance-sdk/prompt.test.mjs +33 -0
  157. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +38 -0
  158. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +39 -0
  159. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +28 -0
  160. package/testdriver/acceptance-sdk/scroll.test.mjs +41 -0
  161. package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
  162. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +420 -0
  163. package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
  164. package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
  165. package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
  166. package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
  167. package/testdriver/acceptance-sdk/type.test.mjs +45 -0
  168. package/verify-element-api.js +89 -0
  169. package/verify-types.js +0 -0
  170. package/vitest.config.example.js +19 -0
  171. package/vitest.config.mjs +66 -0
  172. package/vitest.config.mjs.bak +44 -0
  173. package/.github/workflows/acceptance-v6.yml +0 -169
  174. package/.vscode/mcp.json +0 -9
  175. package/docs/overview/comparison.mdx +0 -82
  176. package/testdriver/lifecycle/prerun.yaml +0 -17
  177. /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
  178. /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
  179. /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
  180. /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
  181. /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
  182. /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
  183. /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
  184. /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
  185. /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
  186. /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
  187. /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
  188. /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
  189. /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
  190. /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
  191. /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
  192. /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
  193. /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
  194. /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
  195. /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
  196. /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
  197. /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
  198. /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
  199. /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
  200. /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
  201. /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
  202. /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
  203. /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
  204. /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
  205. /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
  206. /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
  207. /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
  208. /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
  209. /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
  210. /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
  211. /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
  212. /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
  213. /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
  214. /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
  215. /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
  216. /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
  217. /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
  218. /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
  219. /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
  220. /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
  221. /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
  222. /package/docs/{account → v6/account}/dashboard.mdx +0 -0
  223. /package/docs/{account → v6/account}/enterprise.mdx +0 -0
  224. /package/docs/{account → v6/account}/pricing.mdx +0 -0
  225. /package/docs/{account → v6/account}/projects.mdx +0 -0
  226. /package/docs/{account → v6/account}/team.mdx +0 -0
  227. /package/docs/{action → v6/action}/ami.mdx +0 -0
  228. /package/docs/{action → v6/action}/performance.mdx +0 -0
  229. /package/docs/{action → v6/action}/secrets.mdx +0 -0
  230. /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
  231. /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
  232. /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
  233. /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
  234. /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
  235. /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
  236. /package/docs/{cli → v6/cli}/overview.mdx +0 -0
  237. /package/docs/{commands → v6/commands}/assert.mdx +0 -0
  238. /package/docs/{commands → v6/commands}/exec.mdx +0 -0
  239. /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
  240. /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
  241. /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
  242. /package/docs/{commands → v6/commands}/if.mdx +0 -0
  243. /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
  244. /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
  245. /package/docs/{commands → v6/commands}/remember.mdx +0 -0
  246. /package/docs/{commands → v6/commands}/run.mdx +0 -0
  247. /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
  248. /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
  249. /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
  250. /package/docs/{commands → v6/commands}/type.mdx +0 -0
  251. /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
  252. /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
  253. /package/docs/{commands → v6/commands}/wait.mdx +0 -0
  254. /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
  255. /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
  256. /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
  257. /package/docs/{features → v6/features}/generation.mdx +0 -0
  258. /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
  259. /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
  260. /package/docs/{features → v6/features}/selectorless.mdx +0 -0
  261. /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
  262. /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
  263. /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
  264. /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
  265. /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
  266. /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
  267. /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
  268. /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
  269. /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
  270. /package/docs/{guide → v6/guide}/code.mdx +0 -0
  271. /package/docs/{guide → v6/guide}/locating.mdx +0 -0
  272. /package/docs/{guide → v6/guide}/protips.mdx +0 -0
  273. /package/docs/{guide → v6/guide}/variables.mdx +0 -0
  274. /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
  275. /package/docs/{importing → v6/importing}/csv.mdx +0 -0
  276. /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
  277. /package/docs/{importing → v6/importing}/jira.mdx +0 -0
  278. /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
  279. /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
  280. /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
  281. /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
  282. /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
  283. /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
  284. /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
  285. /package/docs/{overview → v6/overview}/faq.mdx +0 -0
  286. /package/docs/{overview → v6/overview}/performance.mdx +0 -0
  287. /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
  288. /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
  289. /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
  290. /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
  291. /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
  292. /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
  293. /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
  294. /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
  295. /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
  296. /package/docs/{security → v6/security}/action.mdx +0 -0
  297. /package/docs/{security → v6/security}/agent.mdx +0 -0
  298. /package/docs/{security → v6/security}/platform.mdx +0 -0
  299. /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
  300. /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
package/sdk.js ADDED
@@ -0,0 +1,2567 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const crypto = require("crypto");
7
+ const { formatter } = require("./sdk-log-formatter");
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
+
69
+ /**
70
+ * Custom error class for element operation failures
71
+ * Includes debugging information like screenshots and AI responses
72
+ */
73
+ class ElementNotFoundError extends Error {
74
+ constructor(message, debugInfo = {}) {
75
+ super(message);
76
+ this.name = "ElementNotFoundError";
77
+ // Sanitize aiResponse to remove base64 images before storing
78
+ this.aiResponse = this._sanitizeAiResponse(debugInfo.aiResponse);
79
+ this.description = debugInfo.description;
80
+ this.timestamp = new Date().toISOString();
81
+ this.screenshotPath = null;
82
+
83
+ // Capture stack trace but skip internal frames
84
+ if (Error.captureStackTrace) {
85
+ Error.captureStackTrace(this, ElementNotFoundError);
86
+ }
87
+
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) {
91
+ try {
92
+ const tempDir = path.join(os.tmpdir(), "testdriver-debug");
93
+ if (!fs.existsSync(tempDir)) {
94
+ fs.mkdirSync(tempDir, { recursive: true });
95
+ }
96
+
97
+ const filename = `screenshot-${Date.now()}.png`;
98
+ this.screenshotPath = path.join(tempDir, filename);
99
+
100
+ // Remove data:image/png;base64, prefix if present
101
+ const base64Data = debugInfo.screenshot.replace(
102
+ /^data:image\/\w+;base64,/,
103
+ "",
104
+ );
105
+ const buffer = Buffer.from(base64Data, "base64");
106
+
107
+ fs.writeFileSync(this.screenshotPath, buffer);
108
+ } catch {
109
+ // If screenshot save fails, don't break the error
110
+ // Can't emit from constructor, just skip logging
111
+ }
112
+ }
113
+
114
+ // Save cached image if available
115
+ this.cachedImagePath = null;
116
+ if (debugInfo.cachedImageUrl) {
117
+ this.cachedImagePath = debugInfo.cachedImageUrl;
118
+ }
119
+
120
+ // Save pixel diff image if available
121
+ this.pixelDiffPath = null;
122
+ if (debugInfo.pixelDiffImage) {
123
+ try {
124
+ const tempDir = path.join(os.tmpdir(), "testdriver-debug");
125
+ if (!fs.existsSync(tempDir)) {
126
+ fs.mkdirSync(tempDir, { recursive: true });
127
+ }
128
+
129
+ const filename = `pixel-diff-error-${Date.now()}.png`;
130
+ this.pixelDiffPath = path.join(tempDir, filename);
131
+
132
+ const base64Data = debugInfo.pixelDiffImage.replace(
133
+ /^data:image\/\w+;base64,/,
134
+ "",
135
+ );
136
+ const buffer = Buffer.from(base64Data, "base64");
137
+
138
+ fs.writeFileSync(this.pixelDiffPath, buffer);
139
+ } catch {
140
+ // Silently skip logging error from constructor
141
+ }
142
+ }
143
+
144
+ // Extract similarity and input text from AI response
145
+ const similarity = this.aiResponse?.similarity ?? null;
146
+ const cacheHit =
147
+ this.aiResponse?.cacheHit ?? this.aiResponse?.cached ?? false;
148
+ const cacheStrategy = this.aiResponse?.cacheStrategy ?? null;
149
+ const cacheCreatedAt = this.aiResponse?.cacheCreatedAt ?? null;
150
+ const cacheDiffPercent = this.aiResponse?.cacheDiffPercent ?? null;
151
+ const threshold = debugInfo.threshold ?? null;
152
+ const inputText =
153
+ this.aiResponse?.input_text ?? this.aiResponse?.element ?? null;
154
+
155
+ // Enhance error message with debugging hints
156
+ this.message += `\n\n=== Debug Information ===`;
157
+ this.message += `\nElement searched for: "${this.description}"`;
158
+
159
+ if (threshold !== null) {
160
+ const similarityRequired = ((1 - threshold) * 100).toFixed(1);
161
+ this.message += `\nCache threshold: ${threshold} (${similarityRequired}% similarity required)`;
162
+ }
163
+
164
+ if (cacheHit) {
165
+ this.message += `\nCache: HIT`;
166
+ if (cacheStrategy) {
167
+ this.message += ` (${cacheStrategy} strategy)`;
168
+ }
169
+ if (cacheCreatedAt) {
170
+ const cacheAge = Math.round(
171
+ (Date.now() - new Date(cacheCreatedAt).getTime()) / 1000,
172
+ );
173
+ this.message += `\nCache created: ${new Date(cacheCreatedAt).toISOString()} (${cacheAge}s ago)`;
174
+ }
175
+ if (cacheDiffPercent !== null) {
176
+ this.message += `\nCache pixel diff: ${(cacheDiffPercent * 100).toFixed(2)}%`;
177
+ }
178
+ } else {
179
+ this.message += `\nCache: MISS`;
180
+ }
181
+
182
+ if (similarity !== null) {
183
+ const similarityPercent = (similarity * 100).toFixed(2);
184
+ this.message += `\nSimilarity score: ${similarityPercent}%`;
185
+
186
+ if (threshold !== null && similarity < 1 - threshold) {
187
+ this.message += ` (below threshold)`;
188
+ }
189
+ }
190
+
191
+ if (inputText) {
192
+ this.message += `\nInput text: "${inputText}"`;
193
+ }
194
+
195
+ if (this.screenshotPath) {
196
+ this.message += `\nCurrent screenshot: ${this.screenshotPath}`;
197
+ }
198
+
199
+ if (this.cachedImagePath) {
200
+ this.message += `\nCached image URL: ${this.cachedImagePath}`;
201
+ }
202
+
203
+ if (this.pixelDiffPath) {
204
+ this.message += `\nPixel diff image: ${this.pixelDiffPath}`;
205
+ }
206
+
207
+ if (this.aiResponse) {
208
+ const responseText =
209
+ this.aiResponse.response?.content?.[0]?.text ||
210
+ this.aiResponse.content?.[0]?.text ||
211
+ "No detailed response available";
212
+ this.message += `\n\nAI Response:\n${responseText}`;
213
+ }
214
+
215
+ // Clean up stack trace to only show userland code
216
+ if (this.stack) {
217
+ const lines = this.stack.split("\n");
218
+ const filteredLines = [lines[0]]; // Keep error message line
219
+
220
+ // Skip frames until we find userland code (not sdk.js internals)
221
+ let foundUserland = false;
222
+ for (let i = 1; i < lines.length; i++) {
223
+ const line = lines[i];
224
+
225
+ // Skip internal Element method frames (click, hover, etc.)
226
+ if (
227
+ line.includes("Element.click") ||
228
+ line.includes("Element.hover") ||
229
+ line.includes("Element.doubleClick") ||
230
+ line.includes("Element.rightClick") ||
231
+ line.includes("Element.mouseDown") ||
232
+ line.includes("Element.mouseUp")
233
+ ) {
234
+ continue;
235
+ }
236
+
237
+ // Once we hit userland code, include everything from there
238
+ if (!line.includes("sdk.js") || foundUserland) {
239
+ foundUserland = true;
240
+ filteredLines.push(line);
241
+ }
242
+ }
243
+
244
+ this.stack = filteredLines.join("\n");
245
+ }
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
+ }
266
+ }
267
+
268
+ /**
269
+ * Element class representing a located or to-be-located element
270
+ */
271
+ class Element {
272
+ constructor(description, sdk, system, commands) {
273
+ this.description = description;
274
+ this.sdk = sdk;
275
+ this.system = system;
276
+ this.commands = commands;
277
+ this.coordinates = null;
278
+ /* The above code is a JavaScript comment block that sets the `_found` property of an object to
279
+ `false`. The code snippet does not contain any executable code, it is just a comment. */
280
+ this._found = false;
281
+ this._response = null;
282
+ this._screenshot = null;
283
+ this._threshold = null; // Store the threshold used for this find
284
+ }
285
+
286
+ /**
287
+ * Check if element was found
288
+ * @returns {boolean} True if element coordinates were located
289
+ */
290
+ found() {
291
+ return this._found;
292
+ }
293
+
294
+ /**
295
+ * Find the element on screen
296
+ * @param {string} [newDescription] - Optional new description to search for
297
+ * @param {Object} [options] - Optional options object with cacheThreshold and/or cacheKey
298
+ * @returns {Promise<Element>} This element instance
299
+ */
300
+ async find(newDescription, options) {
301
+ const description = newDescription || this.description;
302
+ if (newDescription) {
303
+ this.description = newDescription;
304
+ }
305
+
306
+ const startTime = Date.now();
307
+ let response = null;
308
+ let findError = null;
309
+
310
+ const debugMode =
311
+ process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
312
+
313
+ // Log finding action
314
+ const { events } = require("./agent/events.js");
315
+ const findingMessage = formatter.formatElementFinding(description);
316
+ this.sdk.emitter.emit(events.log.log, findingMessage);
317
+
318
+ try {
319
+ const screenshot = await this.system.captureScreenBase64();
320
+ // Only store screenshot in DEBUG mode to prevent memory leaks
321
+ if (debugMode) {
322
+ this._screenshot = screenshot;
323
+ }
324
+
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
+ }
357
+
358
+ // Store the threshold for debugging
359
+ this._threshold = threshold;
360
+
361
+ // Debug log threshold
362
+ if (debugMode) {
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
+ : '';
367
+ this.sdk.emitter.emit(
368
+ events.log.debug,
369
+ `🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
370
+ );
371
+ }
372
+
373
+ response = await this.sdk.apiClient.req("find", {
374
+ session: this.sdk.getSessionId(),
375
+ element: description,
376
+ image: screenshot,
377
+ threshold: threshold,
378
+ cacheKey: cacheKey,
379
+ os: this.sdk.os,
380
+ resolution: this.sdk.resolution,
381
+ });
382
+
383
+ const duration = Date.now() - startTime;
384
+
385
+ console.log("AI Response Text:", response?.response.content[0]?.text);
386
+
387
+ if (response && response.coordinates) {
388
+ // Store response but clear large base64 data to prevent memory leaks
389
+ this._response = this._sanitizeResponse(response);
390
+ this.coordinates = response.coordinates;
391
+ this._found = true;
392
+
393
+ // Log debug information when element is found
394
+ this._logFoundDebug(response, duration);
395
+ } else {
396
+ this._response = this._sanitizeResponse(response);
397
+ this._found = false;
398
+ findError = "Element not found";
399
+ }
400
+ } catch (error) {
401
+ this._response = error.response
402
+ ? this._sanitizeResponse(error.response)
403
+ : null;
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
+ }
428
+ }
429
+
430
+ return this;
431
+ }
432
+
433
+ /**
434
+ * Sanitize response by removing large base64 data to prevent memory leaks
435
+ * @private
436
+ * @param {Object} response - API response
437
+ * @returns {Object} Sanitized response
438
+ */
439
+ _sanitizeResponse(response) {
440
+ if (!response) return null;
441
+
442
+ // Only keep base64 data in DEBUG mode
443
+ const debugMode =
444
+ process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
445
+ if (debugMode) {
446
+ return response;
447
+ }
448
+
449
+ // Create shallow copy and remove large base64 fields
450
+ const sanitized = { ...response };
451
+ delete sanitized.croppedImage;
452
+ delete sanitized.screenshot;
453
+
454
+ return sanitized;
455
+ }
456
+
457
+ /**
458
+ * Log debug information when element is successfully found
459
+ * @private
460
+ */
461
+ async _logFoundDebug(response, duration) {
462
+ const debugInfo = {
463
+ description: this.description,
464
+ coordinates: this.coordinates,
465
+ duration: `${duration}ms`,
466
+ cacheHit:
467
+ response.cacheHit || response.cache_hit || response.cached || false,
468
+ cacheStrategy: response.cacheStrategy || null,
469
+ similarity: response.similarity ?? null,
470
+ confidence: response.confidence ?? null,
471
+ };
472
+
473
+ // Emit element found as log:log event
474
+ const { events } = require("./agent/events.js");
475
+ const formattedMessage = formatter.formatElementFound(this.description, {
476
+ x: this.coordinates.x,
477
+ y: this.coordinates.y,
478
+ duration: debugInfo.duration,
479
+ cacheHit: debugInfo.cacheHit,
480
+ });
481
+ this.sdk.emitter.emit(events.log.log, formattedMessage);
482
+
483
+ // Log cache information in debug mode
484
+ const debugMode =
485
+ process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
486
+ if (debugMode) {
487
+ const { events } = require("./agent/events.js");
488
+ this.sdk.emitter.emit(events.log.debug, "Element Found:");
489
+ this.sdk.emitter.emit(
490
+ events.log.debug,
491
+ ` Description: ${debugInfo.description}`,
492
+ );
493
+ this.sdk.emitter.emit(
494
+ events.log.debug,
495
+ ` Coordinates: (${this.coordinates.x}, ${this.coordinates.y})`,
496
+ );
497
+ this.sdk.emitter.emit(
498
+ events.log.debug,
499
+ ` Duration: ${debugInfo.duration}`,
500
+ );
501
+ this.sdk.emitter.emit(
502
+ events.log.debug,
503
+ ` Cache Hit: ${debugInfo.cacheHit ? "✅ YES" : "❌ NO"}`,
504
+ );
505
+ if (debugInfo.cacheHit) {
506
+ this.sdk.emitter.emit(
507
+ events.log.debug,
508
+ ` Cache Strategy: ${debugInfo.cacheStrategy || "unknown"}`,
509
+ );
510
+ this.sdk.emitter.emit(
511
+ events.log.debug,
512
+ ` Similarity: ${debugInfo.similarity !== null ? (debugInfo.similarity * 100).toFixed(2) + "%" : "N/A"}`,
513
+ );
514
+ if (response.cacheCreatedAt) {
515
+ const cacheAge = Math.round(
516
+ (Date.now() - new Date(response.cacheCreatedAt).getTime()) / 1000,
517
+ );
518
+ this.sdk.emitter.emit(
519
+ events.log.debug,
520
+ ` Cache Age: ${cacheAge}s (created: ${new Date(response.cacheCreatedAt).toISOString()})`,
521
+ );
522
+ }
523
+ if (response.cachedImageUrl) {
524
+ this.sdk.emitter.emit(
525
+ events.log.debug,
526
+ ` Cached Image URL: ${response.cachedImageUrl}`,
527
+ );
528
+ }
529
+ if (response.cacheDiffPercent !== undefined) {
530
+ this.sdk.emitter.emit(
531
+ events.log.debug,
532
+ ` Pixel Diff: ${(response.cacheDiffPercent * 100).toFixed(2)}%`,
533
+ );
534
+ }
535
+ }
536
+ if (debugInfo.confidence !== null) {
537
+ this.sdk.emitter.emit(
538
+ events.log.debug,
539
+ ` Confidence: ${(debugInfo.confidence * 100).toFixed(2)}%`,
540
+ );
541
+ }
542
+
543
+ // Log available response fields for debugging
544
+ this.sdk.emitter.emit(
545
+ events.log.debug,
546
+ ` Has croppedImage: ${!!response.croppedImage}`,
547
+ );
548
+ this.sdk.emitter.emit(
549
+ events.log.debug,
550
+ ` Has screenshot: ${!!response.screenshot}`,
551
+ );
552
+ this.sdk.emitter.emit(
553
+ events.log.debug,
554
+ ` Has cachedImageUrl: ${!!response.cachedImageUrl}`,
555
+ );
556
+ this.sdk.emitter.emit(
557
+ events.log.debug,
558
+ ` Has pixelDiffImage: ${!!response.pixelDiffImage}`,
559
+ );
560
+ }
561
+
562
+ // Save cropped image with red circle if available
563
+ let croppedImagePath = null;
564
+ if (response.croppedImage) {
565
+ try {
566
+ const tempDir = path.join(os.tmpdir(), "testdriver-debug");
567
+ if (!fs.existsSync(tempDir)) {
568
+ fs.mkdirSync(tempDir, { recursive: true });
569
+ }
570
+
571
+ const filename = `element-found-${Date.now()}.png`;
572
+ croppedImagePath = path.join(tempDir, filename);
573
+
574
+ // Remove data:image/png;base64, prefix if present
575
+ const base64Data = response.croppedImage.replace(
576
+ /^data:image\/\w+;base64,/,
577
+ "",
578
+ );
579
+ const buffer = Buffer.from(base64Data, "base64");
580
+
581
+ fs.writeFileSync(croppedImagePath, buffer);
582
+
583
+ if (debugMode) {
584
+ const { events } = require("./agent/events.js");
585
+ this.sdk.emitter.emit(
586
+ events.log.debug,
587
+ ` Debug Image: ${croppedImagePath}`,
588
+ );
589
+ }
590
+ } catch (err) {
591
+ const { events } = require("./agent/events.js");
592
+ const errorMsg = formatter.formatError(
593
+ "Failed to save debug image",
594
+ err,
595
+ );
596
+ this.sdk.emitter.emit(events.log.log, errorMsg);
597
+ }
598
+ }
599
+
600
+ // Save cached screenshot if available and this was a cache hit
601
+ let cachedScreenshotPath = null;
602
+ if (debugInfo.cacheHit && response.screenshot) {
603
+ try {
604
+ const tempDir = path.join(os.tmpdir(), "testdriver-debug");
605
+ if (!fs.existsSync(tempDir)) {
606
+ fs.mkdirSync(tempDir, { recursive: true });
607
+ }
608
+
609
+ const filename = `cached-screenshot-${Date.now()}.png`;
610
+ cachedScreenshotPath = path.join(tempDir, filename);
611
+
612
+ // Remove data:image/png;base64, prefix if present
613
+ const base64Data = response.screenshot.replace(
614
+ /^data:image\/\w+;base64,/,
615
+ "",
616
+ );
617
+ const buffer = Buffer.from(base64Data, "base64");
618
+
619
+ fs.writeFileSync(cachedScreenshotPath, buffer);
620
+
621
+ if (debugMode) {
622
+ const { events } = require("./agent/events.js");
623
+ this.sdk.emitter.emit(
624
+ events.log.debug,
625
+ ` Cached Screenshot: ${cachedScreenshotPath}`,
626
+ );
627
+ }
628
+ } catch (err) {
629
+ const { events } = require("./agent/events.js");
630
+ const errorMsg = formatter.formatError(
631
+ "Failed to save cached screenshot",
632
+ err,
633
+ );
634
+ this.sdk.emitter.emit(events.log.log, errorMsg);
635
+ }
636
+ }
637
+
638
+ // Save pixel diff image if available and this was a cache hit
639
+ let pixelDiffPath = null;
640
+ if (debugInfo.cacheHit && response.pixelDiffImage) {
641
+ try {
642
+ const tempDir = path.join(os.tmpdir(), "testdriver-debug");
643
+ if (!fs.existsSync(tempDir)) {
644
+ fs.mkdirSync(tempDir, { recursive: true });
645
+ }
646
+
647
+ const filename = `pixel-diff-${Date.now()}.png`;
648
+ pixelDiffPath = path.join(tempDir, filename);
649
+
650
+ // Remove data:image/png;base64, prefix if present
651
+ const base64Data = response.pixelDiffImage.replace(
652
+ /^data:image\/\w+;base64,/,
653
+ "",
654
+ );
655
+ const buffer = Buffer.from(base64Data, "base64");
656
+
657
+ fs.writeFileSync(pixelDiffPath, buffer);
658
+
659
+ if (debugMode) {
660
+ const { events } = require("./agent/events.js");
661
+ this.sdk.emitter.emit(
662
+ events.log.debug,
663
+ ` Pixel Diff Image: ${pixelDiffPath}`,
664
+ );
665
+ }
666
+ } catch (err) {
667
+ const { events } = require("./agent/events.js");
668
+ const errorMsg = formatter.formatError(
669
+ "Failed to save pixel diff image",
670
+ err,
671
+ );
672
+ this.sdk.emitter.emit(events.log.log, errorMsg);
673
+ }
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Click on the element
679
+ * @param {ClickAction} [action='click'] - Type of click action
680
+ * @returns {Promise<void>}
681
+ */
682
+ async click(action = "click") {
683
+ if (!this._found || !this.coordinates) {
684
+ throw new ElementNotFoundError(
685
+ `Element "${this.description}" not found.`,
686
+ {
687
+ description: this.description,
688
+ aiResponse: this._response,
689
+ threshold: this._threshold,
690
+ },
691
+ );
692
+ }
693
+
694
+ // Log the action
695
+ const { events } = require("./agent/events.js");
696
+ const actionName = action === "click" ? "click" : action.replace("-", " ");
697
+ const formattedMessage = formatter.formatAction(
698
+ actionName,
699
+ this.description,
700
+ );
701
+ this.sdk.emitter.emit(events.log.log, formattedMessage);
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
+
715
+ if (action === "hover") {
716
+ await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
717
+ } else {
718
+ await this.commands.click(this.coordinates.x, this.coordinates.y, action, elementData);
719
+ }
720
+ }
721
+
722
+ /**
723
+ * Hover over the element
724
+ * @returns {Promise<void>}
725
+ */
726
+ async hover() {
727
+ if (!this._found || !this.coordinates) {
728
+ throw new ElementNotFoundError(
729
+ `Element "${this.description}" not found.`,
730
+ {
731
+ description: this.description,
732
+ aiResponse: this._response,
733
+ threshold: this._threshold,
734
+ },
735
+ );
736
+ }
737
+
738
+ // Log the hover action
739
+ const { events } = require("./agent/events.js");
740
+ const formattedMessage = formatter.formatAction("hover", this.description);
741
+ this.sdk.emitter.emit(events.log.log, formattedMessage);
742
+
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);
756
+ }
757
+
758
+ /**
759
+ * Double-click on the element
760
+ * @returns {Promise<void>}
761
+ */
762
+ async doubleClick() {
763
+ return this.click("double-click");
764
+ }
765
+
766
+ /**
767
+ * Right-click on the element
768
+ * @returns {Promise<void>}
769
+ */
770
+ async rightClick() {
771
+ return this.click("right-click");
772
+ }
773
+
774
+ /**
775
+ * Press mouse button down on this element
776
+ * @returns {Promise<void>}
777
+ */
778
+ async mouseDown() {
779
+ return this.click("mouseDown");
780
+ }
781
+
782
+ /**
783
+ * Release mouse button on this element
784
+ * @returns {Promise<void>}
785
+ */
786
+ async mouseUp() {
787
+ return this.click("mouseUp");
788
+ }
789
+
790
+ /**
791
+ * Get the coordinates of the element
792
+ * @returns {{x: number, y: number, centerX: number, centerY: number}|null}
793
+ */
794
+ getCoordinates() {
795
+ return this.coordinates;
796
+ }
797
+
798
+ /**
799
+ * Get the x coordinate (top-left)
800
+ * @returns {number|null}
801
+ */
802
+ get x() {
803
+ return this.coordinates?.x ?? null;
804
+ }
805
+
806
+ /**
807
+ * Get the y coordinate (top-left)
808
+ * @returns {number|null}
809
+ */
810
+ get y() {
811
+ return this.coordinates?.y ?? null;
812
+ }
813
+
814
+ /**
815
+ * Get the center x coordinate
816
+ * @returns {number|null}
817
+ */
818
+ get centerX() {
819
+ return this.coordinates?.centerX ?? null;
820
+ }
821
+
822
+ /**
823
+ * Get the center y coordinate
824
+ * @returns {number|null}
825
+ */
826
+ get centerY() {
827
+ return this.coordinates?.centerY ?? null;
828
+ }
829
+
830
+ /**
831
+ * Get the full API response data
832
+ * @returns {Object|null}
833
+ */
834
+ getResponse() {
835
+ return this._response;
836
+ }
837
+
838
+ /**
839
+ * Get element screenshot if available
840
+ * @returns {string|null} Base64 encoded screenshot
841
+ */
842
+ get screenshot() {
843
+ return this._response?.screenshot ?? null;
844
+ }
845
+
846
+ /**
847
+ * Get element confidence score if available
848
+ * @returns {number|null}
849
+ */
850
+ get confidence() {
851
+ return this._response?.confidence ?? null;
852
+ }
853
+
854
+ /**
855
+ * Get element width if available
856
+ * @returns {number|null}
857
+ */
858
+ get width() {
859
+ return this._response?.width ?? null;
860
+ }
861
+
862
+ /**
863
+ * Get element height if available
864
+ * @returns {number|null}
865
+ */
866
+ get height() {
867
+ return this._response?.height ?? null;
868
+ }
869
+
870
+ /**
871
+ * Get element bounding box if available
872
+ * @returns {Object|null}
873
+ */
874
+ get boundingBox() {
875
+ return this._response?.boundingBox ?? null;
876
+ }
877
+
878
+ /**
879
+ * Get element text content if available
880
+ * @returns {string|null}
881
+ */
882
+ get text() {
883
+ return this._response?.text ?? null;
884
+ }
885
+
886
+ /**
887
+ * Get element label if available
888
+ * @returns {string|null}
889
+ */
890
+ get label() {
891
+ return this._response?.label ?? null;
892
+ }
893
+
894
+ /**
895
+ * Save the debug screenshot to a file for manual inspection
896
+ * @param {string} [filepath] - Path to save the screenshot (defaults to ./debug-screenshot-{timestamp}.png)
897
+ * @returns {Promise<string>} Path to the saved screenshot
898
+ */
899
+ async saveDebugScreenshot(filepath) {
900
+ if (!this._screenshot) {
901
+ throw new Error("No screenshot available.");
902
+ }
903
+
904
+ const fs = require("fs").promises;
905
+ const path = require("path");
906
+
907
+ const defaultPath = `./debug-screenshot-${Date.now()}.png`;
908
+ const savePath = filepath || defaultPath;
909
+
910
+ // Remove data:image/png;base64, prefix if present
911
+ const base64Data = this._screenshot.replace(/^data:image\/\w+;base64,/, "");
912
+ const buffer = Buffer.from(base64Data, "base64");
913
+
914
+ await fs.writeFile(savePath, buffer);
915
+ return path.resolve(savePath);
916
+ }
917
+
918
+ /**
919
+ * Get debug information about the last find operation
920
+ * @returns {Object} Debug information including AI response and screenshot metadata
921
+ */
922
+ getDebugInfo() {
923
+ return {
924
+ description: this.description,
925
+ found: this._found,
926
+ coordinates: this.coordinates,
927
+ aiResponse: this._response,
928
+ hasScreenshot: !!this._screenshot,
929
+ screenshotSize: this._screenshot ? this._screenshot.length : 0,
930
+ };
931
+ }
932
+
933
+ /**
934
+ * Clean up element resources to prevent memory leaks
935
+ * Call this when you're done with the element
936
+ */
937
+ destroy() {
938
+ this._screenshot = null;
939
+ this._response = null;
940
+ this.coordinates = null;
941
+ this.sdk = null;
942
+ this.system = null;
943
+ this.commands = null;
944
+ }
945
+ }
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
+
1001
+ /**
1002
+ * TestDriver SDK
1003
+ *
1004
+ * This SDK provides programmatic access to TestDriver's AI-powered testing capabilities.
1005
+ *
1006
+ * @example
1007
+ * const TestDriver = require('testdriverai');
1008
+ *
1009
+ * const client = new TestDriver(process.env.TD_API_KEY);
1010
+ * await client.connect();
1011
+ *
1012
+ * // New API
1013
+ * const element = await client.find('Submit button');
1014
+ * await element.click();
1015
+ *
1016
+ * // Legacy API (deprecated)
1017
+ * await client.hoverText('Submit');
1018
+ * await client.click();
1019
+ */
1020
+
1021
+ /**
1022
+ * @typedef {'click' | 'right-click' | 'double-click' | 'hover' | 'mouseDown' | 'mouseUp'} ClickAction
1023
+ * @typedef {'up' | 'down' | 'left' | 'right'} ScrollDirection
1024
+ * @typedef {'keyboard' | 'mouse'} ScrollMethod
1025
+ * @typedef {'ai' | 'turbo'} TextMatchMethod
1026
+ * @typedef {'js' | 'pwsh'} ExecLanguage
1027
+ * @typedef {'\\t' | '\n' | '\r' | ' ' | '!' | '"' | '#' | '$' | '%' | '&' | "'" | '(' | ')' | '*' | '+' | ',' | '-' | '.' | '/' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | ':' | ';' | '<' | '=' | '>' | '?' | '@' | '[' | '\\' | ']' | '^' | '_' | '`' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | '{' | '|' | '}' | '~' | 'accept' | 'add' | 'alt' | 'altleft' | 'altright' | 'apps' | 'backspace' | 'browserback' | 'browserfavorites' | 'browserforward' | 'browserhome' | 'browserrefresh' | 'browsersearch' | 'browserstop' | 'capslock' | 'clear' | 'convert' | 'ctrl' | 'ctrlleft' | 'ctrlright' | 'decimal' | 'del' | 'delete' | 'divide' | 'down' | 'end' | 'enter' | 'esc' | 'escape' | 'execute' | 'f1' | 'f10' | 'f11' | 'f12' | 'f13' | 'f14' | 'f15' | 'f16' | 'f17' | 'f18' | 'f19' | 'f2' | 'f20' | 'f21' | 'f22' | 'f23' | 'f24' | 'f3' | 'f4' | 'f5' | 'f6' | 'f7' | 'f8' | 'f9' | 'final' | 'fn' | 'hanguel' | 'hangul' | 'hanja' | 'help' | 'home' | 'insert' | 'junja' | 'kana' | 'kanji' | 'launchapp1' | 'launchapp2' | 'launchmail' | 'launchmediaselect' | 'left' | 'modechange' | 'multiply' | 'nexttrack' | 'nonconvert' | 'num0' | 'num1' | 'num2' | 'num3' | 'num4' | 'num5' | 'num6' | 'num7' | 'num8' | 'num9' | 'numlock' | 'pagedown' | 'pageup' | 'pause' | 'pgdn' | 'pgup' | 'playpause' | 'prevtrack' | 'print' | 'printscreen' | 'prntscrn' | 'prtsc' | 'prtscr' | 'return' | 'right' | 'scrolllock' | 'select' | 'separator' | 'shift' | 'shiftleft' | 'shiftright' | 'sleep' | 'space' | 'stop' | 'subtract' | 'tab' | 'up' | 'volumedown' | 'volumemute' | 'volumeup' | 'win' | 'winleft' | 'winright' | 'yen' | 'command' | 'option' | 'optionleft' | 'optionright'} KeyboardKey
1028
+ */
1029
+
1030
+ const TestDriverAgent = require("./agent/index.js");
1031
+ const { events } = require("./agent/events.js");
1032
+ const { createMarkdownLogger } = require("./interfaces/logger.js");
1033
+
1034
+ class TestDriverSDK {
1035
+ constructor(apiKey, options = {}) {
1036
+ // Set up environment with API key
1037
+ const environment = {
1038
+ TD_API_KEY: apiKey,
1039
+ TD_API_ROOT: options.apiRoot || "https://testdriver-api.onrender.com",
1040
+ TD_RESOLUTION: options.resolution || "1366x768",
1041
+ TD_ANALYTICS: options.analytics !== false,
1042
+ ...options.environment,
1043
+ };
1044
+
1045
+ // Create the underlying agent with minimal CLI args
1046
+ this.agent = new TestDriverAgent(environment, {
1047
+ command: "sdk",
1048
+ args: [],
1049
+ options: {
1050
+ os: options.os || "linux",
1051
+ },
1052
+ });
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
+
1065
+ // Store options for later use
1066
+ this.options = options;
1067
+
1068
+ // Store os and resolution for API requests
1069
+ this.os = options.os || "linux";
1070
+ this.resolution = options.resolution || "1366x768";
1071
+
1072
+ // Store newSandbox preference from options
1073
+ this.newSandbox =
1074
+ options.newSandbox !== undefined ? options.newSandbox : true;
1075
+
1076
+ // Store headless preference from options
1077
+ this.headless = options.headless !== undefined ? options.headless : false;
1078
+
1079
+ // Store IP address if provided for direct connection
1080
+ this.ip = options.ip || null;
1081
+
1082
+ // Store sandbox configuration options
1083
+ this.sandboxAmi = options.sandboxAmi || null;
1084
+ this.sandboxOs = options.sandboxOs || null;
1085
+ this.sandboxInstance = options.sandboxInstance || null;
1086
+
1087
+ // Cache threshold configuration
1088
+ // threshold = pixel difference allowed (0.05 = 5% difference, 95% similarity)
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
1097
+ this.cacheThresholds = {
1098
+ find: -1,
1099
+ findAll: -1,
1100
+ };
1101
+ } else {
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
1104
+ this.cacheThresholds = {
1105
+ find: options.cacheThreshold?.find ?? -1, // Default: cache disabled
1106
+ findAll: options.cacheThreshold?.findAll ?? -1, // Default: cache disabled
1107
+ };
1108
+ }
1109
+
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;
1131
+
1132
+ // Track connection state
1133
+ this.connected = false;
1134
+ this.authenticated = false;
1135
+
1136
+ // Expose commonly used agent properties
1137
+ this.emitter = this.agent.emitter;
1138
+ this.config = this.agent.config;
1139
+ this.session = this.agent.session;
1140
+ this.apiClient = this.agent.sdk;
1141
+ this.analytics = this.agent.analytics;
1142
+ this.sandbox = this.agent.sandbox;
1143
+ this.system = this.agent.system;
1144
+ this.instance = null;
1145
+
1146
+ // Commands will be set up dynamically after connection
1147
+ this.commands = null;
1148
+
1149
+ // Set up logging if enabled (after emitter is exposed)
1150
+ this.loggingEnabled = options.logging !== false;
1151
+
1152
+ // Set up event listeners once (they live for the lifetime of the SDK instance)
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
+ };
1390
+ }
1391
+
1392
+ /**
1393
+ * Authenticate with TestDriver API
1394
+ * @returns {Promise<string>} Authentication token
1395
+ */
1396
+ async auth() {
1397
+ if (this.authenticated) {
1398
+ return;
1399
+ }
1400
+
1401
+ const token = await this.apiClient.auth();
1402
+ this.authenticated = true;
1403
+ return token;
1404
+ }
1405
+
1406
+ /**
1407
+ * Connect to a sandbox environment
1408
+ * @param {Object} options - Connection options
1409
+ * @param {string} options.sandboxId - Existing sandbox ID to reconnect to
1410
+ * @param {boolean} options.newSandbox - Force creation of a new sandbox
1411
+ * @param {string} options.ip - Direct IP address to connect to
1412
+ * @param {string} options.sandboxAmi - AMI to use for the sandbox
1413
+ * @param {string} options.sandboxInstance - Instance type for the sandbox
1414
+ * @param {string} options.os - Operating system for the sandbox (windows or linux)
1415
+ * @param {boolean} options.reuseConnection - Reuse recent connection if available (default: true)
1416
+ * @returns {Promise<Object>} Sandbox instance details
1417
+ */
1418
+ async connect(connectOptions = {}) {
1419
+ if (this.connected) {
1420
+ throw new Error(
1421
+ "Already connected. Create a new TestDriver instance to connect again.",
1422
+ );
1423
+ }
1424
+
1425
+ // Authenticate first if not already authenticated
1426
+ if (!this.authenticated) {
1427
+ await this.auth();
1428
+ }
1429
+
1430
+ // Initialize debugger server before connecting to sandbox
1431
+ // This ensures the debuggerUrl is available for renderSandbox
1432
+ await this._initializeDebugger();
1433
+
1434
+ // Map SDK connect options to agent buildEnv options
1435
+ // Use connectOptions.newSandbox if provided, otherwise fall back to this.newSandbox
1436
+ // Use connectOptions.headless if provided, otherwise fall back to this.headless
1437
+ const buildEnvOptions = {
1438
+ headless:
1439
+ connectOptions.headless !== undefined
1440
+ ? connectOptions.headless
1441
+ : this.headless,
1442
+ new:
1443
+ connectOptions.newSandbox !== undefined
1444
+ ? connectOptions.newSandbox
1445
+ : this.newSandbox,
1446
+ };
1447
+
1448
+ // Set agent properties for buildEnv to use
1449
+ if (connectOptions.sandboxId) {
1450
+ this.agent.sandboxId = connectOptions.sandboxId;
1451
+ }
1452
+ // Use IP from connectOptions if provided, otherwise fall back to constructor IP
1453
+ if (connectOptions.ip !== undefined) {
1454
+ this.agent.ip = connectOptions.ip;
1455
+ } else if (this.ip) {
1456
+ this.agent.ip = this.ip;
1457
+ }
1458
+ // Use sandboxAmi from connectOptions if provided, otherwise fall back to constructor value
1459
+ if (connectOptions.sandboxAmi !== undefined) {
1460
+ this.agent.sandboxAmi = connectOptions.sandboxAmi;
1461
+ } else if (this.sandboxAmi) {
1462
+ this.agent.sandboxAmi = this.sandboxAmi;
1463
+ }
1464
+ // Use sandboxInstance from connectOptions if provided, otherwise fall back to constructor value
1465
+ if (connectOptions.sandboxInstance !== undefined) {
1466
+ this.agent.sandboxInstance = connectOptions.sandboxInstance;
1467
+ } else if (this.sandboxInstance) {
1468
+ this.agent.sandboxInstance = this.sandboxInstance;
1469
+ }
1470
+ // Use os from connectOptions if provided, otherwise fall back to constructor value
1471
+ if (connectOptions.os !== undefined) {
1472
+ this.agent.sandboxOs = connectOptions.os;
1473
+ } else if (this.sandboxOs) {
1474
+ this.agent.sandboxOs = this.sandboxOs;
1475
+ }
1476
+
1477
+ // Set redrawThreshold on agent's cliArgs.options
1478
+ this.agent.cliArgs.options.redrawThreshold = this.redrawThreshold;
1479
+
1480
+ // Use the agent's buildEnv method which handles all the connection logic
1481
+ await this.agent.buildEnv(buildEnvOptions);
1482
+
1483
+ // Get the instance from the agent
1484
+ this.instance = this.agent.instance;
1485
+
1486
+ // Expose the agent's commands, parser, and commander
1487
+ this.commands = this.agent.commands;
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
+
1505
+ // Dynamically create command methods based on available commands
1506
+ this._setupCommandMethods();
1507
+
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
+
1513
+ this.analytics.track("sdk.connect", {
1514
+ sandboxId: this.instance?.instanceId,
1515
+ isReconnected: this.isReconnected,
1516
+ });
1517
+
1518
+ return this.instance;
1519
+ }
1520
+
1521
+ /**
1522
+ * Disconnect from the sandbox
1523
+ * Note: After disconnecting, you cannot reconnect with the same SDK instance.
1524
+ * Create a new TestDriver instance if you need to connect again.
1525
+ * @returns {Promise<void>}
1526
+ */
1527
+ async disconnect() {
1528
+ if (this.connected && this.instance) {
1529
+ // Track disconnect event
1530
+ this.analytics.track("sdk.disconnect");
1531
+
1532
+ this.connected = false;
1533
+ this.instance = null;
1534
+ }
1535
+ }
1536
+
1537
+ /**
1538
+ * Get the current session ID
1539
+ * Used for tracking and associating dashcam recordings with test results
1540
+ * @returns {string|null} The session ID or null if not connected
1541
+ */
1542
+ getSessionId() {
1543
+ return this.session?.get() || null;
1544
+ }
1545
+
1546
+ // ====================================
1547
+ // Element Finding API
1548
+ // ====================================
1549
+
1550
+ /**
1551
+ * Find an element by description
1552
+ * Automatically locates the element and returns it
1553
+ *
1554
+ * @param {string} description - Description of the element to find
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
1557
+ *
1558
+ * @example
1559
+ * // Find and click immediately (chainable)
1560
+ * await client.find('the sign in button').click();
1561
+ *
1562
+ * @example
1563
+ * // Find and click (traditional)
1564
+ * const element = await client.find('the sign in button');
1565
+ * await element.click();
1566
+ *
1567
+ * @example
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)
1573
+ * const element = await client.find('login button', 0.01);
1574
+ *
1575
+ * @example
1576
+ * // Poll until element is found
1577
+ * let element;
1578
+ * while (!element?.found()) {
1579
+ * element = await client.find('login button');
1580
+ * if (!element.found()) {
1581
+ * await new Promise(resolve => setTimeout(resolve, 1000));
1582
+ * }
1583
+ * }
1584
+ * await element.click();
1585
+ */
1586
+ find(description, options) {
1587
+ this._ensureConnected();
1588
+ const element = new Element(description, this, this.system, this.commands);
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);
1594
+ }
1595
+
1596
+ /**
1597
+ * Find all elements matching a description
1598
+ * Automatically locates all matching elements and returns them as an array
1599
+ *
1600
+ * @param {string} description - Description of the elements to find
1601
+ * @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
1602
+ * @returns {Promise<Element[]>} Array of Element instances that have been located
1603
+ *
1604
+ * @example
1605
+ * // Find all buttons and click the first one
1606
+ * const buttons = await client.findAll('button');
1607
+ * if (buttons.length > 0) {
1608
+ * await buttons[0].click();
1609
+ * }
1610
+ *
1611
+ * @example
1612
+ * // Find all list items with cache key to enable caching
1613
+ * const items = await client.findAll('list item', { cacheKey: 'my-test-run' });
1614
+ * for (const item of items) {
1615
+ * console.log(`Found item at (${item.x}, ${item.y})`);
1616
+ * }
1617
+ */
1618
+ async findAll(description, options) {
1619
+ this._ensureConnected();
1620
+
1621
+ const startTime = Date.now();
1622
+
1623
+ // Log finding all action
1624
+ const { events } = require("./agent/events.js");
1625
+ const findingMessage = formatter.formatElementsFinding(description);
1626
+ this.emitter.emit(events.log.log, findingMessage);
1627
+
1628
+ try {
1629
+ const screenshot = await this.system.captureScreenBase64();
1630
+
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
+ }
1675
+
1676
+ const response = await this.apiClient.req(
1677
+ "/api/v7.0.0/testdriver-agent/testdriver-find-all",
1678
+ {
1679
+ session: this.getSessionId(),
1680
+ element: description,
1681
+ image: screenshot,
1682
+ threshold: threshold,
1683
+ cacheKey: cacheKey,
1684
+ os: this.os,
1685
+ resolution: this.resolution,
1686
+ },
1687
+ );
1688
+
1689
+ const duration = Date.now() - startTime;
1690
+
1691
+ if (response && response.elements && response.elements.length > 0) {
1692
+ // Log found elements
1693
+ const foundMessage = formatter.formatElementsFound(
1694
+ description,
1695
+ response.elements.length,
1696
+ {
1697
+ duration: `${duration}ms`,
1698
+ cacheHit: response.cached || false,
1699
+ },
1700
+ );
1701
+ this.emitter.emit(events.log.log, foundMessage);
1702
+
1703
+ // Create Element instances for each found element
1704
+ const elements = response.elements.map((elementData) => {
1705
+ const element = new Element(
1706
+ description,
1707
+ this,
1708
+ this.system,
1709
+ this.commands,
1710
+ );
1711
+
1712
+ // Set element as found with its coordinates
1713
+ element.coordinates = elementData.coordinates;
1714
+ element._found = true;
1715
+ element._response = this._sanitizeResponseForElement(
1716
+ response,
1717
+ elementData,
1718
+ );
1719
+
1720
+ // Only store screenshot in DEBUG mode
1721
+ const debugMode =
1722
+ process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
1723
+ if (debugMode) {
1724
+ element._screenshot = screenshot;
1725
+ }
1726
+
1727
+ return element;
1728
+ });
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
+
1751
+ // Log debug information when elements are found
1752
+ if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
1753
+ const { events } = require("./agent/events.js");
1754
+ this.emitter.emit(
1755
+ events.log.debug,
1756
+ `✓ Found ${elements.length} element(s): "${description}"`,
1757
+ );
1758
+ this.emitter.emit(
1759
+ events.log.debug,
1760
+ ` Cache: ${response.cached ? "HIT" : "MISS"}`,
1761
+ );
1762
+ this.emitter.emit(events.log.debug, ` Time: ${duration}ms`);
1763
+ }
1764
+
1765
+ return elements;
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
+
1789
+ // No elements found - return empty array
1790
+ return [];
1791
+ }
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
+
1812
+ const { events } = require("./agent/events.js");
1813
+ this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
1814
+ return [];
1815
+ }
1816
+ }
1817
+
1818
+ /**
1819
+ * Sanitize response for individual element in findAll results
1820
+ * @private
1821
+ * @param {Object} response - Full API response
1822
+ * @param {Object} elementData - Individual element data
1823
+ * @returns {Object} Sanitized response for this element
1824
+ */
1825
+ _sanitizeResponseForElement(response, elementData) {
1826
+ const debugMode =
1827
+ process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
1828
+
1829
+ // Combine global response data with element-specific data
1830
+ const sanitized = {
1831
+ coordinates: elementData.coordinates,
1832
+ cached: response.cached || false,
1833
+ elementType: response.elementType,
1834
+ extractedText: response.extractedText,
1835
+ confidence: elementData.confidence,
1836
+ similarity: elementData.similarity,
1837
+ boundingBox: elementData.boundingBox,
1838
+ width: elementData.width,
1839
+ height: elementData.height,
1840
+ text: elementData.text,
1841
+ label: elementData.label,
1842
+ };
1843
+
1844
+ // Only keep large data in debug mode
1845
+ if (debugMode) {
1846
+ sanitized.croppedImage = elementData.croppedImage;
1847
+ sanitized.screenshot = response.screenshot;
1848
+ }
1849
+
1850
+ return sanitized;
1851
+ }
1852
+
1853
+ // ====================================
1854
+ // Command Methods Setup
1855
+ // ====================================
1856
+
1857
+ /**
1858
+ * Dynamically set up command methods based on available commands
1859
+ * This creates camelCase methods that wrap the underlying command functions
1860
+ * @private
1861
+ */
1862
+ _setupCommandMethods() {
1863
+ // Mapping from command names to SDK method names with type definitions
1864
+ // Each command supports both positional args (legacy) and object args (new)
1865
+ const commandMapping = {
1866
+ "hover-text": {
1867
+ name: "hoverText",
1868
+ /**
1869
+ * Hover over text on screen
1870
+ * @deprecated Use find() and element.click() instead
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
1876
+ * @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
1877
+ */
1878
+ doc: "Hover over text on screen (deprecated - use find() instead)",
1879
+ },
1880
+ "hover-image": {
1881
+ name: "hoverImage",
1882
+ /**
1883
+ * Hover over an image on screen
1884
+ * @deprecated Use find() and element.click() instead
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
1888
+ * @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
1889
+ */
1890
+ doc: "Hover over an image on screen (deprecated - use find() instead)",
1891
+ },
1892
+ "match-image": {
1893
+ name: "matchImage",
1894
+ /**
1895
+ * Match and interact with an image template
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
1900
+ * @returns {Promise<boolean>}
1901
+ */
1902
+ doc: "Match and interact with an image template",
1903
+ },
1904
+ type: {
1905
+ name: "type",
1906
+ /**
1907
+ * Type text
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)
1912
+ * @returns {Promise<void>}
1913
+ */
1914
+ doc: "Type text (use { secret: true } for passwords)",
1915
+ },
1916
+ "press-keys": {
1917
+ name: "pressKeys",
1918
+ /**
1919
+ * Press keyboard keys
1920
+ * @param {KeyboardKey[]} keys - Array of keys to press
1921
+ * @param {Object} [options] - Additional options (reserved for future use)
1922
+ * @returns {Promise<void>}
1923
+ */
1924
+ doc: "Press keyboard keys",
1925
+ },
1926
+ click: {
1927
+ name: "click",
1928
+ /**
1929
+ * Click at coordinates
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
1934
+ * @returns {Promise<void>}
1935
+ */
1936
+ doc: "Click at coordinates",
1937
+ },
1938
+ hover: {
1939
+ name: "hover",
1940
+ /**
1941
+ * Hover at coordinates
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
1945
+ * @returns {Promise<void>}
1946
+ */
1947
+ doc: "Hover at coordinates",
1948
+ },
1949
+ scroll: {
1950
+ name: "scroll",
1951
+ /**
1952
+ * Scroll the page
1953
+ * @param {ScrollDirection} [direction='down'] - Direction to scroll
1954
+ * @param {Object} [options] - Additional options
1955
+ * @param {number} [options.amount=300] - Amount to scroll in pixels
1956
+ * @returns {Promise<void>}
1957
+ */
1958
+ doc: "Scroll the page",
1959
+ },
1960
+ wait: {
1961
+ name: "wait",
1962
+ /**
1963
+ * Wait for specified time
1964
+ * @deprecated Consider using element polling with find() instead of arbitrary waits
1965
+ * @param {number} [timeout=3000] - Time to wait in milliseconds
1966
+ * @param {Object} [options] - Additional options (reserved for future use)
1967
+ * @returns {Promise<void>}
1968
+ */
1969
+ doc: "Wait for specified time (deprecated - consider element polling instead)",
1970
+ },
1971
+ "wait-for-text": {
1972
+ name: "waitForText",
1973
+ /**
1974
+ * Wait for text to appear on screen
1975
+ * @deprecated Use find() in a polling loop instead
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
1979
+ * @returns {Promise<void>}
1980
+ */
1981
+ doc: "Wait for text to appear on screen (deprecated - use find() in a loop instead)",
1982
+ },
1983
+ "wait-for-image": {
1984
+ name: "waitForImage",
1985
+ /**
1986
+ * Wait for image to appear on screen
1987
+ * @deprecated Use find() in a polling loop instead
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
1991
+ * @returns {Promise<void>}
1992
+ */
1993
+ doc: "Wait for image to appear on screen (deprecated - use find() in a loop instead)",
1994
+ },
1995
+ "scroll-until-text": {
1996
+ name: "scrollUntilText",
1997
+ /**
1998
+ * Scroll until text is found
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
2004
+ * @returns {Promise<void>}
2005
+ */
2006
+ doc: "Scroll until text is found",
2007
+ },
2008
+ "scroll-until-image": {
2009
+ name: "scrollUntilImage",
2010
+ /**
2011
+ * Scroll until image is found
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
2019
+ * @returns {Promise<void>}
2020
+ */
2021
+ doc: "Scroll until image is found",
2022
+ },
2023
+ "focus-application": {
2024
+ name: "focusApplication",
2025
+ /**
2026
+ * Focus an application by name
2027
+ * @param {string} name - Application name
2028
+ * @param {Object} [options] - Additional options (reserved for future use)
2029
+ * @returns {Promise<string>}
2030
+ */
2031
+ doc: "Focus an application by name",
2032
+ },
2033
+ remember: {
2034
+ name: "remember",
2035
+ /**
2036
+ * Extract and remember information from the screen using AI
2037
+ * @param {Object|string} options - Options object or description (legacy positional)
2038
+ * @param {string} options.description - What to remember
2039
+ * @returns {Promise<string>}
2040
+ */
2041
+ doc: "Extract and remember information from the screen",
2042
+ },
2043
+ assert: {
2044
+ name: "assert",
2045
+ /**
2046
+ * Make an AI-powered assertion
2047
+ * @param {string} assertion - Assertion to check
2048
+ * @param {Object} [options] - Additional options (reserved for future use)
2049
+ * @returns {Promise<boolean>}
2050
+ */
2051
+ doc: "Make an AI-powered assertion",
2052
+ },
2053
+ exec: {
2054
+ name: "exec",
2055
+ /**
2056
+ * Execute code in the sandbox
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
2062
+ * @returns {Promise<string>}
2063
+ */
2064
+ doc: "Execute code in the sandbox",
2065
+ },
2066
+ };
2067
+
2068
+ // Create SDK methods dynamically from commands
2069
+ Object.keys(this.commands).forEach((commandName) => {
2070
+ const command = this.commands[commandName];
2071
+ const methodInfo = commandMapping[commandName];
2072
+
2073
+ if (!methodInfo) {
2074
+ // Skip commands not in mapping
2075
+ return;
2076
+ }
2077
+
2078
+ const methodName = methodInfo.name;
2079
+
2080
+ // Create the wrapper method with proper stack trace handling
2081
+ this[methodName] = async function (...args) {
2082
+ this._ensureConnected();
2083
+
2084
+ // Capture the call site for better error reporting
2085
+ const callSite = {};
2086
+ Error.captureStackTrace(callSite, this[methodName]);
2087
+
2088
+ try {
2089
+ return await command(...args);
2090
+ } catch (error) {
2091
+ // Ensure we have a proper Error object with a message
2092
+ let properError = error;
2093
+ if (!(error instanceof Error)) {
2094
+ // If it's not an Error object, create one with a proper message
2095
+ const errorMessage =
2096
+ error?.message || error?.reason || JSON.stringify(error);
2097
+ properError = new Error(errorMessage);
2098
+ // Preserve additional properties
2099
+ if (error?.code) properError.code = error.code;
2100
+ if (error?.fullError) properError.fullError = error.fullError;
2101
+ }
2102
+
2103
+ // Replace the stack trace to point to the actual caller instead of SDK internals
2104
+ if (Error.captureStackTrace && callSite.stack) {
2105
+ // Preserve the error message but use the captured call site stack
2106
+ const errorMessage = properError.stack?.split("\n")[0];
2107
+ const callerStack = callSite.stack?.split("\n").slice(1); // Skip "Error" line
2108
+ properError.stack = errorMessage + "\n" + callerStack.join("\n");
2109
+ }
2110
+ throw properError;
2111
+ }
2112
+ }.bind(this);
2113
+
2114
+ // Preserve the original function's name for better debugging
2115
+ Object.defineProperty(this[methodName], "name", {
2116
+ value: methodName,
2117
+ writable: false,
2118
+ });
2119
+ });
2120
+ }
2121
+
2122
+ // ====================================
2123
+ // Helper Methods
2124
+ // ====================================
2125
+
2126
+ /**
2127
+ * Capture a screenshot of the current screen
2128
+ * @param {number} [scale=1] - Scale factor for the screenshot (1 = original size)
2129
+ * @param {boolean} [silent=false] - Whether to suppress logging
2130
+ * @param {boolean} [mouse=false] - Whether to include mouse cursor
2131
+ * @returns {Promise<string>} Base64 encoded PNG screenshot
2132
+ *
2133
+ * @example
2134
+ * // Capture a screenshot
2135
+ * const screenshot = await client.screenshot();
2136
+ * fs.writeFileSync('screenshot.png', Buffer.from(screenshot, 'base64'));
2137
+ *
2138
+ * @example
2139
+ * // Capture with mouse cursor visible
2140
+ * const screenshot = await client.screenshot(1, false, true);
2141
+ */
2142
+ async screenshot(scale = 1, silent = false, mouse = false) {
2143
+ this._ensureConnected();
2144
+ return await this.system.captureScreenBase64(scale, silent, mouse);
2145
+ }
2146
+
2147
+ /**
2148
+ * Ensure the SDK is connected before running commands
2149
+ * @private
2150
+ */
2151
+ _ensureConnected() {
2152
+ if (!this.connected) {
2153
+ throw new Error("SDK is not connected. Call connect() first.");
2154
+ }
2155
+ }
2156
+
2157
+ /**
2158
+ * Get the current sandbox instance details
2159
+ * @returns {Object|null} Sandbox instance
2160
+ */
2161
+ getInstance() {
2162
+ return this.instance;
2163
+ }
2164
+
2165
+ /**
2166
+ * Enable or disable logging output
2167
+ * @param {boolean} enabled - Whether to enable logging
2168
+ */
2169
+ setLogging(enabled) {
2170
+ this.loggingEnabled = enabled;
2171
+ if (enabled && !this._loggingSetup) {
2172
+ this._setupLogging();
2173
+ }
2174
+ }
2175
+
2176
+ /**
2177
+ * Get the event emitter for custom event handling
2178
+ * @returns {EventEmitter2} Event emitter
2179
+ */
2180
+ getEmitter() {
2181
+ return this.emitter;
2182
+ }
2183
+
2184
+ /**
2185
+ * Set test context for enhanced logging (integrates with Vitest)
2186
+ * @param {Object} context - Test context with file, test name, start time
2187
+ * @param {string} [context.file] - Current test file name
2188
+ * @param {string} [context.test] - Current test name
2189
+ * @param {number} [context.startTime] - Test start timestamp
2190
+ */
2191
+ setTestContext(context) {
2192
+ formatter.setTestContext(context);
2193
+ }
2194
+
2195
+ /**
2196
+ * Set up logging for the SDK
2197
+ * @private
2198
+ */
2199
+ _setupLogging() {
2200
+ // Set up markdown logger
2201
+ createMarkdownLogger(this.emitter);
2202
+
2203
+ // Set up basic event logging
2204
+ this.emitter.on("log:**", (message) => {
2205
+ const event = this.emitter.event;
2206
+ if (event === events.log.debug) return;
2207
+ if (this.loggingEnabled && message) {
2208
+ const prefixedMessage = this.testContext
2209
+ ? `[${this.testContext}] ${message}`
2210
+ : message;
2211
+ console.log(prefixedMessage);
2212
+
2213
+ // Also forward to sandbox for dashcam
2214
+ this._forwardLogToSandbox(prefixedMessage);
2215
+ }
2216
+ });
2217
+
2218
+ this.emitter.on("error:**", (data) => {
2219
+ if (this.loggingEnabled) {
2220
+ const event = this.emitter.event;
2221
+ console.error(event, ":", data);
2222
+ }
2223
+ });
2224
+
2225
+ this.emitter.on("status", (message) => {
2226
+ if (this.loggingEnabled) {
2227
+ console.log(`- ${message}`);
2228
+ }
2229
+ });
2230
+
2231
+ // Handle redraw status for debugging scroll and other async operations
2232
+ this.emitter.on("redraw:status", (status) => {
2233
+ if (this.loggingEnabled) {
2234
+ console.log(
2235
+ `[redraw] screen:${status.redraw.text} network:${status.network.text} timeout:${status.timeout.text}`,
2236
+ );
2237
+ }
2238
+ });
2239
+
2240
+ this.emitter.on("redraw:complete", (info) => {
2241
+ if (this.loggingEnabled) {
2242
+ console.log(
2243
+ `[redraw complete] screen:${info.screenHasRedrawn} network:${info.networkSettled} timeout:${info.isTimeout} elapsed:${info.timeElapsed}ms`,
2244
+ );
2245
+ }
2246
+ });
2247
+
2248
+ // Handle show window events for sandbox visualization
2249
+ this.emitter.on("show-window", async (url) => {
2250
+ if (this.loggingEnabled) {
2251
+ console.log("");
2252
+ console.log("Live test execution:");
2253
+ if (this.config.CI) {
2254
+ // In CI mode, just print the view-only URL
2255
+ const u = new URL(url);
2256
+ const encodedData = u.searchParams.get("data");
2257
+ // Data is base64 encoded, not URL encoded
2258
+ const data = JSON.parse(
2259
+ Buffer.from(encodedData, "base64").toString(),
2260
+ );
2261
+ console.log(`${data.url}&view_only=true`);
2262
+ } else {
2263
+ // In local mode, print the URL and open it in the browser
2264
+ console.log(url);
2265
+ await this._openBrowser(url);
2266
+ }
2267
+ }
2268
+ });
2269
+ }
2270
+
2271
+ /**
2272
+ * Forward log message to sandbox for debugger display
2273
+ * @private
2274
+ * @param {string} message - Log message to forward
2275
+ */
2276
+ _forwardLogToSandbox(message) {
2277
+ try {
2278
+ // Only forward if sandbox is connected
2279
+ if (this.sandbox && this.sandbox.instanceSocketConnected) {
2280
+ // Don't send objects as they cause base64 encoding errors
2281
+ if (typeof message === "object") {
2282
+ return;
2283
+ }
2284
+
2285
+ // Add test context prefix if available
2286
+ const prefixedMessage = this.testContext
2287
+ ? `[${this.testContext}] ${message}`
2288
+ : message;
2289
+
2290
+ this.sandbox.send({
2291
+ type: "output",
2292
+ output: Buffer.from(prefixedMessage).toString("base64"),
2293
+ });
2294
+ }
2295
+ } catch {
2296
+ // Silently fail to avoid breaking the log flow
2297
+ // console.error("Error forwarding log to sandbox:", error);
2298
+ }
2299
+ }
2300
+
2301
+ /**
2302
+ * Open URL in default browser
2303
+ * @private
2304
+ * @param {string} url - URL to open
2305
+ */
2306
+ async _openBrowser(url) {
2307
+ try {
2308
+ // Use dynamic import for the 'open' package (ES module)
2309
+ const { default: open } = await import("open");
2310
+
2311
+ // Open the browser
2312
+ await open(url, {
2313
+ wait: false,
2314
+ });
2315
+ } catch (error) {
2316
+ const { events } = require("./agent/events.js");
2317
+ this.emitter.emit(
2318
+ events.log.log,
2319
+ `Failed to open browser automatically: ${error.message}`,
2320
+ );
2321
+ this.emitter.emit(events.log.log, `Please manually open: ${url}`);
2322
+ }
2323
+ }
2324
+
2325
+ /**
2326
+ * Initialize debugger server
2327
+ * @private
2328
+ */
2329
+ async _initializeDebugger() {
2330
+ // Import createDebuggerProcess at the module level if not already done
2331
+ const { createDebuggerProcess } = require("./agent/lib/debugger.js");
2332
+
2333
+ // Only initialize once
2334
+ if (!this.agent.debuggerUrl) {
2335
+ const debuggerProcess = await createDebuggerProcess(
2336
+ this.config,
2337
+ this.emitter,
2338
+ );
2339
+ this.agent.debuggerUrl = debuggerProcess.url || null;
2340
+ }
2341
+ }
2342
+
2343
+ // ====================================
2344
+ // Test Recording Methods
2345
+ // ====================================
2346
+
2347
+ /**
2348
+ * Create a new test run to track test execution
2349
+ *
2350
+ * @param {Object} options - Test run configuration
2351
+ * @param {string} options.runId - Unique identifier for this test run
2352
+ * @param {string} options.suiteName - Name of the test suite
2353
+ * @param {string} [options.platform] - Platform (windows/mac/linux)
2354
+ * @param {string} [options.sandboxId] - Sandbox ID (auto-detected from session if not provided)
2355
+ * @param {Object} [options.ci] - CI/CD metadata
2356
+ * @param {Object} [options.git] - Git metadata
2357
+ * @param {Object} [options.env] - Environment metadata
2358
+ * @returns {Promise<Object>} Created test run
2359
+ *
2360
+ * @example
2361
+ * const testRun = await client.createTestRun({
2362
+ * runId: 'unique-run-id',
2363
+ * suiteName: 'My Test Suite',
2364
+ * platform: 'windows',
2365
+ * git: {
2366
+ * repo: 'myorg/myrepo',
2367
+ * branch: 'main',
2368
+ * commit: 'abc123'
2369
+ * }
2370
+ * });
2371
+ */
2372
+ async createTestRun(options) {
2373
+ this._ensureConnected();
2374
+
2375
+ const { createSDK } = require("./agent/lib/sdk.js");
2376
+ const sdk = createSDK(
2377
+ this.emitter,
2378
+ this.config,
2379
+ this.agent.sessionInstance,
2380
+ );
2381
+ await sdk.auth();
2382
+
2383
+ const platform = options.platform || this.config.TD_PLATFORM || "windows";
2384
+
2385
+ // Auto-detect sandbox ID from the active sandbox if not provided
2386
+ const sandboxId = options.sandboxId || this.agent?.sandbox?.id || null;
2387
+
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
+ }
2419
+
2420
+ const data = {
2421
+ runId: options.runId,
2422
+ suiteName: options.suiteName,
2423
+ platform,
2424
+ sandboxId: sandboxId,
2425
+ sessionId: sessionId,
2426
+ // CI/CD
2427
+ ciProvider: options.ci?.provider,
2428
+ ciRunId: options.ci?.runId,
2429
+ ciJobId: options.ci?.jobId,
2430
+ ciUrl: options.ci?.url,
2431
+ // Git
2432
+ repo: options.git?.repo,
2433
+ branch: options.git?.branch,
2434
+ commit: options.git?.commit,
2435
+ commitMessage: options.git?.commitMessage,
2436
+ author: options.git?.author,
2437
+ // Environment
2438
+ nodeVersion: options.env?.nodeVersion || process.version,
2439
+ testDriverVersion:
2440
+ options.env?.testDriverVersion || require("./package.json").version,
2441
+ vitestVersion: options.env?.vitestVersion,
2442
+ environment: options.env?.additional,
2443
+ };
2444
+
2445
+ const result = await sdk.req("/api/v1/testdriver/test-run-create", data);
2446
+ return result.data;
2447
+ }
2448
+
2449
+ /**
2450
+ * Complete a test run and update final statistics
2451
+ *
2452
+ * @param {Object} options - Test run completion data
2453
+ * @param {string} options.runId - Test run ID
2454
+ * @param {string} options.status - Final status (passed/failed/cancelled)
2455
+ * @param {number} [options.totalTests] - Total number of tests
2456
+ * @param {number} [options.passedTests] - Number of passed tests
2457
+ * @param {number} [options.failedTests] - Number of failed tests
2458
+ * @param {number} [options.skippedTests] - Number of skipped tests
2459
+ * @returns {Promise<Object>} Updated test run
2460
+ *
2461
+ * @example
2462
+ * await client.completeTestRun({
2463
+ * runId: 'unique-run-id',
2464
+ * status: 'passed',
2465
+ * totalTests: 10,
2466
+ * passedTests: 10,
2467
+ * failedTests: 0
2468
+ * });
2469
+ */
2470
+ async completeTestRun(options) {
2471
+ this._ensureConnected();
2472
+
2473
+ const { createSDK } = require("./agent/lib/sdk.js");
2474
+ const sdk = createSDK(
2475
+ this.emitter,
2476
+ this.config,
2477
+ this.agent.sessionInstance,
2478
+ );
2479
+ await sdk.auth();
2480
+
2481
+ const result = await sdk.req(
2482
+ "/api/v1/testdriver/test-run-complete",
2483
+ options,
2484
+ );
2485
+ return result.data;
2486
+ }
2487
+
2488
+ /**
2489
+ * Record a test case result
2490
+ *
2491
+ * @param {Object} options - Test case data
2492
+ * @param {string} options.runId - Test run ID
2493
+ * @param {string} options.testName - Name of the test
2494
+ * @param {string} options.testFile - Path to test file
2495
+ * @param {string} options.status - Test status (passed/failed/skipped/pending)
2496
+ * @param {string} [options.suiteName] - Test suite/describe block name
2497
+ * @param {number} [options.duration] - Test duration in ms
2498
+ * @param {string} [options.errorMessage] - Error message if failed
2499
+ * @param {string} [options.errorStack] - Error stack trace if failed
2500
+ * @param {string} [options.replayUrl] - Dashcam replay URL
2501
+ * @param {number} [options.replayStartTime] - Start time in replay
2502
+ * @param {number} [options.replayEndTime] - End time in replay
2503
+ * @returns {Promise<Object>} Created/updated test case
2504
+ *
2505
+ * @example
2506
+ * await client.recordTestCase({
2507
+ * runId: 'unique-run-id',
2508
+ * testName: 'should login successfully',
2509
+ * testFile: 'tests/login.test.js',
2510
+ * status: 'passed',
2511
+ * duration: 1500,
2512
+ * replayUrl: 'https://app.dashcam.io/replay/abc123'
2513
+ * });
2514
+ */
2515
+ async recordTestCase(options) {
2516
+ this._ensureConnected();
2517
+
2518
+ const { createSDK } = require("./agent/lib/sdk.js");
2519
+ const sdk = createSDK(
2520
+ this.emitter,
2521
+ this.config,
2522
+ this.agent.sessionInstance,
2523
+ );
2524
+ await sdk.auth();
2525
+
2526
+ const result = await sdk.req(
2527
+ "/api/v1/testdriver/test-case-create",
2528
+ options,
2529
+ );
2530
+ return result.data;
2531
+ }
2532
+
2533
+ // ====================================
2534
+ // AI Methods (Exploratory Loop)
2535
+ // ====================================
2536
+
2537
+ /**
2538
+ * Execute a natural language task using AI
2539
+ * This is the SDK equivalent of the CLI's exploratory loop
2540
+ *
2541
+ * @param {string} task - Natural language description of what to do
2542
+ * @param {Object} options - Execution options
2543
+ * @param {boolean} [options.validateAndLoop=false] - Whether to validate completion and retry if incomplete
2544
+ * @returns {Promise<string|void>} Final AI response if validateAndLoop is true
2545
+ *
2546
+ * @example
2547
+ * // Simple execution
2548
+ * await client.ai('Click the submit button');
2549
+ *
2550
+ * @example
2551
+ * // With validation loop
2552
+ * const result = await client.ai('Fill out the contact form', { validateAndLoop: true });
2553
+ * console.log(result); // AI's final assessment
2554
+ */
2555
+ async ai(task) {
2556
+ this._ensureConnected();
2557
+
2558
+ this.analytics.track("sdk.ai", { task });
2559
+
2560
+ // Use the agent's exploratoryLoop method directly
2561
+ return await this.agent.exploratoryLoop(task, false, true, false);
2562
+ }
2563
+ }
2564
+
2565
+ module.exports = TestDriverSDK;
2566
+ module.exports.Element = Element;
2567
+ module.exports.ElementNotFoundError = ElementNotFoundError;