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
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Repo-Specific Test Helpers
3
+ *
4
+ * These helpers are specific to this repository's test infrastructure.
5
+ * For reusable plugin helpers, import from 'testdriverai/vitest' (or src/vitest/index.mjs locally)
6
+ */
7
+
8
+ import crypto from "crypto";
9
+ import { config } from "dotenv";
10
+ import fs from "fs";
11
+ import os from "os";
12
+ import path, { dirname } from "path";
13
+ import { fileURLToPath } from "url";
14
+
15
+ // Import TestDriver SDK locally (for repo development)
16
+ import TestDriver from "../../../sdk.js";
17
+
18
+ // Import plugin helpers
19
+ import {
20
+ addDashcamLog,
21
+ authDashcam,
22
+ launchChrome,
23
+ launchChromeExtension,
24
+ launchChromeForTesting,
25
+ runPostrun,
26
+ runPrerun,
27
+ runPrerunChromeExtension,
28
+ runPrerunChromeForTesting,
29
+ startDashcam,
30
+ stopDashcam,
31
+ waitForPage,
32
+ } from "../../../src/vitest/lifecycle.mjs";
33
+
34
+ import {
35
+ retryAsync,
36
+ setupEventLogging,
37
+ sleep,
38
+ waitFor,
39
+ } from "../../../src/vitest/utils.mjs";
40
+
41
+ // Re-export plugin lifecycle helpers for backward compatibility
42
+ export {
43
+ addDashcamLog,
44
+ authDashcam,
45
+ launchChrome,
46
+ launchChromeExtension,
47
+ launchChromeForTesting,
48
+ runPostrun,
49
+ runPrerun,
50
+ runPrerunChromeExtension,
51
+ runPrerunChromeForTesting,
52
+ startDashcam,
53
+ stopDashcam,
54
+ waitForPage
55
+ };
56
+
57
+ // Re-export plugin utilities for backward compatibility
58
+ export {
59
+ retryAsync,
60
+ setupEventLogging,
61
+ sleep,
62
+ waitFor
63
+ };
64
+
65
+ // Get the directory of the current module
66
+ const __filename = fileURLToPath(import.meta.url);
67
+ const __dirname = dirname(__filename);
68
+
69
+ // Load environment variables from .env file in the project root
70
+ const envPath = path.resolve(__dirname, "../../../.env");
71
+ config({ path: envPath });
72
+
73
+ // Log loaded env vars for debugging
74
+ console.log("🔧 Environment variables loaded from:", envPath);
75
+ console.log(" TD_API_KEY:", process.env.TD_API_KEY ? "✓ Set" : "✗ Not set");
76
+ console.log(" TD_API_ROOT:", process.env.TD_API_ROOT || "Not set");
77
+ console.log(" TD_OS:", process.env.TD_OS || "Not set (will default to linux)");
78
+
79
+ // =============================================================================
80
+ // TEST RESULTS STORAGE (Repo-specific CI/CD integration)
81
+ // =============================================================================
82
+
83
+ const testResults = {
84
+ tests: [],
85
+ startTime: Date.now(),
86
+ };
87
+
88
+ /**
89
+ * Store test result with dashcam URL
90
+ * @param {string} testName - Name of the test
91
+ * @param {string} testFile - Test file path
92
+ * @param {string|null} dashcamUrl - Dashcam URL if available
93
+ * @param {Object} sessionInfo - Session information
94
+ */
95
+ export function storeTestResult(testName, testFile, dashcamUrl, sessionInfo = {}) {
96
+ console.log(`📝 Storing test result: ${testName}`);
97
+ console.log(` Dashcam URL: ${dashcamUrl || "none"}`);
98
+
99
+ let replayObjectId = null;
100
+ if (dashcamUrl) {
101
+ const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
102
+ replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
103
+ if (replayObjectId) {
104
+ console.log(` Replay Object ID: ${replayObjectId}`);
105
+ }
106
+ }
107
+
108
+ testResults.tests.push({
109
+ name: testName,
110
+ file: testFile,
111
+ dashcamUrl,
112
+ replayObjectId,
113
+ sessionId: sessionInfo.sessionId,
114
+ timestamp: new Date().toISOString(),
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Get all test results
120
+ * @returns {Object} All collected test results
121
+ */
122
+ export function getTestResults() {
123
+ return {
124
+ ...testResults,
125
+ endTime: Date.now(),
126
+ duration: Date.now() - testResults.startTime,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Save test results to a JSON file
132
+ * @param {string} outputPath - Path to save the results
133
+ */
134
+ export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
135
+ const results = getTestResults();
136
+ const dir = path.dirname(outputPath);
137
+
138
+ if (!fs.existsSync(dir)) {
139
+ fs.mkdirSync(dir, { recursive: true });
140
+ }
141
+
142
+ fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
143
+ console.log(`\n📊 Test results saved to: ${outputPath}`);
144
+
145
+ console.log("\n🎥 Dashcam URLs:");
146
+ results.tests.forEach((test) => {
147
+ if (test.dashcamUrl) {
148
+ console.log(` ${test.name}: ${test.dashcamUrl}`);
149
+ }
150
+ });
151
+
152
+ return results;
153
+ }
154
+
155
+ // =============================================================================
156
+ // REPO-SPECIFIC CLIENT CREATION
157
+ // =============================================================================
158
+
159
+ /**
160
+ * Create a configured TestDriver client for this repo's tests
161
+ * Uses local SDK path for development
162
+ * @param {Object} options - Additional options
163
+ * @returns {TestDriver} Configured client
164
+ */
165
+ export function createTestClient(options = {}) {
166
+ if (!process.env.TD_API_KEY) {
167
+ console.error("\n❌ Error: TD_API_KEY is not set!");
168
+ console.error("Please set it in one of the following ways:");
169
+ console.error(" 1. Create a .env file in the project root with: TD_API_KEY=your_key");
170
+ console.error(" 2. Pass it as an environment variable: TD_API_KEY=your_key npm run test:sdk");
171
+ console.error(" 3. Export it in your shell: export TD_API_KEY=your_key\n");
172
+ throw new Error("TD_API_KEY environment variable is required");
173
+ }
174
+
175
+ const osConfig = process.env.TEST_PLATFORM || "linux";
176
+ const { task, ...clientOptions } = options;
177
+ const taskId = task?.id || task?.name || null;
178
+
179
+ const client = new TestDriver(process.env.TD_API_KEY, {
180
+ resolution: "1366x768",
181
+ analytics: true,
182
+ os: osConfig,
183
+ apiKey: process.env.TD_API_KEY,
184
+ apiRoot: process.env.TD_API_ROOT || "https://testdriver-api.onrender.com",
185
+ newSandbox: true,
186
+ ...clientOptions,
187
+ });
188
+
189
+ console.log("🔧 createTestClient: SDK created, cacheThresholds =", client.cacheThresholds);
190
+ console.log(`[TestHelpers] Client OS configured as: ${client.os}`);
191
+
192
+ if (taskId) {
193
+ console.log(`[TestHelpers] Storing task ID on client: ${taskId}`);
194
+ client.vitestTaskId = taskId;
195
+ }
196
+
197
+ if (process.env.DEBUG_EVENTS === "true") {
198
+ setupEventLogging(client);
199
+ }
200
+
201
+ return client;
202
+ }
203
+
204
+ // =============================================================================
205
+ // REPO-SPECIFIC LOGIN HELPER (for TestDriver Sandbox)
206
+ // =============================================================================
207
+
208
+ /**
209
+ * Perform login flow on TestDriver Sandbox
210
+ * This is specific to http://testdriver-sandbox.vercel.app/login
211
+ * @param {TestDriver} client - TestDriver client
212
+ * @param {string} username - Username (default: 'standard_user')
213
+ * @param {string} password - Password (default: retrieved from screen)
214
+ */
215
+ export async function performLogin(client, username = "standard_user", password = null) {
216
+ await client.focusApplication("Google Chrome");
217
+
218
+ if (!password) {
219
+ password = await client.remember("the password");
220
+ }
221
+
222
+ const usernameField = await client.find(
223
+ "Username, label above the username input field on the login form",
224
+ );
225
+ await usernameField.click();
226
+ await client.type(username);
227
+
228
+ await client.pressKeys(["tab"]);
229
+ await client.type(password, { secret: true });
230
+
231
+ await client.pressKeys(["tab"]);
232
+ await client.pressKeys(["enter"]);
233
+ }
234
+
235
+ // =============================================================================
236
+ // SUITE TEST RUN MANAGEMENT (CI/CD Integration)
237
+ // =============================================================================
238
+
239
+ /**
240
+ * Initialize a test run for the entire suite
241
+ * @param {Object} suiteTask - Vitest suite task context
242
+ * @returns {Promise<Object>} Test run info { runId, testRunDbId, token }
243
+ */
244
+ export async function initializeSuiteTestRun(suiteTask) {
245
+ const apiKey = process.env.TD_API_KEY;
246
+ const apiRoot = process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
247
+
248
+ if (!apiKey || !globalThis.__testdriverPlugin) {
249
+ console.log(`[TestHelpers] Skipping suite test run initialization - no API key or plugin`);
250
+ return null;
251
+ }
252
+
253
+ const existingRun = globalThis.__testdriverPlugin.getSuiteTestRun(suiteTask.id);
254
+ if (existingRun) {
255
+ console.log(`[TestHelpers] Test run already exists for suite: ${existingRun.runId}`);
256
+ return existingRun;
257
+ }
258
+
259
+ try {
260
+ console.log(`[TestHelpers] Initializing test run for suite: ${suiteTask.name}`);
261
+
262
+ const token = await globalThis.__testdriverPlugin.authenticateWithApiKey(apiKey, apiRoot);
263
+ console.log(`[TestHelpers] ✅ Authenticated for suite`);
264
+
265
+ const runId = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
266
+ const testFile = suiteTask.file?.name || "unknown";
267
+ const testRunData = {
268
+ runId,
269
+ suiteName: suiteTask.name || testFile,
270
+ };
271
+
272
+ const testRunResponse = await globalThis.__testdriverPlugin.createTestRunDirect(
273
+ token,
274
+ apiRoot,
275
+ testRunData,
276
+ );
277
+ const testRunDbId = testRunResponse.data?.id;
278
+
279
+ const runInfo = { runId, testRunDbId, token };
280
+ globalThis.__testdriverPlugin.setSuiteTestRun(suiteTask.id, runInfo);
281
+
282
+ process.env.TD_TEST_RUN_ID = runId;
283
+ process.env.TD_TEST_RUN_DB_ID = testRunDbId;
284
+ process.env.TD_TEST_RUN_TOKEN = token;
285
+
286
+ console.log(`[TestHelpers] ✅ Created test run for suite: ${runId} (DB ID: ${testRunDbId})`);
287
+ return runInfo;
288
+ } catch (error) {
289
+ console.error(`[TestHelpers] ❌ Failed to initialize suite test run:`, error.message);
290
+ return null;
291
+ }
292
+ }
293
+
294
+ // =============================================================================
295
+ // SETUP/TEARDOWN HELPERS
296
+ // =============================================================================
297
+
298
+ /**
299
+ * Setup function to run before each test
300
+ * @param {TestDriver} client - TestDriver client
301
+ * @param {Object} options - Connection options
302
+ * @returns {Promise<Object>} Sandbox instance
303
+ */
304
+ export async function setupTest(client, options = {}) {
305
+ await client.auth();
306
+ const instance = await client.connect({ ...options });
307
+
308
+ if (options.prerun !== false) {
309
+ await runPrerun(client);
310
+ }
311
+
312
+ return instance;
313
+ }
314
+
315
+ /**
316
+ * Teardown function to run after each test
317
+ * @param {TestDriver} client - TestDriver client
318
+ * @param {Object} options - Teardown options
319
+ * @returns {Promise<Object>} Session info including dashcam URL
320
+ */
321
+ export async function teardownTest(client, options = {}) {
322
+ let dashcamUrl = options.dashcamUrl || null;
323
+
324
+ console.log("🧹 Running teardown...");
325
+
326
+ try {
327
+ if (options.postrun !== false && !dashcamUrl) {
328
+ dashcamUrl = await runPostrun(client);
329
+
330
+ if (dashcamUrl && options.task) {
331
+ const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
332
+ const replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
333
+
334
+ console.log(`🎥 Dashcam URL: ${dashcamUrl}`);
335
+ if (replayObjectId) {
336
+ console.log(`📝 Replay Object ID: ${replayObjectId}`);
337
+ }
338
+
339
+ options.task.meta.testdriverDashcamUrl = dashcamUrl;
340
+ options.task.meta.testdriverReplayObjectId = replayObjectId;
341
+ console.log(`[TestHelpers] ✅ Stored dashcam URL in task.meta for test: ${options.task.name}`);
342
+ }
343
+ } else {
344
+ console.log("⏭️ Postrun skipped (disabled in options)");
345
+ }
346
+
347
+ // Write test result to temp file for reporter
348
+ if (options.task) {
349
+ const testResultFile = path.join(
350
+ os.tmpdir(),
351
+ "testdriver-results",
352
+ `${options.task.id}.json`,
353
+ );
354
+
355
+ try {
356
+ const dir = path.dirname(testResultFile);
357
+ if (!fs.existsSync(dir)) {
358
+ fs.mkdirSync(dir, { recursive: true });
359
+ }
360
+
361
+ const testFile = options.task.file?.filepath || options.task.file?.name || "unknown";
362
+ let testOrder = 0;
363
+ if (options.task.suite && options.task.suite.tasks) {
364
+ testOrder = options.task.suite.tasks.indexOf(options.task);
365
+ }
366
+
367
+ const result = options.task.result?.();
368
+ const duration = result?.duration || 0;
369
+
370
+ const testResult = {
371
+ testId: options.task.id,
372
+ testName: options.task.name,
373
+ testFile: testFile,
374
+ testOrder: testOrder,
375
+ dashcamUrl: dashcamUrl,
376
+ replayObjectId: dashcamUrl ? dashcamUrl.match(/\/replay\/([^?]+)/)?.[1] : null,
377
+ platform: client.os,
378
+ timestamp: Date.now(),
379
+ duration: duration,
380
+ };
381
+
382
+ fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
383
+ console.log(`[TestHelpers] ✅ Wrote test result to file: ${testResultFile}`);
384
+ } catch (error) {
385
+ console.error(`[TestHelpers] ❌ Failed to write test result file:`, error.message);
386
+ }
387
+ }
388
+ } catch (error) {
389
+ console.error("❌ Error in postrun:", error);
390
+ console.error("❌ Error stack:", error.stack);
391
+ } finally {
392
+ if (options.disconnect !== false) {
393
+ console.log("🔌 Disconnecting client...");
394
+ try {
395
+ await client.disconnect();
396
+ console.log("✅ Client disconnected");
397
+ } catch (disconnectError) {
398
+ console.error("❌ Error disconnecting:", disconnectError.message);
399
+ }
400
+ } else {
401
+ console.log("⏭️ Disconnect skipped (disabled in options)");
402
+ }
403
+ }
404
+
405
+ let replayObjectId = null;
406
+ if (dashcamUrl) {
407
+ const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
408
+ replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
409
+ }
410
+
411
+ const sessionInfo = {
412
+ sessionId: client.getSessionId(),
413
+ dashcamUrl: dashcamUrl,
414
+ replayObjectId: replayObjectId,
415
+ instance: client.getInstance(),
416
+ };
417
+
418
+ console.log("📊 Session info:", JSON.stringify(sessionInfo, null, 2));
419
+ return sessionInfo;
420
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Vitest Setup File
3
+ * Runs once before all tests in each worker process
4
+ * This ensures the TestDriver plugin global state is available in test processes
5
+ */
6
+
7
+ // Import the plugin functions
8
+ import {
9
+ authenticateWithApiKey,
10
+ clearDashcamUrls,
11
+ clearSuiteTestRun,
12
+ createTestRunDirect,
13
+ getDashcamUrl,
14
+ getPluginState,
15
+ getSuiteTestRun,
16
+ pluginState,
17
+ recordTestCaseDirect,
18
+ registerDashcamUrl,
19
+ setSuiteTestRun,
20
+ } from "../../../interfaces/vitest-plugin.mjs";
21
+
22
+ // Make the plugin API available globally in the test worker process
23
+ if (typeof globalThis !== "undefined") {
24
+ globalThis.__testdriverPlugin = {
25
+ registerDashcamUrl,
26
+ getDashcamUrl,
27
+ clearDashcamUrls,
28
+ authenticateWithApiKey,
29
+ createTestRunDirect,
30
+ recordTestCaseDirect,
31
+ getSuiteTestRun,
32
+ setSuiteTestRun,
33
+ clearSuiteTestRun,
34
+ getPluginState,
35
+ state: pluginState,
36
+ };
37
+ console.log(
38
+ "[Vitest Setup] TestDriver plugin API initialized in worker process",
39
+ );
40
+ }
@@ -0,0 +1,234 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { chrome } from "../../src/presets/index.mjs";
3
+
4
+ /**
5
+ * CHECKPOINT SYSTEM GUIDE FOR TESTDRIVER TESTS
6
+ * ============================================
7
+ *
8
+ * When newSandbox is set to false in chrome(), the sandbox persists between test runs.
9
+ * This allows you to skip completed steps when debugging or developing tests.
10
+ *
11
+ * HOW TO USE CHECKPOINTS:
12
+ *
13
+ * 1. Run your test normally until it fails at a certain step
14
+ * 2. Find the checkpoint comment just before the failing section (e.g., "// CHECKPOINT: Login Complete")
15
+ * 3. Comment out all code BEFORE that checkpoint using /* ... *\/ block comments
16
+ * 4. Re-run the test - it will resume from that checkpoint using the existing sandbox state
17
+ * 5. Once the test passes completely, uncomment all sections to verify end-to-end
18
+ *
19
+ * CHECKPOINT LOCATIONS IN THIS TEST:
20
+ * - CHECKPOINT 1: Cookie banner handled
21
+ * - CHECKPOINT 2: Login complete
22
+ * - CHECKPOINT 3: Patients page loaded
23
+ * - CHECKPOINT 4: Patient selected/created
24
+ * - CHECKPOINT 5: Patient profile opened
25
+ *
26
+ * IMPORTANT: Always set newSandbox: false in chrome() options to enable this feature!
27
+ *
28
+ * FOR AI ASSISTANTS:
29
+ * When helping users debug TestDriver tests:
30
+ * - If a test fails at a certain step, suggest commenting out earlier checkpoints
31
+ * - Remind them to set newSandbox: false
32
+ * - After fixes, remind them to uncomment all code for full end-to-end validation
33
+ */
34
+
35
+ // Helper function to poll for an element until it's found
36
+ async function waitForElement(testdriver, description, maxAttempts = 10, delayMs = 1000) {
37
+ for (let i = 0; i < maxAttempts; i++) {
38
+ try {
39
+ const element = await testdriver.find(description);
40
+ if (element.found()) {
41
+ return element;
42
+ }
43
+ } catch (e) {
44
+ if (i === maxAttempts - 1) throw e;
45
+ }
46
+ await new Promise(resolve => setTimeout(resolve, delayMs));
47
+ }
48
+ throw new Error(`Element not found after ${maxAttempts} attempts: ${description}`);
49
+ }
50
+
51
+ describe("Sully.ai Patient Management Workflow", () => {
52
+ it("should complete full patient workflow: login, view patients, update note, and edit profile", async (context) => {
53
+ const { testdriver } = await chrome(context, {
54
+ url: 'https://app.sully.ai',
55
+ redrawThreshold: { enabled: true }
56
+ });
57
+
58
+ // ========================================================================
59
+ // CHECKPOINT 1: Cookie Banner
60
+ // If test fails after this point, comment out this section (lines above)
61
+ // ========================================================================
62
+
63
+ // Handle cookie banner if present
64
+ try {
65
+ const acceptCookies = await testdriver.find("Accept All button for cookies");
66
+ await acceptCookies.click();
67
+ } catch {
68
+ console.log("No cookie banner or already dismissed");
69
+ }
70
+
71
+ // Wait for login page to load by polling for email field
72
+ console.log("Waiting for login page to load...");
73
+ const emailField = await waitForElement(testdriver, "Email input field");
74
+ console.log("Found email field");
75
+ await emailField.click();
76
+ await testdriver.type("razeen+testdriver@sully.ai");
77
+
78
+ const passwordField = await testdriver.find("Password input field");
79
+ await passwordField.click();
80
+ await testdriver.type("plmokn7@A", {secret: true});
81
+
82
+ const loginButton = await testdriver.find("Login button");
83
+ await loginButton.click();
84
+
85
+ // ========================================================================
86
+ // CHECKPOINT 2: Login Complete
87
+ // If test fails after this point, comment out all code above this checkpoint
88
+ // ========================================================================
89
+
90
+ // Wait for navigation to complete after login
91
+ console.log("Waiting for login to process...");
92
+ await new Promise(resolve => setTimeout(resolve, 5000));
93
+
94
+ // Take a screenshot to see where we are
95
+ await testdriver.screenshot();
96
+ console.log("Screenshot taken after login");
97
+
98
+ // Verify login successful
99
+ const loginSuccess = await testdriver.assert("user is logged in and on the main application page or dashboard");
100
+ expect(loginSuccess).toBeTruthy();
101
+
102
+ // Poll for dashboard element to ensure login completed
103
+ console.log("Looking for Patients link...");
104
+ await waitForElement(testdriver, "Patients button or link in the navigation menu", 15, 1000);
105
+
106
+ // Dismiss password save dialog first if it appears
107
+ try {
108
+ const neverButton = await testdriver.find("Never button or Never save button");
109
+ await neverButton.click();
110
+ console.log("Dismissed password save dialog");
111
+ } catch {
112
+ console.log("Password save dialog not found");
113
+ }
114
+
115
+ await testdriver.find("save profile changes popup or banner").click();
116
+
117
+ // Click Patients
118
+ const patientsLink = await testdriver.find("Patients navigation link or menu item");
119
+ await patientsLink.click();
120
+
121
+ // ========================================================================
122
+ // CHECKPOINT 3: Patients Page Loaded
123
+ // If test fails after this point, comment out all code above this checkpoint
124
+ // ========================================================================
125
+
126
+ // Approach 1: Search for existing patient
127
+ const existingPatientInput = await testdriver.find("Patient search input box");
128
+ await existingPatientInput.click();
129
+
130
+ // Type a search term to find a patient
131
+ await testdriver.type("Patient Test");
132
+
133
+ // Poll for search results to appear
134
+ const firstPatient = await waitForElement(testdriver, "first patient name in the dropdown or list", 5, 500);
135
+ await firstPatient.click();
136
+
137
+ console.log("Found patient via search");
138
+
139
+ // Verify patient visit page or details are loaded
140
+ const patientLoaded = await testdriver.assert("we are on a patient recording page");
141
+ expect(patientLoaded).toBeTruthy();
142
+
143
+ // ========================================================================
144
+ // CHECKPOINT 4: Patient Selected/Created
145
+ // If test fails after this point, comment out all code above this checkpoint
146
+ // ========================================================================
147
+
148
+ await testdriver.find('input box at the bottom of the screen').click();
149
+ await testdriver.type("This is a test note added by TestDriver automation.");
150
+
151
+ // Regenerate note - click make note more concise
152
+ console.log("Looking for note regeneration option...");
153
+ try {
154
+ const makeNoteConcise = await testdriver.find("Make note more concise button or option");
155
+ await makeNoteConcise.click();
156
+ console.log("Clicked make note more concise");
157
+
158
+ // Wait for note to regenerate
159
+ await new Promise(resolve => setTimeout(resolve, 3000));
160
+
161
+ // Verify note regeneration completed
162
+ const noteRegenerated = await testdriver.assert("note has been regenerated or updated");
163
+ expect(noteRegenerated).toBeTruthy();
164
+ } catch {
165
+ console.log("Could not find or click make note more concise option");
166
+ }
167
+
168
+ // Look for kebab menu on the right to access patient profile
169
+ console.log("Looking for kebab menu on the right...");
170
+
171
+ // Try to find the menu - it might be a three-dot menu or settings icon on the right side
172
+ const kebabMenu = await testdriver.find("three dots menu or kebab menu on the right side of the page");
173
+ await kebabMenu.click();
174
+
175
+ // Wait for menu to open
176
+ await new Promise(resolve => setTimeout(resolve, 2000));
177
+
178
+ // Look for patient profile option
179
+ const patientProfile = await testdriver.find("Patient Profile or Edit Profile option in menu");
180
+ await patientProfile.click();
181
+
182
+ // Wait for patient profile form to load
183
+ await new Promise(resolve => setTimeout(resolve, 3000));
184
+
185
+ // Verify patient profile form is displayed
186
+ const profileForm = await testdriver.assert("patient profile form or edit patient page is visible");
187
+ expect(profileForm).toBeTruthy();
188
+
189
+ // ========================================================================
190
+ // CHECKPOINT 5: Patient Profile Opened
191
+ // If test fails after this point, comment out all code above this checkpoint
192
+ // ========================================================================
193
+
194
+ // Fill out patient details
195
+ console.log("Filling out patient details...");
196
+ try {
197
+ // Look for phone number field as an example
198
+ const phoneField = await testdriver.find("phone number field or mobile number input");
199
+ await phoneField.click();
200
+ await testdriver.type("555-0123");
201
+ console.log("Filled phone number");
202
+
203
+ // Look for email field
204
+ try {
205
+ const emailFieldProfile = await testdriver.find("email address field in the patient profile");
206
+ await emailFieldProfile.click();
207
+ await testdriver.type("patient@example.com");
208
+ console.log("Filled email address");
209
+ } catch {
210
+ console.log("Email field not found");
211
+ }
212
+
213
+ // Look for and click the save button
214
+ const saveButton = await testdriver.find("Save button or Submit button");
215
+ await saveButton.click();
216
+ console.log("Clicked save button");
217
+
218
+ // Wait for save to complete
219
+ await new Promise(resolve => setTimeout(resolve, 2000));
220
+
221
+ // Verify save was successful
222
+ const saveSuccess = await testdriver.assert("changes were saved successfully or save confirmation appears");
223
+ expect(saveSuccess).toBeTruthy();
224
+ } catch (error) {
225
+ console.log("Could not complete patient profile form:", error.message);
226
+ }
227
+
228
+ // Take final screenshot
229
+ const finalScreenshot = await testdriver.screenshot();
230
+ expect(finalScreenshot).toBeDefined();
231
+
232
+ console.log("✅ Test completed successfully! Remember to uncomment all checkpoints for final validation.");
233
+ });
234
+ });