@testdriverai/agent 7.8.0-test.38

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 (528) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.env.example +4 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc +1 -0
  5. package/CHANGELOG.md +953 -0
  6. package/README.md +81 -0
  7. package/agent/events.js +135 -0
  8. package/agent/index.js +2450 -0
  9. package/agent/interface.js +35 -0
  10. package/agent/lib/analytics.js +22 -0
  11. package/agent/lib/censorship.js +75 -0
  12. package/agent/lib/commander.js +246 -0
  13. package/agent/lib/commands.js +1684 -0
  14. package/agent/lib/config.js +60 -0
  15. package/agent/lib/generator.js +91 -0
  16. package/agent/lib/http.js +144 -0
  17. package/agent/lib/logger.js +56 -0
  18. package/agent/lib/outputs.js +29 -0
  19. package/agent/lib/parser.js +209 -0
  20. package/agent/lib/redraw.js +386 -0
  21. package/agent/lib/resources/cursor-2.png +0 -0
  22. package/agent/lib/sandbox.js +1104 -0
  23. package/agent/lib/sdk.js +633 -0
  24. package/agent/lib/session.js +25 -0
  25. package/agent/lib/source-mapper.js +342 -0
  26. package/agent/lib/subimage/index.js +77 -0
  27. package/agent/lib/subimage/opencv.js +69 -0
  28. package/agent/lib/system.js +204 -0
  29. package/agent/lib/theme.js +14 -0
  30. package/agent/lib/valid-version.js +21 -0
  31. package/agent/lib/validation.js +169 -0
  32. package/ai/.claude-plugin/plugin.json +9 -0
  33. package/ai/agents/testdriver.md +638 -0
  34. package/ai/skills/testdriver-ai/SKILL.md +204 -0
  35. package/ai/skills/testdriver-assert/SKILL.md +315 -0
  36. package/ai/skills/testdriver-aws-setup/SKILL.md +448 -0
  37. package/ai/skills/testdriver-cache/SKILL.md +221 -0
  38. package/ai/skills/testdriver-caching/SKILL.md +124 -0
  39. package/ai/skills/testdriver-captcha/SKILL.md +158 -0
  40. package/ai/skills/testdriver-ci-cd/SKILL.md +602 -0
  41. package/ai/skills/testdriver-click/SKILL.md +286 -0
  42. package/ai/skills/testdriver-client/SKILL.md +477 -0
  43. package/ai/skills/testdriver-cloud/SKILL.md +119 -0
  44. package/ai/skills/testdriver-customizing-devices/SKILL.md +319 -0
  45. package/ai/skills/testdriver-dashcam/SKILL.md +418 -0
  46. package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +401 -0
  47. package/ai/skills/testdriver-device-config/SKILL.md +317 -0
  48. package/ai/skills/testdriver-double-click/SKILL.md +102 -0
  49. package/ai/skills/testdriver-elements/SKILL.md +605 -0
  50. package/ai/skills/testdriver-enterprise/SKILL.md +114 -0
  51. package/ai/skills/testdriver-errors/SKILL.md +246 -0
  52. package/ai/skills/testdriver-events/SKILL.md +356 -0
  53. package/ai/skills/testdriver-examples/SKILL.md +7 -0
  54. package/ai/skills/testdriver-exec/SKILL.md +317 -0
  55. package/ai/skills/testdriver-find/SKILL.md +829 -0
  56. package/ai/skills/testdriver-focus-application/SKILL.md +293 -0
  57. package/ai/skills/testdriver-generating-tests/SKILL.md +36 -0
  58. package/ai/skills/testdriver-hover/SKILL.md +278 -0
  59. package/ai/skills/testdriver-locating-elements/SKILL.md +71 -0
  60. package/ai/skills/testdriver-making-assertions/SKILL.md +32 -0
  61. package/ai/skills/testdriver-mcp/SKILL.md +7 -0
  62. package/ai/skills/testdriver-mcp-workflow/SKILL.md +410 -0
  63. package/ai/skills/testdriver-mouse-down/SKILL.md +161 -0
  64. package/ai/skills/testdriver-mouse-up/SKILL.md +164 -0
  65. package/ai/skills/testdriver-parse/SKILL.md +236 -0
  66. package/ai/skills/testdriver-performing-actions/SKILL.md +54 -0
  67. package/ai/skills/testdriver-press-keys/SKILL.md +348 -0
  68. package/ai/skills/testdriver-provision/SKILL.md +331 -0
  69. package/ai/skills/testdriver-quickstart/SKILL.md +144 -0
  70. package/ai/skills/testdriver-redraw/SKILL.md +214 -0
  71. package/ai/skills/testdriver-reusable-code/SKILL.md +249 -0
  72. package/ai/skills/testdriver-right-click/SKILL.md +123 -0
  73. package/ai/skills/testdriver-running-tests/SKILL.md +185 -0
  74. package/ai/skills/testdriver-screenshot/SKILL.md +248 -0
  75. package/ai/skills/testdriver-screenshots/SKILL.md +184 -0
  76. package/ai/skills/testdriver-scroll/SKILL.md +335 -0
  77. package/ai/skills/testdriver-secrets/SKILL.md +115 -0
  78. package/ai/skills/testdriver-self-hosted/SKILL.md +65 -0
  79. package/ai/skills/testdriver-test-writer/SKILL.md +448 -0
  80. package/ai/skills/testdriver-testdriver/SKILL.md +628 -0
  81. package/ai/skills/testdriver-testdriver-mechanic/SKILL.md +165 -0
  82. package/ai/skills/testdriver-type/SKILL.md +357 -0
  83. package/ai/skills/testdriver-variables/SKILL.md +111 -0
  84. package/ai/skills/testdriver-wait/SKILL.md +50 -0
  85. package/ai/skills/testdriver-waiting-for-elements/SKILL.md +90 -0
  86. package/ai/skills/testdriver-what-is-testdriver/SKILL.md +54 -0
  87. package/bin/testdriverai.js +22 -0
  88. package/debugger/bg.png +0 -0
  89. package/debugger/icon.png +0 -0
  90. package/debugger/index.html +469 -0
  91. package/debugger/td.png +0 -0
  92. package/debugger/tray-buffered.png +0 -0
  93. package/debugger/tray.png +0 -0
  94. package/docs/GITHUB_COMMENTS.md +330 -0
  95. package/docs/GITHUB_COMMENTS_ANNOUNCEMENT.md +167 -0
  96. package/docs/QUICK-START-GITHUB-COMMENTS.md +84 -0
  97. package/docs/TEST-GITHUB-COMMENTS.md +129 -0
  98. package/docs/_data/examples-manifest.json +177 -0
  99. package/docs/_data/examples-manifest.schema.json +41 -0
  100. package/docs/_scripts/extract-example-urls.js +165 -0
  101. package/docs/_scripts/generate-examples.js +560 -0
  102. package/docs/_scripts/generate-skills.js +154 -0
  103. package/docs/_scripts/link-replacer.js +164 -0
  104. package/docs/_scripts/upload-docs-to-openai.js +284 -0
  105. package/docs/changelog.mdx +161 -0
  106. package/docs/claude-mcp-plugin.mdx +160 -0
  107. package/docs/docs.json +442 -0
  108. package/docs/github-integration-setup.md +266 -0
  109. package/docs/guide/best-practices-polling.mdx +174 -0
  110. package/docs/images/content/account/newprojectsettings.png +0 -0
  111. package/docs/images/content/account/projectpage.png +0 -0
  112. package/docs/images/content/account/projectreplays.png +0 -0
  113. package/docs/images/content/account/team-manage.png +0 -0
  114. package/docs/images/content/account/teampage.png +0 -0
  115. package/docs/images/content/extension/cursor.svg +1 -0
  116. package/docs/images/content/extension/vscode.svg +57 -0
  117. package/docs/images/content/extension/windsurf.svg +3 -0
  118. package/docs/images/content/parse/output.png +0 -0
  119. package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
  120. package/docs/images/content/side-by-side.png +0 -0
  121. package/docs/images/content/vscode/ide-full.png +0 -0
  122. package/docs/images/content/vscode/running.png +0 -0
  123. package/docs/images/content/vscode/v7-chat.png +0 -0
  124. package/docs/images/content/vscode/v7-choose-agent.png +0 -0
  125. package/docs/images/content/vscode/v7-full.png +0 -0
  126. package/docs/images/content/vscode/v7-onboarding.png +0 -0
  127. package/docs/images/content/vscode/vscode-2-assert.png +0 -0
  128. package/docs/images/content/vscode/vscode-agent-preview.png +0 -0
  129. package/docs/images/content/vscode/vscode-copilot-ask.png +0 -0
  130. package/docs/images/content/vscode/vscode-file-creation.png +0 -0
  131. package/docs/images/content/vscode/vscode-install.png +0 -0
  132. package/docs/images/content/vscode/vscode-overview.png +0 -0
  133. package/docs/images/content/vscode/vscode-setup-walkthrough.png +0 -0
  134. package/docs/images/content/vscode/vscode-stopchat.png +0 -0
  135. package/docs/images/content/vscode/vscode-stoptest.png +0 -0
  136. package/docs/images/content/vscode/vscode-tdservice.png +0 -0
  137. package/docs/images/content/vscode/vscode-test-output.png +0 -0
  138. package/docs/images/content/vscode/vscode-testhistory.png +0 -0
  139. package/docs/images/content/vscode/vscode-testpane-runtests.png +0 -0
  140. package/docs/images/content/vscode/vscode-testpane.png +0 -0
  141. package/docs/images/template/dark.png +0 -0
  142. package/docs/images/template/icon.png +0 -0
  143. package/docs/images/template/light.png +0 -0
  144. package/docs/snippets/calendar-link.mdx +4 -0
  145. package/docs/snippets/gitignore-warning.mdx +7 -0
  146. package/docs/snippets/lifecycle-warning.mdx +6 -0
  147. package/docs/snippets/test-prereqs.mdx +12 -0
  148. package/docs/snippets/tests/assert-replay.mdx +7 -0
  149. package/docs/snippets/tests/assert-yaml.mdx +8 -0
  150. package/docs/snippets/tests/exec-js-replay.mdx +7 -0
  151. package/docs/snippets/tests/exec-js-yaml.mdx +32 -0
  152. package/docs/snippets/tests/exec-shell-replay.mdx +7 -0
  153. package/docs/snippets/tests/exec-shell-yaml.mdx +15 -0
  154. package/docs/snippets/tests/hover-image-replay.mdx +7 -0
  155. package/docs/snippets/tests/hover-image-yaml.mdx +17 -0
  156. package/docs/snippets/tests/hover-text-replay.mdx +7 -0
  157. package/docs/snippets/tests/hover-text-with-description-replay.mdx +7 -0
  158. package/docs/snippets/tests/hover-text-with-description-yaml.mdx +24 -0
  159. package/docs/snippets/tests/hover-text-yaml.mdx +14 -0
  160. package/docs/snippets/tests/match-image-replay.mdx +7 -0
  161. package/docs/snippets/tests/match-image-yaml.mdx +17 -0
  162. package/docs/snippets/tests/press-keys-replay.mdx +7 -0
  163. package/docs/snippets/tests/press-keys-yaml.mdx +36 -0
  164. package/docs/snippets/tests/remember-replay.mdx +7 -0
  165. package/docs/snippets/tests/remember-yaml.mdx +28 -0
  166. package/docs/snippets/tests/scroll-replay.mdx +7 -0
  167. package/docs/snippets/tests/scroll-until-image-replay.mdx +7 -0
  168. package/docs/snippets/tests/scroll-until-image-yaml.mdx +14 -0
  169. package/docs/snippets/tests/scroll-until-text-replay.mdx +7 -0
  170. package/docs/snippets/tests/scroll-until-text-yaml.mdx +17 -0
  171. package/docs/snippets/tests/scroll-yaml.mdx +30 -0
  172. package/docs/snippets/tests/type-repeated-replay.mdx +7 -0
  173. package/docs/snippets/tests/type-repeated-yaml.mdx +22 -0
  174. package/docs/snippets/tests/type-replay.mdx +7 -0
  175. package/docs/snippets/tests/type-yaml.mdx +28 -0
  176. package/docs/snippets/tests/wait-for-image-replay.mdx +7 -0
  177. package/docs/snippets/tests/wait-for-image-yaml.mdx +18 -0
  178. package/docs/snippets/tests/wait-for-text-replay.mdx +7 -0
  179. package/docs/snippets/tests/wait-for-text-yaml.mdx +18 -0
  180. package/docs/snippets/tests/wait-replay.mdx +7 -0
  181. package/docs/snippets/tests/wait-yaml.mdx +13 -0
  182. package/docs/styles.css +65 -0
  183. package/docs/v6/account/dashboard.mdx +16 -0
  184. package/docs/v6/account/enterprise.mdx +110 -0
  185. package/docs/v6/account/pricing.mdx +33 -0
  186. package/docs/v6/account/projects.mdx +33 -0
  187. package/docs/v6/account/team.mdx +35 -0
  188. package/docs/v6/action/ami.mdx +109 -0
  189. package/docs/v6/action/performance.mdx +105 -0
  190. package/docs/v6/action/secrets.mdx +93 -0
  191. package/docs/v6/apps/chrome-extensions.mdx +48 -0
  192. package/docs/v6/apps/desktop-apps.mdx +93 -0
  193. package/docs/v6/apps/mobile-apps.mdx +26 -0
  194. package/docs/v6/apps/static-websites.mdx +54 -0
  195. package/docs/v6/apps/tauri-apps.mdx +361 -0
  196. package/docs/v6/bugs/jira.mdx +232 -0
  197. package/docs/v6/cli/overview.mdx +66 -0
  198. package/docs/v6/commands/assert.mdx +45 -0
  199. package/docs/v6/commands/exec.mdx +276 -0
  200. package/docs/v6/commands/focus-application.mdx +44 -0
  201. package/docs/v6/commands/hover-image.mdx +69 -0
  202. package/docs/v6/commands/hover-text.mdx +47 -0
  203. package/docs/v6/commands/if.mdx +53 -0
  204. package/docs/v6/commands/match-image.mdx +67 -0
  205. package/docs/v6/commands/press-keys.mdx +87 -0
  206. package/docs/v6/commands/remember.mdx +49 -0
  207. package/docs/v6/commands/run.mdx +44 -0
  208. package/docs/v6/commands/scroll-until-image.mdx +66 -0
  209. package/docs/v6/commands/scroll-until-text.mdx +60 -0
  210. package/docs/v6/commands/scroll.mdx +69 -0
  211. package/docs/v6/commands/type.mdx +45 -0
  212. package/docs/v6/commands/wait-for-image.mdx +54 -0
  213. package/docs/v6/commands/wait-for-text.mdx +48 -0
  214. package/docs/v6/commands/wait.mdx +45 -0
  215. package/docs/v6/exporting/junit.mdx +218 -0
  216. package/docs/v6/exporting/playwright.mdx +197 -0
  217. package/docs/v6/features/auto-healing.mdx +144 -0
  218. package/docs/v6/features/generation.mdx +116 -0
  219. package/docs/v6/features/parallel-testing.mdx +151 -0
  220. package/docs/v6/features/reusable-snippets.mdx +131 -0
  221. package/docs/v6/features/selectorless.mdx +80 -0
  222. package/docs/v6/features/visual-assertions.mdx +139 -0
  223. package/docs/v6/getting-started/ci.mdx +146 -0
  224. package/docs/v6/getting-started/cli.mdx +91 -0
  225. package/docs/v6/getting-started/editing.mdx +100 -0
  226. package/docs/v6/getting-started/playwright.mdx +342 -0
  227. package/docs/v6/getting-started/running.mdx +48 -0
  228. package/docs/v6/getting-started/self-hosting.mdx +408 -0
  229. package/docs/v6/getting-started/vscode.mdx +88 -0
  230. package/docs/v6/guide/assertions.mdx +189 -0
  231. package/docs/v6/guide/authentication.mdx +136 -0
  232. package/docs/v6/guide/code.mdx +65 -0
  233. package/docs/v6/guide/dashcam.mdx +118 -0
  234. package/docs/v6/guide/environment-variables.mdx +26 -0
  235. package/docs/v6/guide/lifecycle.mdx +242 -0
  236. package/docs/v6/guide/locating.mdx +141 -0
  237. package/docs/v6/guide/protips.mdx +43 -0
  238. package/docs/v6/guide/variables.mdx +143 -0
  239. package/docs/v6/guide/waiting.mdx +130 -0
  240. package/docs/v6/importing/csv.mdx +196 -0
  241. package/docs/v6/importing/gherkin.mdx +143 -0
  242. package/docs/v6/importing/jira.mdx +164 -0
  243. package/docs/v6/importing/testrail.mdx +162 -0
  244. package/docs/v6/integrations/electron.mdx +146 -0
  245. package/docs/v6/integrations/netlify.mdx +100 -0
  246. package/docs/v6/integrations/vercel.mdx +125 -0
  247. package/docs/v6/interactive/explore.mdx +99 -0
  248. package/docs/v6/interactive/run.mdx +52 -0
  249. package/docs/v6/interactive/save.mdx +63 -0
  250. package/docs/v6/overview/comparison.mdx +101 -0
  251. package/docs/v6/overview/faq.mdx +162 -0
  252. package/docs/v6/overview/performance.mdx +52 -0
  253. package/docs/v6/overview/quickstart.mdx +137 -0
  254. package/docs/v6/overview/what-is-testdriver.mdx +85 -0
  255. package/docs/v6/scenarios/ai-chatbot.mdx +28 -0
  256. package/docs/v6/scenarios/cookie-banner.mdx +32 -0
  257. package/docs/v6/scenarios/file-upload.mdx +33 -0
  258. package/docs/v6/scenarios/form-filling.mdx +32 -0
  259. package/docs/v6/scenarios/log-in.mdx +75 -0
  260. package/docs/v6/scenarios/pdf-generation.mdx +25 -0
  261. package/docs/v6/scenarios/spell-check.mdx +22 -0
  262. package/docs/v6/security/action.mdx +84 -0
  263. package/docs/v6/security/agent.mdx +73 -0
  264. package/docs/v6/security/platform.mdx +77 -0
  265. package/docs/v6/tutorials/advanced-test.mdx +81 -0
  266. package/docs/v6/tutorials/basic-test.mdx +45 -0
  267. package/docs/v7/_drafts/agents.mdx +843 -0
  268. package/docs/v7/_drafts/architecture.mdx +399 -0
  269. package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
  270. package/docs/v7/_drafts/awesome-logs-quick-ref.mdx +100 -0
  271. package/docs/v7/_drafts/best-practices.mdx +486 -0
  272. package/docs/v7/_drafts/caching-ai.mdx +215 -0
  273. package/docs/v7/_drafts/caching-selectors.mdx +424 -0
  274. package/docs/v7/_drafts/caching.mdx +366 -0
  275. package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
  276. package/docs/v7/_drafts/commands/assert.mdx +45 -0
  277. package/docs/v7/_drafts/commands/exec.mdx +276 -0
  278. package/docs/v7/_drafts/commands/focus-application.mdx +44 -0
  279. package/docs/v7/_drafts/commands/hover-image.mdx +69 -0
  280. package/docs/v7/_drafts/commands/hover-text.mdx +47 -0
  281. package/docs/v7/_drafts/commands/if.mdx +53 -0
  282. package/docs/v7/_drafts/commands/match-image.mdx +67 -0
  283. package/docs/v7/_drafts/commands/press-keys.mdx +87 -0
  284. package/docs/v7/_drafts/commands/remember.mdx +49 -0
  285. package/docs/v7/_drafts/commands/run.mdx +44 -0
  286. package/docs/v7/_drafts/commands/scroll-until-image.mdx +66 -0
  287. package/docs/v7/_drafts/commands/scroll-until-text.mdx +60 -0
  288. package/docs/v7/_drafts/commands/scroll.mdx +69 -0
  289. package/docs/v7/_drafts/commands/type.mdx +45 -0
  290. package/docs/v7/_drafts/commands/wait-for-image.mdx +54 -0
  291. package/docs/v7/_drafts/commands/wait-for-text.mdx +48 -0
  292. package/docs/v7/_drafts/commands/wait.mdx +45 -0
  293. package/docs/v7/_drafts/configuration.mdx +378 -0
  294. package/docs/v7/_drafts/contributing.mdx +174 -0
  295. package/docs/v7/_drafts/core.mdx +458 -0
  296. package/docs/v7/_drafts/dashcam-title-feature.mdx +89 -0
  297. package/docs/v7/_drafts/debugging.mdx +349 -0
  298. package/docs/v7/_drafts/error-handling.mdx +501 -0
  299. package/docs/v7/_drafts/faq.mdx +393 -0
  300. package/docs/v7/_drafts/hooks.mdx +360 -0
  301. package/docs/v7/_drafts/init-command.mdx +95 -0
  302. package/docs/v7/_drafts/installation.mdx +420 -0
  303. package/docs/v7/_drafts/migration.mdx +562 -0
  304. package/docs/v7/_drafts/observable.mdx +604 -0
  305. package/docs/v7/_drafts/playwright.mdx +342 -0
  306. package/docs/v7/_drafts/plugin-migration.mdx +220 -0
  307. package/docs/v7/_drafts/powerful.mdx +419 -0
  308. package/docs/v7/_drafts/presets.mdx +210 -0
  309. package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
  310. package/docs/v7/_drafts/prompt-cache.mdx +200 -0
  311. package/docs/v7/_drafts/provision.mdx +390 -0
  312. package/docs/v7/_drafts/quick-start-test-recording.mdx +214 -0
  313. package/docs/v7/_drafts/readme.mdx +135 -0
  314. package/docs/v7/_drafts/reports.mdx +414 -0
  315. package/docs/v7/_drafts/scalable.mdx +763 -0
  316. package/docs/v7/_drafts/screenshot.mdx +155 -0
  317. package/docs/v7/_drafts/sdk-awesome-logs.mdx +468 -0
  318. package/docs/v7/_drafts/sdk-browser-rendering.mdx +167 -0
  319. package/docs/v7/_drafts/sdk-migration.mdx +474 -0
  320. package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
  321. package/docs/v7/_drafts/self-hosting.mdx +369 -0
  322. package/docs/v7/_drafts/test-recording.mdx +382 -0
  323. package/docs/v7/_drafts/troubleshooting.mdx +526 -0
  324. package/docs/v7/_drafts/vitest-plugin.mdx +477 -0
  325. package/docs/v7/_drafts/vitest.mdx +535 -0
  326. package/docs/v7/_drafts/writing-tests.mdx +25 -0
  327. package/docs/v7/ai.mdx +205 -0
  328. package/docs/v7/assert.mdx +316 -0
  329. package/docs/v7/aws-setup.mdx +449 -0
  330. package/docs/v7/cache.mdx +223 -0
  331. package/docs/v7/caching.mdx +128 -0
  332. package/docs/v7/captcha.mdx +159 -0
  333. package/docs/v7/ci-cd.mdx +603 -0
  334. package/docs/v7/click.mdx +287 -0
  335. package/docs/v7/client.mdx +478 -0
  336. package/docs/v7/copilot/auto-healing.mdx +265 -0
  337. package/docs/v7/copilot/creating-tests.mdx +156 -0
  338. package/docs/v7/copilot/github.mdx +143 -0
  339. package/docs/v7/copilot/running-tests.mdx +149 -0
  340. package/docs/v7/copilot/setup.mdx +143 -0
  341. package/docs/v7/customizing-devices.mdx +319 -0
  342. package/docs/v7/dashcam.mdx +419 -0
  343. package/docs/v7/debugging-with-screenshots.mdx +402 -0
  344. package/docs/v7/device-config.mdx +317 -0
  345. package/docs/v7/double-click.mdx +102 -0
  346. package/docs/v7/elements.mdx +606 -0
  347. package/docs/v7/enterprise.mdx +9 -0
  348. package/docs/v7/errors.mdx +248 -0
  349. package/docs/v7/events.mdx +358 -0
  350. package/docs/v7/examples/ai.mdx +72 -0
  351. package/docs/v7/examples/assert.mdx +72 -0
  352. package/docs/v7/examples/captcha-api.mdx +92 -0
  353. package/docs/v7/examples/chrome-extension.mdx +132 -0
  354. package/docs/v7/examples/drag-and-drop.mdx +100 -0
  355. package/docs/v7/examples/element-not-found.mdx +67 -0
  356. package/docs/v7/examples/exec-output.mdx +85 -0
  357. package/docs/v7/examples/exec-pwsh.mdx +83 -0
  358. package/docs/v7/examples/focus-window.mdx +62 -0
  359. package/docs/v7/examples/hover-image.mdx +94 -0
  360. package/docs/v7/examples/hover-text.mdx +69 -0
  361. package/docs/v7/examples/installer.mdx +91 -0
  362. package/docs/v7/examples/launch-vscode-linux.mdx +101 -0
  363. package/docs/v7/examples/match-image.mdx +96 -0
  364. package/docs/v7/examples/press-keys.mdx +92 -0
  365. package/docs/v7/examples/scroll-keyboard.mdx +79 -0
  366. package/docs/v7/examples/scroll-until-image.mdx +81 -0
  367. package/docs/v7/examples/scroll-until-text.mdx +109 -0
  368. package/docs/v7/examples/scroll.mdx +81 -0
  369. package/docs/v7/examples/type.mdx +92 -0
  370. package/docs/v7/examples/windows-installer.mdx +89 -0
  371. package/docs/v7/exec.mdx +318 -0
  372. package/docs/v7/find.mdx +830 -0
  373. package/docs/v7/focus-application.mdx +294 -0
  374. package/docs/v7/generating-tests.mdx +36 -0
  375. package/docs/v7/hosted.mdx +158 -0
  376. package/docs/v7/hover.mdx +279 -0
  377. package/docs/v7/locating-elements.mdx +71 -0
  378. package/docs/v7/making-assertions.mdx +32 -0
  379. package/docs/v7/mcp.mdx +9 -0
  380. package/docs/v7/mouse-down.mdx +161 -0
  381. package/docs/v7/mouse-up.mdx +164 -0
  382. package/docs/v7/parse.mdx +237 -0
  383. package/docs/v7/performing-actions.mdx +54 -0
  384. package/docs/v7/press-keys.mdx +349 -0
  385. package/docs/v7/provision.mdx +333 -0
  386. package/docs/v7/quickstart.mdx +173 -0
  387. package/docs/v7/redraw.mdx +216 -0
  388. package/docs/v7/reusable-code.mdx +249 -0
  389. package/docs/v7/right-click.mdx +123 -0
  390. package/docs/v7/running-tests.mdx +185 -0
  391. package/docs/v7/screenshot.mdx +249 -0
  392. package/docs/v7/screenshots.mdx +186 -0
  393. package/docs/v7/scroll.mdx +336 -0
  394. package/docs/v7/secrets.mdx +115 -0
  395. package/docs/v7/self-hosted.mdx +149 -0
  396. package/docs/v7/type.mdx +358 -0
  397. package/docs/v7/variables.mdx +111 -0
  398. package/docs/v7/wait.mdx +52 -0
  399. package/docs/v7/waiting-for-elements.mdx +90 -0
  400. package/docs/v7/what-is-testdriver.mdx +54 -0
  401. package/eslint.config.js +67 -0
  402. package/examples/ai.test.mjs +31 -0
  403. package/examples/assert.test.mjs +47 -0
  404. package/examples/chrome-extension.test.mjs +97 -0
  405. package/examples/config.mjs +5 -0
  406. package/examples/element-not-found.test.mjs +27 -0
  407. package/examples/exec-output.test.mjs +60 -0
  408. package/examples/exec-pwsh.test.mjs +58 -0
  409. package/examples/findall-coffee-icons.test.mjs +42 -0
  410. package/examples/focus-window.test.mjs +37 -0
  411. package/examples/formatted-logging.test.mjs +27 -0
  412. package/examples/hover-image.test.mjs +53 -0
  413. package/examples/hover-text-with-description.test.mjs +57 -0
  414. package/examples/hover-text.test.mjs +28 -0
  415. package/examples/installer.test.mjs +50 -0
  416. package/examples/launch-vscode-linux.test.mjs +55 -0
  417. package/examples/match-image.test.mjs +55 -0
  418. package/examples/parse.test.mjs +19 -0
  419. package/examples/press-keys.test.mjs +44 -0
  420. package/examples/prompt.test.mjs +34 -0
  421. package/examples/scroll-keyboard.test.mjs +38 -0
  422. package/examples/scroll-until-image.test.mjs +40 -0
  423. package/examples/scroll.test.mjs +42 -0
  424. package/examples/type.test.mjs +46 -0
  425. package/examples/windows-installer.test.mjs +54 -0
  426. package/index.js +2 -0
  427. package/interfaces/cli/commands/init.js +438 -0
  428. package/interfaces/cli/commands/setup.js +382 -0
  429. package/interfaces/cli/lib/base.js +285 -0
  430. package/interfaces/cli.js +20 -0
  431. package/interfaces/junit-reporter.js +290 -0
  432. package/interfaces/logger.js +388 -0
  433. package/interfaces/readline.js +234 -0
  434. package/interfaces/shared-test-state.mjs +64 -0
  435. package/interfaces/vitest-plugin.d.ts +115 -0
  436. package/interfaces/vitest-plugin.mjs +1698 -0
  437. package/lib/captcha/solver.js +358 -0
  438. package/lib/core/Dashcam.js +533 -0
  439. package/lib/core/index.d.ts +172 -0
  440. package/lib/core/index.js +12 -0
  441. package/lib/environments.json +18 -0
  442. package/lib/github-comment-formatter.js +263 -0
  443. package/lib/github-comment.mjs +452 -0
  444. package/lib/init-project.js +575 -0
  445. package/lib/presets/index.mjs +331 -0
  446. package/lib/resolve-channel.js +46 -0
  447. package/lib/sentry.js +417 -0
  448. package/lib/vitest/hooks.d.ts +57 -0
  449. package/lib/vitest/hooks.mjs +674 -0
  450. package/lib/vitest/setup-aws.mjs +247 -0
  451. package/lib/vitest/setup-self-hosted.mjs +151 -0
  452. package/lib/vitest/setup.mjs +46 -0
  453. package/manual/captcha-api.test.mjs +51 -0
  454. package/manual/drag-and-drop.test.mjs +59 -0
  455. package/manual/flake-diffthreshold-001.test.mjs +9 -0
  456. package/manual/flake-diffthreshold-01.test.mjs +9 -0
  457. package/manual/flake-diffthreshold-05.test.mjs +9 -0
  458. package/manual/flake-noredraw-cache.test.mjs +9 -0
  459. package/manual/flake-noredraw-nocache.test.mjs +9 -0
  460. package/manual/flake-redraw-cache.test.mjs +9 -0
  461. package/manual/flake-redraw-nocache.test.mjs +9 -0
  462. package/manual/flake-rocket-match.test.mjs +30 -0
  463. package/manual/flake-shared.mjs +51 -0
  464. package/manual/no-provision.test.mjs +31 -0
  465. package/manual/packer-hover-image.test.mjs +176 -0
  466. package/manual/scroll-until-text.test.mjs +68 -0
  467. package/manual/test-init-command.js +223 -0
  468. package/mcp-server/README.md +322 -0
  469. package/mcp-server/dist/codegen.d.ts +9 -0
  470. package/mcp-server/dist/codegen.js +165 -0
  471. package/mcp-server/dist/mcp-app.html +114 -0
  472. package/mcp-server/dist/package.json +1 -0
  473. package/mcp-server/dist/provision-types.d.ts +290 -0
  474. package/mcp-server/dist/provision-types.js +174 -0
  475. package/mcp-server/dist/server.d.ts +6 -0
  476. package/mcp-server/dist/server.mjs +1925 -0
  477. package/mcp-server/dist/session.d.ts +85 -0
  478. package/mcp-server/dist/session.js +152 -0
  479. package/mcp-server/mcp-app.html +28 -0
  480. package/mcp-server/mcp-config.example.json +19 -0
  481. package/mcp-server/package-lock.json +4027 -0
  482. package/mcp-server/package.json +31 -0
  483. package/mcp-server/src/codegen.ts +189 -0
  484. package/mcp-server/src/mcp-app.css +360 -0
  485. package/mcp-server/src/mcp-app.ts +547 -0
  486. package/mcp-server/src/provision-types.ts +209 -0
  487. package/mcp-server/src/server.ts +2391 -0
  488. package/mcp-server/src/session.ts +194 -0
  489. package/mcp-server/tsconfig.json +16 -0
  490. package/mcp-server/vite.config.ts +23 -0
  491. package/package.json +158 -0
  492. package/schema.json +1046 -0
  493. package/scripts/generate-skills.js +94 -0
  494. package/sdk-log-formatter.js +1157 -0
  495. package/sdk.d.ts +1486 -0
  496. package/sdk.js +4336 -0
  497. package/setup/aws/cloudformation.yaml +463 -0
  498. package/setup/aws/disable-defender.sh +42 -0
  499. package/setup/aws/install-dev-runner.sh +79 -0
  500. package/setup/aws/spawn-runner.sh +289 -0
  501. package/test/captcha-solver.test.mjs +152 -0
  502. package/test/chrome-remote-debugging.test.mjs +66 -0
  503. package/test/duckduckgo/experiment.test.mjs +28 -0
  504. package/test/duckduckgo/setup.test.mjs +29 -0
  505. package/test/manual/debug-locate-response.js +82 -0
  506. package/test/manual/reconnect-provision.test.mjs +49 -0
  507. package/test/manual/test-console-logs.test.mjs +42 -0
  508. package/test/manual/test-find-api.js +73 -0
  509. package/test/manual/test-init.sh +54 -0
  510. package/test/manual/test-prompt-cache.js +97 -0
  511. package/test/manual/test-provision-auth.mjs +22 -0
  512. package/test/manual/test-sandbox-render.js +29 -0
  513. package/test/manual/test-sdk-methods.js +15 -0
  514. package/test/manual/test-sdk-refactor.js +53 -0
  515. package/test/manual/test-stack-trace.mjs +57 -0
  516. package/test/manual/verify-element-api.js +89 -0
  517. package/test/manual/verify-types.js +0 -0
  518. package/test/manual-unawaited-promise.test.mjs +31 -0
  519. package/vitest.config.mjs +58 -0
  520. package/vitest.runner.config.mjs +33 -0
  521. package/vscode-extension/.vscodeignore +12 -0
  522. package/vscode-extension/README.md +94 -0
  523. package/vscode-extension/media/icon.png +0 -0
  524. package/vscode-extension/package-lock.json +4126 -0
  525. package/vscode-extension/package.json +86 -0
  526. package/vscode-extension/src/extension.ts +829 -0
  527. package/vscode-extension/testdriverai-0.1.0.vsix +0 -0
  528. package/vscode-extension/tsconfig.json +16 -0
