testdriverai 6.2.2 → 7.0.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 (264) 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/MIGRATION.md +389 -0
  5. package/PLUGIN_MIGRATION.md +222 -0
  6. package/PROMPT_CACHE.md +200 -0
  7. package/SDK_LOGGING.md +222 -0
  8. package/SDK_MIGRATION.md +474 -0
  9. package/SDK_README.md +1122 -0
  10. package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
  11. package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
  12. package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
  13. package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
  14. package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
  15. package/_testdriver/lifecycle/prerun.yaml +15 -0
  16. package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
  17. package/agent/index.js +258 -68
  18. package/agent/interface.js +15 -0
  19. package/agent/lib/cache.js +142 -0
  20. package/agent/lib/commander.js +1 -39
  21. package/agent/lib/commands.js +143 -188
  22. package/agent/lib/redraw.js +6 -3
  23. package/agent/lib/sandbox.js +19 -5
  24. package/agent/lib/sdk.js +1 -0
  25. package/agent/lib/system.js +0 -3
  26. package/agent/lib/validation.js +1 -7
  27. package/debug-locate-response.js +82 -0
  28. package/debug-screenshot-1763401388589.png +0 -0
  29. package/debugger/index.html +15 -4
  30. package/docs/ARCHITECTURE.md +424 -0
  31. package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
  32. package/docs/QUICK_START_TEST_RECORDING.md +215 -0
  33. package/docs/SDK_AWESOME_LOGS.md +468 -0
  34. package/docs/TEST_RECORDING.md +388 -0
  35. package/docs/docs.json +232 -152
  36. package/docs/sdk-browser-rendering.md +167 -0
  37. package/docs/v6/getting-started/self-hosting.mdx +407 -0
  38. package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
  39. package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
  40. package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
  41. package/docs/v6/overview/comparison.mdx +101 -0
  42. package/docs/v7/README.md +135 -0
  43. package/docs/v7/api/ai.mdx +205 -0
  44. package/docs/v7/api/assert.mdx +285 -0
  45. package/docs/v7/api/assertions.mdx +403 -0
  46. package/docs/v7/api/click.mdx +287 -0
  47. package/docs/v7/api/client.mdx +322 -0
  48. package/docs/v7/api/elements.mdx +479 -0
  49. package/docs/v7/api/exec.mdx +346 -0
  50. package/docs/v7/api/find.mdx +316 -0
  51. package/docs/v7/api/focusApplication.mdx +294 -0
  52. package/docs/v7/api/hover.mdx +279 -0
  53. package/docs/v7/api/pressKeys.mdx +349 -0
  54. package/docs/v7/api/sandbox.mdx +404 -0
  55. package/docs/v7/api/scroll.mdx +300 -0
  56. package/docs/v7/api/type.mdx +314 -0
  57. package/docs/v7/commands/assert.mdx +45 -0
  58. package/docs/v7/commands/exec.mdx +282 -0
  59. package/docs/v7/commands/focus-application.mdx +44 -0
  60. package/docs/v7/commands/hover-image.mdx +69 -0
  61. package/docs/v7/commands/hover-text.mdx +47 -0
  62. package/docs/v7/commands/if.mdx +53 -0
  63. package/docs/v7/commands/match-image.mdx +67 -0
  64. package/docs/v7/commands/press-keys.mdx +87 -0
  65. package/docs/v7/commands/remember.mdx +49 -0
  66. package/docs/v7/commands/run.mdx +44 -0
  67. package/docs/v7/commands/scroll-until-image.mdx +66 -0
  68. package/docs/v7/commands/scroll-until-text.mdx +60 -0
  69. package/docs/v7/commands/scroll.mdx +69 -0
  70. package/docs/v7/commands/type.mdx +45 -0
  71. package/docs/v7/commands/wait-for-image.mdx +54 -0
  72. package/docs/v7/commands/wait-for-text.mdx +48 -0
  73. package/docs/v7/commands/wait.mdx +45 -0
  74. package/docs/v7/getting-started/quickstart.mdx +199 -0
  75. package/docs/v7/guides/migration.mdx +562 -0
  76. package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
  77. package/docs/v7/playwright.mdx +342 -0
  78. package/eslint.config.js +19 -1
  79. package/examples/run-tests-with-recording.sh +70 -0
  80. package/examples/screenshot-example.js +63 -0
  81. package/examples/sdk-awesome-logs-demo.js +177 -0
  82. package/examples/sdk-cache-thresholds.js +96 -0
  83. package/examples/sdk-element-properties.js +155 -0
  84. package/examples/sdk-simple-example.js +65 -0
  85. package/examples/test-recording-example.test.js +166 -0
  86. package/interfaces/cli/lib/base.js +10 -4
  87. package/interfaces/logger.js +2 -1
  88. package/interfaces/shared-test-state.mjs +69 -0
  89. package/interfaces/vitest-plugin.mjs +744 -0
  90. package/mcp-server/AI_GUIDELINES.md +57 -0
  91. package/package.json +18 -5
  92. package/schema.json +8 -29
  93. package/scripts/view-test-results.mjs +96 -0
  94. package/sdk-log-formatter.js +714 -0
  95. package/sdk.d.ts +735 -0
  96. package/sdk.js +1906 -0
  97. package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
  98. package/setup/aws/cloudformation.yaml +9 -2
  99. package/test/mcp-example-test.yaml +27 -0
  100. package/test-find-api.js +73 -0
  101. package/test-prompt-cache.js +96 -0
  102. package/test-sandbox-render.js +28 -0
  103. package/test-sdk-methods.js +15 -0
  104. package/test-sdk-refactor.js +53 -0
  105. package/test-stack-trace.mjs +57 -0
  106. package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
  107. package/testdriver/acceptance-sdk/README.md +128 -0
  108. package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
  109. package/testdriver/acceptance-sdk/assert.test.mjs +44 -0
  110. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +70 -0
  111. package/testdriver/acceptance-sdk/element-not-found.test.mjs +38 -0
  112. package/testdriver/acceptance-sdk/exec-js.test.mjs +55 -0
  113. package/testdriver/acceptance-sdk/exec-output.test.mjs +71 -0
  114. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +69 -0
  115. package/testdriver/acceptance-sdk/focus-window.test.mjs +48 -0
  116. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +41 -0
  117. package/testdriver/acceptance-sdk/hover-image.test.mjs +43 -0
  118. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +50 -0
  119. package/testdriver/acceptance-sdk/hover-text.test.mjs +41 -0
  120. package/testdriver/acceptance-sdk/match-image.test.mjs +48 -0
  121. package/testdriver/acceptance-sdk/press-keys.test.mjs +64 -0
  122. package/testdriver/acceptance-sdk/prompt.test.mjs +45 -0
  123. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +52 -0
  124. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +51 -0
  125. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +42 -0
  126. package/testdriver/acceptance-sdk/scroll.test.mjs +50 -0
  127. package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
  128. package/testdriver/acceptance-sdk/setup/lifecycleHelpers.mjs +239 -0
  129. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +648 -0
  130. package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
  131. package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
  132. package/testdriver/acceptance-sdk/type.test.mjs +84 -0
  133. package/verify-element-api.js +89 -0
  134. package/verify-types.js +0 -0
  135. package/vitest.config.example.js +19 -0
  136. package/vitest.config.mjs +65 -0
  137. package/vitest.config.mjs.bak +44 -0
  138. package/.github/workflows/acceptance-v6.yml +0 -169
  139. package/docs/overview/comparison.mdx +0 -82
  140. package/testdriver/lifecycle/prerun.yaml +0 -17
  141. /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
  142. /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
  143. /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
  144. /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
  145. /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
  146. /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
  147. /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
  148. /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
  149. /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
  150. /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
  151. /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
  152. /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
  153. /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
  154. /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
  155. /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
  156. /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
  157. /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
  158. /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
  159. /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
  160. /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
  161. /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
  162. /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
  163. /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
  164. /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
  165. /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
  166. /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
  167. /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
  168. /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
  169. /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
  170. /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
  171. /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
  172. /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
  173. /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
  174. /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
  175. /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
  176. /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
  177. /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
  178. /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
  179. /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
  180. /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
  181. /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
  182. /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
  183. /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
  184. /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
  185. /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
  186. /package/docs/{account → v6/account}/dashboard.mdx +0 -0
  187. /package/docs/{account → v6/account}/enterprise.mdx +0 -0
  188. /package/docs/{account → v6/account}/pricing.mdx +0 -0
  189. /package/docs/{account → v6/account}/projects.mdx +0 -0
  190. /package/docs/{account → v6/account}/team.mdx +0 -0
  191. /package/docs/{action → v6/action}/ami.mdx +0 -0
  192. /package/docs/{action → v6/action}/performance.mdx +0 -0
  193. /package/docs/{action → v6/action}/secrets.mdx +0 -0
  194. /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
  195. /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
  196. /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
  197. /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
  198. /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
  199. /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
  200. /package/docs/{cli → v6/cli}/overview.mdx +0 -0
  201. /package/docs/{commands → v6/commands}/assert.mdx +0 -0
  202. /package/docs/{commands → v6/commands}/exec.mdx +0 -0
  203. /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
  204. /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
  205. /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
  206. /package/docs/{commands → v6/commands}/if.mdx +0 -0
  207. /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
  208. /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
  209. /package/docs/{commands → v6/commands}/remember.mdx +0 -0
  210. /package/docs/{commands → v6/commands}/run.mdx +0 -0
  211. /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
  212. /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
  213. /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
  214. /package/docs/{commands → v6/commands}/type.mdx +0 -0
  215. /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
  216. /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
  217. /package/docs/{commands → v6/commands}/wait.mdx +0 -0
  218. /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
  219. /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
  220. /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
  221. /package/docs/{features → v6/features}/generation.mdx +0 -0
  222. /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
  223. /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
  224. /package/docs/{features → v6/features}/selectorless.mdx +0 -0
  225. /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
  226. /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
  227. /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
  228. /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
  229. /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
  230. /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
  231. /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
  232. /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
  233. /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
  234. /package/docs/{guide → v6/guide}/code.mdx +0 -0
  235. /package/docs/{guide → v6/guide}/locating.mdx +0 -0
  236. /package/docs/{guide → v6/guide}/protips.mdx +0 -0
  237. /package/docs/{guide → v6/guide}/variables.mdx +0 -0
  238. /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
  239. /package/docs/{importing → v6/importing}/csv.mdx +0 -0
  240. /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
  241. /package/docs/{importing → v6/importing}/jira.mdx +0 -0
  242. /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
  243. /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
  244. /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
  245. /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
  246. /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
  247. /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
  248. /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
  249. /package/docs/{overview → v6/overview}/faq.mdx +0 -0
  250. /package/docs/{overview → v6/overview}/performance.mdx +0 -0
  251. /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
  252. /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
  253. /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
  254. /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
  255. /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
  256. /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
  257. /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
  258. /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
  259. /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
  260. /package/docs/{security → v6/security}/action.mdx +0 -0
  261. /package/docs/{security → v6/security}/agent.mdx +0 -0
  262. /package/docs/{security → v6/security}/platform.mdx +0 -0
  263. /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
  264. /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