@@ -0,0 +1,674 @@
1
+ /**
2
+ * Vitest Hooks for TestDriver
3
+ *
4
+ * Provides lifecycle management for TestDriver in Vitest tests.
5
+ *
6
+ * @example
7
+ * import { TestDriver } from 'testdriverai/vitest/hooks';
8
+ *
9
+ * test('my test', async (context) => {
10
+ * const testdriver = TestDriver(context, { headless: true });
11
+ *
12
+ * await testdriver.provision.chrome({ url: 'https://example.com' });
13
+ * await testdriver.find('button').click();
14
+ * });
15
+ */
16
+
17
+ import chalk from "chalk";
18
+ import { createRequire } from "module";
19
+ import path from "path";
20
+ import { vi } from "vitest";
21
+ import TestDriverSDK from "../../sdk.js";
22
+
23
+ // Use createRequire to import CommonJS modules
24
+ const require = createRequire(import.meta.url);
25
+ const channelConfig = require("../../lib/resolve-channel.js");
26
+
27
+ /**
28
+ * Minimum required Vitest major version
29
+ */
30
+ const MINIMUM_VITEST_VERSION = 4;
31
+
32
+ /**
33
+ * Check that Vitest version meets minimum requirements
34
+ * @throws {Error} if Vitest version is below minimum or not installed
35
+ */
36
+ function checkVitestVersion() {
37
+ try {
38
+ const vitestPkg = require("vitest/package.json");
39
+ const version = vitestPkg.version;
40
+ const major = parseInt(version.split(".")[0], 10);
41
+
42
+ if (major < MINIMUM_VITEST_VERSION) {
43
+ throw new Error(
44
+ `TestDriver requires Vitest >= ${MINIMUM_VITEST_VERSION}.0.0, but found ${version}. ` +
45
+ `Please upgrade Vitest: npm install vitest@latest`,
46
+ );
47
+ }
48
+ } catch (err) {
49
+ if (err.code === "MODULE_NOT_FOUND") {
50
+ throw new Error(
51
+ "TestDriver requires Vitest to be installed. " +
52
+ "Please install it: npm install vitest@latest",
53
+ );
54
+ }
55
+ throw err;
56
+ }
57
+ }
58
+
59
+ // Check Vitest version at module load time
60
+ checkVitestVersion();
61
+
62
+ /**
63
+ * Singleton console spy that forwards logs to all active sandbox connections.
64
+ *
65
+ * When --sequence.concurrent is used, multiple tests run at the same time in
66
+ * the same worker process. The previous implementation called vi.spyOn on
67
+ * console.log once per test, stacking N mock layers deep. Every console.log
68
+ * then cascaded through all N layers — each one calling JSON.stringify inside
69
+ * forwardToSandbox — easily exceeding the call-stack limit for ≥ ~30 tests.
70
+ *
71
+ * This singleton intercepts the console methods exactly **once** and keeps a
72
+ * Set of active sandbox clients. Each log call is forwarded to every active
73
+ * client's sandbox in O(N) *flat* iterations instead of O(N) nested frames.
74
+ */
75
+ const _consoleSpy = {
76
+ /** @type {Set<import('../../sdk.js').default>} */
77
+ activeClients: new Set(),
78
+ installed: false,
79
+ /** Original (un-spied) console references, captured once. */
80
+ originals: /** @type {{ log: Function, error: Function, warn: Function, info: Function } | null} */ (null),
81
+ spies: /** @type {{ log: any, error: any, warn: any, info: any } | null} */ (null),
82
+ };
83
+
84
+ const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
85
+
86
+ /**
87
+ * Serialise console args to a single string for sandbox forwarding.
88
+ * Falls back to toString on circular/huge objects to avoid blowing out the
89
+ * stack inside JSON.stringify.
90
+ */
91
+ function serialiseConsoleArgs(args) {
92
+ return args
93
+ .map((arg) => {
94
+ if (typeof arg === "object" && arg !== null) {
95
+ try {
96
+ return JSON.stringify(arg, null, 2);
97
+ } catch {
98
+ // Circular reference or too deep — fall back safely
99
+ return String(arg);
100
+ }
101
+ }
102
+ return String(arg);
103
+ })
104
+ .join(" ");
105
+ }
106
+
107
+ /**
108
+ * Buffer a console message into every active client's local log buffer.
109
+ * Replaces the old forwardToAllSandboxes which sent data over websocket
110
+ * to the sandbox for dashcam file-log capture. Logs are now uploaded
111
+ * directly to S3 from the vitest client at cleanup time.
112
+ */
113
+ function bufferConsoleToClients(args, level) {
114
+ if (_consoleSpy.activeClients.size === 0) return;
115
+
116
+ const message = serialiseConsoleArgs(args);
117
+
118
+ for (const client of _consoleSpy.activeClients) {
119
+ if (client._logBuffer) {
120
+ client._logBuffer.push({
121
+ time: Date.now(),
122
+ line: message,
123
+ level: level || "log",
124
+ source: "console",
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Install the singleton console spy (idempotent).
132
+ * Must be called *after* Vitest has set up its own console interception so
133
+ * that the originals we capture are Vitest's wrappers (which feed the test
134
+ * reporter output).
135
+ */
136
+ function installConsoleSpy() {
137
+ // Check both installed flag AND that spies are still valid.
138
+ // Guards against a race where cleanupConsoleSpy restores mocks (setting
139
+ // installed=false) while a new test is starting up concurrently.
140
+ if (_consoleSpy.installed && _consoleSpy.spies) return;
141
+ _consoleSpy.installed = true;
142
+
143
+ // Capture originals once — these are whatever console methods look like
144
+ // right now (possibly already wrapped by Vitest's own reporter).
145
+ _consoleSpy.originals = {
146
+ log: console.log.bind(console),
147
+ error: console.error.bind(console),
148
+ warn: console.warn.bind(console),
149
+ info: console.info.bind(console),
150
+ };
151
+
152
+ const makeHandler = (originalFn, level) => (...args) => {
153
+ originalFn(...args); // Let Vitest's reporter capture the output
154
+ bufferConsoleToClients(args, level); // Buffer into local log store for S3 upload
155
+ };
156
+
157
+ _consoleSpy.spies = {
158
+ log: vi.spyOn(console, "log").mockImplementation(makeHandler(_consoleSpy.originals.log, "log")),
159
+ error: vi.spyOn(console, "error").mockImplementation(makeHandler(_consoleSpy.originals.error, "error")),
160
+ warn: vi.spyOn(console, "warn").mockImplementation(makeHandler(_consoleSpy.originals.warn, "warn")),
161
+ info: vi.spyOn(console, "info").mockImplementation(makeHandler(_consoleSpy.originals.info, "info")),
162
+ };
163
+
164
+ if (debugConsoleSpy) {
165
+ process.stdout.write("[DEBUG consoleSpy] Singleton console spy installed\n");
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Register a TestDriver client so its sandbox receives forwarded logs.
171
+ * @param {import('../../sdk.js').default} client - TestDriver client instance
172
+ * @param {string} taskId - Unique task identifier (for debug logging)
173
+ */
174
+ function setupConsoleSpy(client, taskId) {
175
+ if (debugConsoleSpy) {
176
+ process.stdout.write(`[DEBUG setupConsoleSpy] registering taskId: ${taskId}\n`);
177
+ }
178
+ installConsoleSpy();
179
+ _consoleSpy.activeClients.add(client);
180
+ }
181
+
182
+ /**
183
+ * Unregister a client so its sandbox no longer receives forwarded logs.
184
+ * When the last client is removed we restore the original console methods so
185
+ * the Vitest worker fork can exit cleanly (unreleased vi.spyOn mocks prevent
186
+ * the worker from shutting down, producing "Worker exited unexpectedly").
187
+ * If another test starts later (e.g. a retry), installConsoleSpy() will
188
+ * re-install the spy on demand.
189
+ * @param {import('../../sdk.js').default} client - TestDriver client instance
190
+ */
191
+ function cleanupConsoleSpy(client) {
192
+ _consoleSpy.activeClients.delete(client);
193
+
194
+ // Restore spies when no tests need them — allows clean worker exit
195
+ if (_consoleSpy.activeClients.size === 0 && _consoleSpy.spies) {
196
+ _consoleSpy.spies.log.mockRestore();
197
+ _consoleSpy.spies.error.mockRestore();
198
+ _consoleSpy.spies.warn.mockRestore();
199
+ _consoleSpy.spies.info.mockRestore();
200
+ _consoleSpy.spies = null;
201
+ _consoleSpy.originals = null;
202
+ _consoleSpy.installed = false;
203
+
204
+ if (debugConsoleSpy) {
205
+ process.stdout.write("[DEBUG cleanupConsoleSpy] All spies restored\n");
206
+ }
207
+ }
208
+
209
+ if (debugConsoleSpy) {
210
+ process.stdout.write(
211
+ `[DEBUG cleanupConsoleSpy] clients remaining: ${_consoleSpy.activeClients.size}\n`,
212
+ );
213
+ }
214
+ }
215
+
216
+ // Weak maps to store instances per test context
217
+ const testDriverInstances = new WeakMap();
218
+ const lifecycleHandlers = new WeakMap();
219
+
220
+ /**
221
+ * Upload buffered SDK + console logs directly to S3 via the existing Log system.
222
+ * Extracts the replayId from the dashcam URL, calls POST /api/v1/logs to create
223
+ * a Log record and get a presigned PUT URL, then uploads the JSONL payload.
224
+ *
225
+ * @param {import('../../sdk.js').default} client - TestDriver SDK instance
226
+ * @param {string|null} dashcamUrl - Dashcam replay URL from dashcam.stop()
227
+ */
228
+ async function uploadLogsToReplay(client, dashcamUrl) {
229
+ if (!dashcamUrl) return;
230
+
231
+ // Extract replayId from the dashcam URL (e.g. https://app.dashcam.io/replay/6789abcdef012345...)
232
+ const replayMatch = dashcamUrl.match(/replay\/([a-f0-9]{24})/);
233
+ if (!replayMatch) {
234
+ if (debugConsoleSpy) {
235
+ console.log("[uploadLogsToReplay] Could not extract replayId from:", dashcamUrl);
236
+ }
237
+ return;
238
+ }
239
+
240
+ const replayId = replayMatch[1];
241
+ const logData = client.getLogs();
242
+
243
+ if (!logData || logData.trim().length === 0) {
244
+ if (debugConsoleSpy) {
245
+ console.log("[uploadLogsToReplay] No logs to upload");
246
+ }
247
+ return;
248
+ }
249
+
250
+ try {
251
+ // Get TD_API_KEY for auth (prefer SDK config which is always correctly resolved)
252
+ const apiKey = client.config?.TD_API_KEY || process.env.TD_API_KEY;
253
+ if (!apiKey) {
254
+ console.warn("[TestDriver] TD_API_KEY not set, skipping log upload");
255
+ return;
256
+ }
257
+
258
+ // Use the SDK's configured API root (matches what the SDK uses for all other API calls)
259
+ const apiRoot = client.config?.TD_API_ROOT || process.env.TD_API_ROOT || channelConfig.channels[channelConfig.active];
260
+
261
+ console.log(`[TestDriver] Uploading logs for replay ${replayId} to ${apiRoot}...`);
262
+
263
+ const authRes = await fetch(`${apiRoot}/auth/exchange-api-key`, {
264
+ method: "POST",
265
+ headers: { "Content-Type": "application/json" },
266
+ body: JSON.stringify({ apiKey }),
267
+ });
268
+
269
+ if (!authRes.ok) {
270
+ console.warn("[TestDriver] Failed to exchange API key for log upload:", authRes.status);
271
+ return;
272
+ }
273
+
274
+ const authData = await authRes.json();
275
+ const token = authData.token;
276
+
277
+ // Create a Log record and get presigned upload URL
278
+ const logCreateRes = await fetch(`${apiRoot}/api/v1/logs`, {
279
+ method: "POST",
280
+ headers: {
281
+ "Content-Type": "application/json",
282
+ Authorization: `Bearer ${token}`,
283
+ },
284
+ body: JSON.stringify({
285
+ replayId,
286
+ appId: "testdriver-sdk",
287
+ name: "TestDriver Log",
288
+ type: "cli",
289
+ }),
290
+ });
291
+
292
+ if (!logCreateRes.ok) {
293
+ console.warn("[TestDriver] Failed to create log record:", logCreateRes.status);
294
+ return;
295
+ }
296
+
297
+ const logCreateData = await logCreateRes.json();
298
+ const uploadUrl = logCreateData.presignedUploadUrl;
299
+
300
+ if (!uploadUrl) {
301
+ console.warn("[TestDriver] No presigned upload URL returned from log-create");
302
+ return;
303
+ }
304
+
305
+ // Upload the JSONL log data directly to S3
306
+ const uploadRes = await fetch(uploadUrl, {
307
+ method: "PUT",
308
+ headers: { "Content-Type": "application/x-ndjson" },
309
+ body: logData,
310
+ });
311
+
312
+ if (!uploadRes.ok) {
313
+ console.warn("[TestDriver] Failed to upload logs to S3:", uploadRes.status);
314
+ return;
315
+ }
316
+
317
+ console.log(`[TestDriver] ✅ Logs uploaded successfully for replay: ${replayId}`);
318
+ } catch (err) {
319
+ // Fire-and-forget — don't let log upload failure break the test
320
+ console.warn("[TestDriver] Log upload failed:", err.message);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Create a TestDriver client in a Vitest test with automatic lifecycle management
326
+ *
327
+ * @param {import('vitest').TestContext} context - Vitest test context (from async (context) => {})
328
+ * @param {import('../../sdk.js').TestDriverOptions} [options] - TestDriver options (passed directly to TestDriver constructor)
329
+ * @returns {import('../../sdk.js').default} TestDriver client instance
330
+ *
331
+ * @example
332
+ * test('my test', async (context) => {
333
+ * const testdriver = TestDriver(context, { headless: true });
334
+ *
335
+ * // provision.chrome() automatically calls ready() and starts dashcam
336
+ * await testdriver.provision.chrome({ url: 'https://example.com' });
337
+ *
338
+ * await testdriver.find('Login button').click();
339
+ * });
340
+ */
341
+ export function TestDriver(context, options = {}) {
342
+ if (!context || !context.task) {
343
+ throw new Error(
344
+ 'TestDriver() requires Vitest context. Pass the context parameter from your test function: test("name", async (context) => { ... })',
345
+ );
346
+ }
347
+
348
+ // Return existing instance if already created for this test AND it's still connected
349
+ // On retry, the previous instance will be disconnected, so we need to create a new one
350
+ if (testDriverInstances.has(context.task)) {
351
+ const existingInstance = testDriverInstances.get(context.task);
352
+ if (existingInstance.connected) {
353
+ return existingInstance;
354
+ }
355
+ // Instance exists but is disconnected (likely a retry) - remove it and create fresh
356
+ testDriverInstances.delete(context.task);
357
+ lifecycleHandlers.delete(context.task);
358
+ }
359
+
360
+ // Get global plugin options if available
361
+ const pluginOptions =
362
+ globalThis.__testdriverPlugin?.state?.testDriverOptions || {};
363
+
364
+ // Merge options: plugin global options < test-specific options
365
+ const mergedOptions = { ...pluginOptions, ...options };
366
+
367
+ // Support TD_OS environment variable for specifying target OS (linux, mac, windows)
368
+ // Priority: test options > plugin options > environment variable > default (linux)
369
+ if (!mergedOptions.os && process.env.TD_OS) {
370
+ mergedOptions.os = process.env.TD_OS;
371
+ console.log(
372
+ `[testdriver] Set mergedOptions.os = ${mergedOptions.os} from TD_OS environment variable`,
373
+ );
374
+ }
375
+
376
+ // Use IP from context if set by setup-aws.mjs (or other setup files)
377
+ // Priority: test options > context.ip (from setup hooks)
378
+ if (!mergedOptions.ip && context.ip) {
379
+ mergedOptions.ip = context.ip;
380
+ console.log(
381
+ `[testdriver] Set mergedOptions.ip = ${mergedOptions.ip} from context.ip`,
382
+ );
383
+ }
384
+
385
+ // Use instanceId from context if set by setup-aws.mjs
386
+ // This allows the API to provision Ably credentials via SSM for direct connections
387
+ if (!mergedOptions.instanceId && context.instanceId) {
388
+ mergedOptions.instanceId = context.instanceId;
389
+ console.log(
390
+ `[testdriver] Set mergedOptions.instanceId = ${mergedOptions.instanceId} from context.instanceId`,
391
+ );
392
+ }
393
+
394
+ // Extract TestDriver-specific options
395
+ const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
396
+
397
+ // Build config for TestDriverSDK constructor
398
+ const config = { ...mergedOptions };
399
+ delete config.apiKey;
400
+
401
+ // Use TD_API_ROOT from environment if not provided in config
402
+ if (!config.apiRoot && process.env.TD_API_ROOT) {
403
+ config.apiRoot = process.env.TD_API_ROOT;
404
+ }
405
+
406
+ const testdriver = new TestDriverSDK(apiKey, config);
407
+ testdriver.__vitestContext = context.task;
408
+ testdriver._debugOnFailure = mergedOptions.debugOnFailure || false;
409
+ testDriverInstances.set(context.task, testdriver);
410
+
411
+ // Set platform metadata early so the reporter can show the correct OS from the start
412
+ if (!context.task.meta) {
413
+ context.task.meta = {};
414
+ }
415
+ const platform = mergedOptions.os || "linux";
416
+ const absolutePath =
417
+ context.task.file?.filepath || context.task.file?.name || "unknown";
418
+ const projectRoot = process.cwd();
419
+ const testFile =
420
+ absolutePath !== "unknown"
421
+ ? path.relative(projectRoot, absolutePath)
422
+ : absolutePath;
423
+
424
+ context.task.meta.platform = platform;
425
+ context.task.meta.testFile = testFile;
426
+ context.task.meta.testOrder = 0;
427
+
428
+ // Pass test file name to SDK for debugger display
429
+ testdriver.testFile = testFile;
430
+
431
+ const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
432
+
433
+ testdriver.__connectionPromise = (async () => {
434
+ if (debugConsoleSpy) {
435
+ console.log(
436
+ "[DEBUG] Before auth - sandbox.instanceSocketConnected:",
437
+ testdriver.sandbox?.instanceSocketConnected,
438
+ );
439
+ }
440
+
441
+ await testdriver.auth();
442
+ await testdriver.connect();
443
+
444
+ // Clear the connection promise now that we're connected
445
+ // This prevents deadlock when exec() is called below (exec() lazy-awaits __connectionPromise)
446
+ testdriver.__connectionPromise = null;
447
+
448
+ if (debugConsoleSpy) {
449
+ console.log(
450
+ "[DEBUG] After connect - sandbox.instanceSocketConnected:",
451
+ testdriver.sandbox?.instanceSocketConnected,
452
+ );
453
+ console.log(
454
+ "[DEBUG] After connect - sandbox.send:",
455
+ typeof testdriver.sandbox?.send,
456
+ );
457
+ }
458
+
459
+ // Set up console spy for local log buffering (always, regardless of dashcam)
460
+ setupConsoleSpy(testdriver, context.task.id);
461
+
462
+ // Note: We no longer create a log file on the sandbox or call dashcam.addFileLog().
463
+ // TestDriver logs are buffered locally in _logBuffer and uploaded directly to S3
464
+ // from the vitest client at cleanup time. This avoids the expensive path of
465
+ // forwarding logs over websocket → sandbox file → dashcam upload.
466
+ //
467
+ // Web log tracking and dashcam.start() are still handled by provision.chrome()
468
+ // This ensures addWebLog is called with the domain pattern BEFORE dashcam.start()
469
+ })();
470
+
471
+ // Register cleanup handler with dashcam.stop()
472
+ // We always register a new cleanup handler because on retry we need to clean up the new instance
473
+ const cleanup = async () => {
474
+ // Get the current instance from the WeakMap (not from closure)
475
+ // This ensures we clean up the correct instance on retries
476
+ const currentInstance = testDriverInstances.get(context.task);
477
+ if (!currentInstance) {
478
+ return; // Already cleaned up
479
+ }
480
+
481
+ // Check if debug-on-failure mode is enabled and test failed
482
+ const debugOnFailure = currentInstance._debugOnFailure;
483
+ const testFailed = context.task.result?.state === "fail";
484
+
485
+ if (debugOnFailure && testFailed) {
486
+ // Get sandbox ID before we skip cleanup
487
+ const instance = currentInstance.getInstance?.();
488
+ const sandboxId = instance?.sandboxId || instance?.instanceId;
489
+
490
+ console.log("");
491
+ console.log(
492
+ chalk.yellow(
493
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
494
+ ),
495
+ );
496
+ console.log(
497
+ chalk.yellow(" DEBUG MODE: Sandbox kept alive for debugging"),
498
+ );
499
+ console.log(
500
+ chalk.yellow(
501
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
502
+ ),
503
+ );
504
+ console.log("");
505
+ console.log(" To connect via MCP:");
506
+ console.log(
507
+ chalk.cyan(` session_start({ sandboxId: "${sandboxId}" })`),
508
+ );
509
+ console.log("");
510
+ console.log(chalk.dim(" Sandbox will expire in 5 minutes."));
511
+ console.log(
512
+ chalk.yellow(
513
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
514
+ ),
515
+ );
516
+ console.log("");
517
+
518
+ // Still stop dashcam to save the recording
519
+ if (currentInstance._dashcam && currentInstance._dashcam.recording) {
520
+ try {
521
+ await currentInstance.dashcam.stop();
522
+ } catch {
523
+ // Ignore dashcam stop errors in debug mode
524
+ }
525
+ }
526
+
527
+ // Clean up console spies
528
+ cleanupConsoleSpy(currentInstance);
529
+
530
+ // DO NOT disconnect or terminate - keep sandbox alive for debugging
531
+ return;
532
+ }
533
+
534
+ try {
535
+ // Ensure meta object exists
536
+ if (!context.task.meta) {
537
+ context.task.meta = {};
538
+ }
539
+
540
+ // Always set test metadata, even if dashcam never started or fails to stop
541
+ // This ensures the reporter can record test results even for early failures
542
+ const platform = currentInstance.os || "linux";
543
+ const absolutePath =
544
+ context.task.file?.filepath || context.task.file?.name || "unknown";
545
+ const projectRoot = process.cwd();
546
+ const testFile =
547
+ absolutePath !== "unknown"
548
+ ? path.relative(projectRoot, absolutePath)
549
+ : absolutePath;
550
+
551
+ // Set basic metadata that's always available
552
+ context.task.meta.platform = platform;
553
+ context.task.meta.testFile = testFile;
554
+ context.task.meta.testOrder = 0;
555
+ context.task.meta.sessionId = currentInstance.getSessionId?.() || null;
556
+
557
+ // Initialize dashcamUrls array for tracking per-attempt URLs (persists across retries)
558
+ if (!context.task.meta.dashcamUrls) {
559
+ context.task.meta.dashcamUrls = [];
560
+ }
561
+
562
+ // Determine the current attempt number (1-based)
563
+ const attemptNumber = context.task.meta.dashcamUrls.length + 1;
564
+ const isRetry = attemptNumber > 1;
565
+ const attemptLabel = isRetry ? ` (attempt ${attemptNumber})` : "";
566
+
567
+ // Stop dashcam if it was started - with timeout to prevent hanging
568
+ if (currentInstance._dashcam && currentInstance._dashcam.recording) {
569
+ try {
570
+ const dashcamUrl = await currentInstance.dashcam.stop();
571
+
572
+ // Track this attempt's URL in the per-attempt array
573
+ context.task.meta.dashcamUrls.push({
574
+ attempt: attemptNumber,
575
+ url: dashcamUrl || null,
576
+ sessionId: currentInstance.getSessionId?.() || null,
577
+ });
578
+
579
+ // Keep backward compatibility - last attempt's URL
580
+ context.task.meta.dashcamUrl = dashcamUrl || null;
581
+
582
+ // Also register in memory if plugin is available (for cross-process scenarios)
583
+ if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
584
+ globalThis.__testdriverPlugin.registerDashcamUrl(
585
+ context.task.id,
586
+ dashcamUrl,
587
+ platform,
588
+ attemptNumber,
589
+ );
590
+ }
591
+
592
+ // Always print the dashcam URL for each attempt so it's visible in logs
593
+ if (dashcamUrl) {
594
+ console.log("");
595
+ console.log(
596
+ "🎥" + chalk.yellow(` Dashcam URL${attemptLabel}`) + `: ${dashcamUrl}`,
597
+ );
598
+ console.log("");
599
+ }
600
+
601
+ // Upload buffered logs directly to S3 via the existing Log system.
602
+ // This replaces the old path of forwarding logs to the sandbox for dashcam capture.
603
+ await uploadLogsToReplay(currentInstance, dashcamUrl);
604
+ } catch (error) {
605
+ // Log more detailed error information for debugging
606
+ console.error(
607
+ "❌ Failed to stop dashcam:",
608
+ error.name || error.constructor?.name || "Error",
609
+ );
610
+ if (error.message) console.error(" Message:", error.message);
611
+ // NotFoundError during cleanup is expected if sandbox already terminated
612
+ if (
613
+ error.name === "NotFoundError" ||
614
+ error.responseData?.error === "NotFoundError"
615
+ ) {
616
+ console.log(
617
+ " ℹ️ Sandbox session already terminated - dashcam stop skipped",
618
+ );
619
+ }
620
+ // Mark as not recording to prevent retries
621
+ if (currentInstance._dashcam) {
622
+ currentInstance._dashcam.recording = false;
623
+ }
624
+ // Track failed attempt
625
+ context.task.meta.dashcamUrls.push({
626
+ attempt: attemptNumber,
627
+ url: null,
628
+ sessionId: currentInstance.getSessionId?.() || null,
629
+ error: error.message,
630
+ });
631
+ // Ensure dashcamUrl is set to null if stop failed
632
+ context.task.meta.dashcamUrl = null;
633
+ }
634
+ } else {
635
+ // No dashcam recording - still track the attempt
636
+ context.task.meta.dashcamUrls.push({
637
+ attempt: attemptNumber,
638
+ url: null,
639
+ sessionId: currentInstance.getSessionId?.() || null,
640
+ });
641
+ context.task.meta.dashcamUrl = null;
642
+ }
643
+
644
+ // Clean up console spies
645
+ cleanupConsoleSpy(currentInstance);
646
+
647
+ // Wait for connection to finish if it was initiated
648
+ if (currentInstance.__connectionPromise) {
649
+ await currentInstance.__connectionPromise.catch(() => {}); // Ignore connection errors during cleanup
650
+ }
651
+
652
+ // Disconnect with timeout
653
+ await Promise.race([
654
+ currentInstance.disconnect(),
655
+ new Promise((resolve) => setTimeout(resolve, 5000)), // 5s timeout for disconnect
656
+ ]);
657
+ } catch (error) {
658
+ console.error("Error disconnecting client:", error);
659
+ } finally {
660
+ // Terminate AWS instance if one was spawned for this test
661
+ // This must happen AFTER dashcam.stop() to ensure recording is saved
662
+ // AND it must happen even if disconnect() fails
663
+ if (globalThis.__testdriverAWS?.terminateInstance) {
664
+ await globalThis.__testdriverAWS.terminateInstance(context.task.id);
665
+ }
666
+ }
667
+ };
668
+ lifecycleHandlers.set(context.task, cleanup);
669
+
670
+ // Vitest will call this automatically after the test (each retry attempt)
671
+ context.onTestFinished?.(cleanup);
672
+
673
+ return testdriver;
674
+ }