@@ -0,0 +1,648 @@
1
+ /**
2
+ * Test Helpers and Utilities
3
+ * Shared functions for SDK tests
4
+ */
5
+
6
+ import crypto from "crypto";
7
+ import { config } from "dotenv";
8
+ import fs from "fs";
9
+ import os from "os";
10
+ import path, { dirname } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import TestDriver from "../../../sdk.js";
13
+ import {
14
+ addDashcamLog,
15
+ authDashcam,
16
+ launchChrome,
17
+ runPostrun,
18
+ runPrerun,
19
+ startDashcam,
20
+ stopDashcam,
21
+ waitForPage,
22
+ } from "./lifecycleHelpers.mjs";
23
+
24
+ // Re-export lifecycle helpers for backward compatibility
25
+ export {
26
+ addDashcamLog,
27
+ authDashcam,
28
+ launchChrome,
29
+ runPostrun,
30
+ runPrerun,
31
+ startDashcam,
32
+ stopDashcam,
33
+ waitForPage
34
+ };
35
+
36
+ // Get the directory of the current module
37
+ const __filename = fileURLToPath(import.meta.url);
38
+ const __dirname = dirname(__filename);
39
+
40
+ // Load environment variables from .env file in the project root
41
+ // Go up 3 levels from setup/ to reach the project root
42
+ const envPath = path.resolve(__dirname, "../../../.env");
43
+ config({ path: envPath });
44
+
45
+ // Log loaded env vars for debugging
46
+ console.log("🔧 Environment variables loaded from:", envPath);
47
+ console.log(" TD_API_KEY:", process.env.TD_API_KEY ? "✓ Set" : "✗ Not set");
48
+ console.log(" TD_API_ROOT:", process.env.TD_API_ROOT || "Not set");
49
+ console.log(
50
+ " TD_OS:",
51
+ process.env.TD_OS || "Not set (will default to linux)",
52
+ );
53
+
54
+ // Global test results storage
55
+ const testResults = {
56
+ tests: [],
57
+ startTime: Date.now(),
58
+ };
59
+
60
+ /**
61
+ * Store test result with dashcam URL
62
+ * @param {string} testName - Name of the test
63
+ * @param {string} testFile - Test file path
64
+ * @param {string|null} dashcamUrl - Dashcam URL if available
65
+ * @param {Object} sessionInfo - Session information
66
+ */
67
+ export function storeTestResult(
68
+ testName,
69
+ testFile,
70
+ dashcamUrl,
71
+ sessionInfo = {},
72
+ ) {
73
+ console.log(`📝 Storing test result: ${testName}`);
74
+ console.log(` Dashcam URL: ${dashcamUrl || "none"}`);
75
+
76
+ // Extract replay object ID from dashcam URL
77
+ let replayObjectId = null;
78
+ if (dashcamUrl) {
79
+ const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
80
+ replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
81
+ if (replayObjectId) {
82
+ console.log(` Replay Object ID: ${replayObjectId}`);
83
+ }
84
+ }
85
+
86
+ testResults.tests.push({
87
+ name: testName,
88
+ file: testFile,
89
+ dashcamUrl,
90
+ replayObjectId,
91
+ sessionId: sessionInfo.sessionId,
92
+ timestamp: new Date().toISOString(),
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Get all test results
98
+ * @returns {Object} All collected test results
99
+ */
100
+ export function getTestResults() {
101
+ return {
102
+ ...testResults,
103
+ endTime: Date.now(),
104
+ duration: Date.now() - testResults.startTime,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Save test results to a JSON file
110
+ * @param {string} outputPath - Path to save the results
111
+ */
112
+ export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
113
+ const results = getTestResults();
114
+ const dir = path.dirname(outputPath);
115
+
116
+ // Create directory if it doesn't exist
117
+ if (!fs.existsSync(dir)) {
118
+ fs.mkdirSync(dir, { recursive: true });
119
+ }
120
+
121
+ fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
122
+ console.log(`\n📊 Test results saved to: ${outputPath}`);
123
+
124
+ // Also print dashcam URLs to console
125
+ console.log("\n🎥 Dashcam URLs:");
126
+ results.tests.forEach((test) => {
127
+ if (test.dashcamUrl) {
128
+ console.log(` ${test.name}: ${test.dashcamUrl}`);
129
+ }
130
+ });
131
+
132
+ return results;
133
+ }
134
+
135
+ /**
136
+ * Intercept console logs and forward to TestDriver sandbox
137
+ * @param {TestDriver} client - TestDriver client instance
138
+ * @param {string} taskId - Unique task identifier for this test
139
+ */
140
+ function setupConsoleInterceptor(client, taskId) {
141
+ // Store original console methods
142
+ const originalConsole = {
143
+ log: console.log,
144
+ error: console.error,
145
+ warn: console.warn,
146
+ info: console.info,
147
+ };
148
+
149
+ // Create wrapper that forwards to sandbox
150
+ const createInterceptor = (level, originalMethod) => {
151
+ return function (...args) {
152
+ // Call original console method first
153
+ originalMethod.apply(console, args);
154
+
155
+ // Forward to sandbox if connected
156
+ if (client.sandbox && client.sandbox.instanceSocketConnected) {
157
+ try {
158
+ // Format the log message
159
+ const message = args
160
+ .map((arg) =>
161
+ typeof arg === "object"
162
+ ? JSON.stringify(arg, null, 2)
163
+ : String(arg),
164
+ )
165
+ .join(" ");
166
+
167
+ // Preserve ANSI color codes and emojis for rich sandbox output
168
+ // (don't add level prefix - sdk-log-formatter handles styling)
169
+ const logOutput = message;
170
+
171
+ client.sandbox.send({
172
+ type: "output",
173
+ output: Buffer.from(logOutput, "utf8").toString("base64"),
174
+ });
175
+ } catch (error) {
176
+ // Silently fail to avoid breaking the test
177
+ // Use original console to avoid infinite loop
178
+ originalConsole.error(
179
+ `[TestHelpers] Failed to forward log to sandbox:`,
180
+ error.message,
181
+ );
182
+ }
183
+ }
184
+ };
185
+ };
186
+
187
+ // Replace console methods with interceptors
188
+ console.log = createInterceptor("log", originalConsole.log);
189
+ console.error = createInterceptor("error", originalConsole.error);
190
+ console.warn = createInterceptor("warn", originalConsole.warn);
191
+ console.info = createInterceptor("info", originalConsole.info);
192
+
193
+ // Store original methods and taskId on client for cleanup
194
+ client._consoleInterceptor = {
195
+ taskId,
196
+ original: originalConsole,
197
+ };
198
+
199
+ // Use original console for this message
200
+ originalConsole.log(
201
+ `[TestHelpers] Console interceptor enabled for task: ${taskId}`,
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Remove console interceptor and restore original console methods
207
+ * @param {TestDriver} client - TestDriver client instance
208
+ */
209
+ function removeConsoleInterceptor(client) {
210
+ if (client._consoleInterceptor) {
211
+ const { original, taskId } = client._consoleInterceptor;
212
+
213
+ // Restore original console methods
214
+ console.log = original.log;
215
+ console.error = original.error;
216
+ console.warn = original.warn;
217
+ console.info = original.info;
218
+
219
+ // Use original console for cleanup message
220
+ original.log(
221
+ `[TestHelpers] Console interceptor removed for task: ${taskId}`,
222
+ );
223
+
224
+ // Clean up reference
225
+ delete client._consoleInterceptor;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Create a configured TestDriver client
231
+ * @param {Object} options - Additional options
232
+ * @param {Object} options.task - Vitest task context (from beforeEach/it context)
233
+ * @returns {TestDriver} Configured client
234
+ */
235
+ export function createTestClient(options = {}) {
236
+ // Check if API key is set
237
+ if (!process.env.TD_API_KEY) {
238
+ console.error("\n❌ Error: TD_API_KEY is not set!");
239
+ console.error("Please set it in one of the following ways:");
240
+ console.error(
241
+ " 1. Create a .env file in the project root with: TD_API_KEY=your_key",
242
+ );
243
+ console.error(
244
+ " 2. Pass it as an environment variable: TD_API_KEY=your_key npm run test:sdk",
245
+ );
246
+ console.error(" 3. Export it in your shell: export TD_API_KEY=your_key\n");
247
+ throw new Error("TD_API_KEY environment variable is required");
248
+ }
249
+
250
+ // Determine OS from TEST_PLATFORM or TD_OS
251
+ const os = process.env.TEST_PLATFORM || "linux";
252
+
253
+ // Extract task context if provided - we use taskId but remove task from clientOptions
254
+ let taskId = options.task?.id || options.task?.name || null;
255
+
256
+ // Remove task from options before passing to TestDriver (eslint wants us to use 'task')
257
+ // eslint-disable-next-line no-unused-vars
258
+ const { task, ...clientOptions } = options;
259
+
260
+ const client = new TestDriver(process.env.TD_API_KEY, {
261
+ resolution: "1366x768",
262
+ analytics: true,
263
+ os: os, // Use OS from environment variable (windows or linux)
264
+ apiKey: process.env.TD_API_KEY,
265
+ apiRoot: process.env.TD_API_ROOT || "https://testdriver-api.onrender.com",
266
+ // headless: false,
267
+ // newSandbox: true,
268
+ // ip: '18.217.194.23'
269
+ // ...clientOptions,
270
+ // cache: false,
271
+ });
272
+
273
+ console.log(
274
+ "🔧 createTestClient: SDK created, cacheThresholds =",
275
+ client.cacheThresholds,
276
+ );
277
+
278
+ console.log(`[TestHelpers] Client OS configured as: ${client.os}`);
279
+
280
+ // Set Vitest task ID if available (for log filtering in parallel tests)
281
+ if (taskId) {
282
+ console.log(`[TestHelpers] Storing task ID on client: ${taskId}`);
283
+ // Store task ID directly on client for later use in teardown
284
+ client.vitestTaskId = taskId;
285
+ } else {
286
+ console.log(`[TestHelpers] No task ID available`);
287
+ }
288
+
289
+ // Enable detailed event logging if requested
290
+ if (process.env.DEBUG_EVENTS === "true") {
291
+ setupEventLogging(client);
292
+ }
293
+
294
+ return client;
295
+ }
296
+
297
+ /**
298
+ * Set up detailed event logging for debugging
299
+ * @param {TestDriver} client - TestDriver client
300
+ */
301
+ export function setupEventLogging(client) {
302
+ const emitter = client.getEmitter();
303
+
304
+ // Log all events
305
+ emitter.on("**", function (data) {
306
+ const event = this.event;
307
+ if (event.startsWith("log:debug")) return; // Skip debug logs
308
+ console.log(`[EVENT] ${event}`, data || "");
309
+ });
310
+
311
+ // Log command lifecycle
312
+ emitter.on("command:start", (data) => {
313
+ console.log("🚀 Command started:", data);
314
+ });
315
+
316
+ emitter.on("command:success", (data) => {
317
+ console.log("✅ Command succeeded:", data);
318
+ });
319
+
320
+ emitter.on("command:error", (data) => {
321
+ console.error("❌ Command error:", data);
322
+ });
323
+
324
+ // Log sandbox events
325
+ emitter.on("sandbox:connected", () => {
326
+ console.log("🔌 Sandbox connected");
327
+ });
328
+
329
+ emitter.on("sandbox:authenticated", () => {
330
+ console.log("🔐 Sandbox authenticated");
331
+ });
332
+
333
+ emitter.on("sandbox:error", (error) => {
334
+ console.error("⚠️ Sandbox error:", error);
335
+ });
336
+
337
+ // Log SDK API calls
338
+ emitter.on("sdk:request", (data) => {
339
+ console.log("📤 SDK Request:", data);
340
+ });
341
+
342
+ emitter.on("sdk:response", (data) => {
343
+ console.log("📥 SDK Response:", data);
344
+ });
345
+ }
346
+
347
+ /**
348
+ * Setup function to run before each test
349
+ * Authenticates and connects to sandbox
350
+ * @param {TestDriver} client - TestDriver client
351
+ * @param {Object} options - Connection options
352
+ * @returns {Promise<Object>} Sandbox instance
353
+ */
354
+ export async function setupTest(client, options = {}) {
355
+ await client.auth();
356
+ const instance = await client.connect({
357
+ ...options,
358
+ });
359
+
360
+ // Set up console interceptor after connection (needs sandbox to be connected)
361
+ if (client.vitestTaskId) {
362
+ setupConsoleInterceptor(client, client.vitestTaskId);
363
+ }
364
+
365
+ // Run prerun lifecycle if enabled
366
+ if (options.prerun !== false) {
367
+ await runPrerun(client);
368
+ }
369
+
370
+ return instance;
371
+ }
372
+
373
+ /**
374
+ * Initialize a test run for the entire suite
375
+ * Should be called once in beforeEach
376
+ * @param {Object} suiteTask - Vitest suite task context
377
+ * @returns {Promise<Object>} Test run info { runId, testRunDbId, token }
378
+ */
379
+ export async function initializeSuiteTestRun(suiteTask) {
380
+ const apiKey = process.env.TD_API_KEY;
381
+ const apiRoot =
382
+ process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
383
+
384
+ if (!apiKey || !globalThis.__testdriverPlugin) {
385
+ console.log(
386
+ `[TestHelpers] Skipping suite test run initialization - no API key or plugin`,
387
+ );
388
+ return null;
389
+ }
390
+
391
+ // Check if test run already exists for this suite
392
+ const existingRun = globalThis.__testdriverPlugin.getSuiteTestRun(
393
+ suiteTask.id,
394
+ );
395
+ if (existingRun) {
396
+ console.log(
397
+ `[TestHelpers] Test run already exists for suite: ${existingRun.runId}`,
398
+ );
399
+ return existingRun;
400
+ }
401
+
402
+ try {
403
+ console.log(
404
+ `[TestHelpers] Initializing test run for suite: ${suiteTask.name}`,
405
+ );
406
+
407
+ // Authenticate
408
+ const token = await globalThis.__testdriverPlugin.authenticateWithApiKey(
409
+ apiKey,
410
+ apiRoot,
411
+ );
412
+ console.log(`[TestHelpers] ✅ Authenticated for suite`);
413
+
414
+ // Create test run for the suite
415
+ const runId = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
416
+ const testFile = suiteTask.file?.name || "unknown";
417
+ const testRunData = {
418
+ runId,
419
+ suiteName: suiteTask.name || testFile,
420
+ };
421
+
422
+ const testRunResponse =
423
+ await globalThis.__testdriverPlugin.createTestRunDirect(
424
+ token,
425
+ apiRoot,
426
+ testRunData,
427
+ );
428
+ const testRunDbId = testRunResponse.data?.id;
429
+
430
+ const runInfo = { runId, testRunDbId, token };
431
+
432
+ // Store in plugin state
433
+ globalThis.__testdriverPlugin.setSuiteTestRun(suiteTask.id, runInfo);
434
+
435
+ // Set environment variables for the reporter to use
436
+ process.env.TD_TEST_RUN_ID = runId;
437
+ process.env.TD_TEST_RUN_DB_ID = testRunDbId;
438
+ process.env.TD_TEST_RUN_TOKEN = token;
439
+
440
+ console.log(
441
+ `[TestHelpers] ✅ Created test run for suite: ${runId} (DB ID: ${testRunDbId})`,
442
+ );
443
+
444
+ return runInfo;
445
+ } catch (error) {
446
+ console.error(
447
+ `[TestHelpers] ❌ Failed to initialize suite test run:`,
448
+ error.message,
449
+ );
450
+ return null;
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Teardown function to run after each test
456
+ * @param {TestDriver} client - TestDriver client
457
+ * @param {Object} options - Teardown options
458
+ * @param {Object} options.task - Vitest task context (optional, for storing in task.meta)
459
+ * @param {string} options.dashcamUrl - Dashcam URL if already retrieved
460
+ * @param {boolean} options.postrun - Whether to run postrun lifecycle (default: true)
461
+ * @param {boolean} options.disconnect - Whether to disconnect client (default: true)
462
+ * @returns {Promise<Object>} Session info including dashcam URL
463
+ */
464
+ export async function teardownTest(client, options = {}) {
465
+ let dashcamUrl = options.dashcamUrl || null;
466
+
467
+ console.log("🧹 Running teardown...");
468
+
469
+ try {
470
+ // Run postrun lifecycle if enabled and dashcamUrl not already provided
471
+ if (options.postrun !== false && !dashcamUrl) {
472
+ dashcamUrl = await runPostrun(client);
473
+
474
+ // Store dashcamUrl in client for reporter access
475
+ if (dashcamUrl) {
476
+ // Extract replay object ID from URL
477
+ // URL format: https://app.testdriver.ai/replay/{replayObjectId}?share={shareToken}
478
+ const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
479
+ const replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
480
+
481
+ console.log(`🎥 Dashcam URL: ${dashcamUrl}`);
482
+ if (replayObjectId) {
483
+ console.log(`📝 Replay Object ID: ${replayObjectId}`);
484
+ }
485
+
486
+ // Store dashcam URL in task meta
487
+ if (options.task) {
488
+ options.task.meta.testdriverDashcamUrl = dashcamUrl;
489
+ options.task.meta.testdriverReplayObjectId = replayObjectId;
490
+ console.log(
491
+ `[TestHelpers] ✅ Stored dashcam URL in task.meta for test: ${options.task.name}`,
492
+ );
493
+ }
494
+ }
495
+ } else {
496
+ console.log("⏭️ Postrun skipped (disabled in options)");
497
+ }
498
+
499
+ // Write test result to a file for the reporter to pick up (cross-process communication)
500
+ if (options.task) {
501
+ const testResultFile = path.join(
502
+ os.tmpdir(),
503
+ "testdriver-results",
504
+ `${options.task.id}.json`,
505
+ );
506
+
507
+ try {
508
+ // Ensure directory exists
509
+ const dir = path.dirname(testResultFile);
510
+ if (!fs.existsSync(dir)) {
511
+ fs.mkdirSync(dir, { recursive: true });
512
+ }
513
+
514
+ // Get test file path
515
+ const testFile =
516
+ options.task.file?.filepath || options.task.file?.name || "unknown";
517
+
518
+ // Calculate test order (index within parent suite)
519
+ let testOrder = 0;
520
+ if (options.task.suite && options.task.suite.tasks) {
521
+ testOrder = options.task.suite.tasks.indexOf(options.task);
522
+ }
523
+
524
+ // Note: Duration is calculated by Vitest and passed via result.duration
525
+ // We don't need to track start time ourselves
526
+
527
+ // Write test result with dashcam URL, platform, and metadata
528
+ const testResult = {
529
+ testId: options.task.id,
530
+ testName: options.task.name,
531
+ testFile: testFile,
532
+ testOrder: testOrder,
533
+ dashcamUrl: dashcamUrl,
534
+ replayObjectId: dashcamUrl
535
+ ? dashcamUrl.match(/\/replay\/([^?]+)/)?.[1]
536
+ : null,
537
+ platform: client.os, // Include platform from SDK client (source of truth)
538
+ timestamp: Date.now(),
539
+ };
540
+
541
+ fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
542
+ console.log(
543
+ `[TestHelpers] ✅ Wrote test result to file: ${testResultFile} (testFile: ${testFile}, testOrder: ${testOrder})`,
544
+ );
545
+ } catch (error) {
546
+ console.error(
547
+ `[TestHelpers] ❌ Failed to write test result file:`,
548
+ error.message,
549
+ );
550
+ }
551
+ }
552
+ } catch (error) {
553
+ console.error("❌ Error in postrun:", error);
554
+ console.error("❌ Error stack:", error.stack);
555
+ } finally {
556
+ // Remove console interceptor before disconnecting
557
+ removeConsoleInterceptor(client);
558
+
559
+ // Only disconnect if not explicitly disabled
560
+ if (options.disconnect !== false) {
561
+ console.log("🔌 Disconnecting client...");
562
+ try {
563
+ await client.disconnect();
564
+ console.log("✅ Client disconnected");
565
+ } catch (disconnectError) {
566
+ console.error("❌ Error disconnecting:", disconnectError.message);
567
+ // Don't throw - we're already in cleanup
568
+ }
569
+ } else {
570
+ console.log("⏭️ Disconnect skipped (disabled in options)");
571
+ }
572
+ }
573
+
574
+ // Extract replay object ID from dashcam URL
575
+ let replayObjectId = null;
576
+ if (dashcamUrl) {
577
+ const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
578
+ replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
579
+ }
580
+
581
+ const sessionInfo = {
582
+ sessionId: client.getSessionId(),
583
+ dashcamUrl: dashcamUrl,
584
+ replayObjectId: replayObjectId,
585
+ instance: client.getInstance(),
586
+ };
587
+
588
+ console.log("📊 Session info:", JSON.stringify(sessionInfo, null, 2));
589
+
590
+ return sessionInfo;
591
+ }
592
+
593
+ /**
594
+ * Perform login flow (reusable snippet)
595
+ * @param {TestDriver} client - TestDriver client
596
+ * @param {string} username - Username (default: 'standard_user')
597
+ * @param {string} password - Password (default: retrieved from screen)
598
+ */
599
+ export async function performLogin(
600
+ client,
601
+ username = "standard_user",
602
+ password = null,
603
+ ) {
604
+ await client.focusApplication("Google Chrome");
605
+
606
+ // Get password from screen if not provided
607
+ if (!password) {
608
+ password = await client.remember("the password");
609
+ }
610
+
611
+ const usernameField = await client.find(
612
+ "Username, label above the username input field on the login form",
613
+ );
614
+ await usernameField.click();
615
+ await client.type(username);
616
+
617
+ // Enter password
618
+ await client.pressKeys(["tab"]);
619
+ await client.type(password);
620
+
621
+ // Submit form
622
+ await client.pressKeys(["tab"]);
623
+ await client.pressKeys(["enter"]);
624
+ }
625
+
626
+ /**
627
+ * Wait with retry logic
628
+ * @param {Function} fn - Async function to retry
629
+ * @param {number} retries - Number of retries (default: 3)
630
+ * @param {number} delay - Delay between retries in ms (default: 1000)
631
+ * @returns {Promise} Result of successful execution
632
+ */
633
+ export async function retryAsync(fn, retries = 3, delay = 1000) {
634
+ let lastError;
635
+
636
+ for (let i = 0; i < retries; i++) {
637
+ try {
638
+ return await fn();
639
+ } catch (error) {
640
+ lastError = error;
641
+ if (i < retries - 1) {
642
+ await new Promise((resolve) => setTimeout(resolve, delay));
643
+ }
644
+ }
645
+ }
646
+
647
+ throw lastError;
648
+ }
@@ -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
+ }