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,744 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { setTestRunInfo } from "./shared-test-state.mjs";
6
+
7
+ /**
8
+ * Timeout wrapper for promises
9
+ * @param {Promise} promise - Promise to wrap
10
+ * @param {number} timeoutMs - Timeout in milliseconds
11
+ * @param {string} operationName - Name of operation for error message
12
+ * @returns {Promise} Promise that rejects if timeout is reached
13
+ */
14
+ function withTimeout(promise, timeoutMs, operationName) {
15
+ return Promise.race([
16
+ promise,
17
+ new Promise((_, reject) =>
18
+ setTimeout(
19
+ () =>
20
+ reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)),
21
+ timeoutMs,
22
+ ),
23
+ ),
24
+ ]);
25
+ }
26
+
27
+ /**
28
+ * Vitest Plugin for TestDriver
29
+ *
30
+ * Records test runs, test cases, and associates them with dashcam recordings.
31
+ * Uses plugin architecture for better global state management.
32
+ *
33
+ * ## How it works:
34
+ *
35
+ * 1. **Plugin State**: All state is managed in a single `pluginState` object
36
+ * - No class instances or complex scoping
37
+ * - Easy to access from anywhere in the plugin
38
+ * - Dashcam URLs tracked in memory (no temp files!)
39
+ *
40
+ * 2. **Dashcam URL Registration**: Tests register dashcam URLs via simple API
41
+ * - `globalThis.__testdriverPlugin.registerDashcamUrl(testId, url, platform)`
42
+ * - No file system operations
43
+ * - No complex matching logic
44
+ * - Direct association via test ID
45
+ *
46
+ * 3. **Test Recording Flow**:
47
+ * - `onTestRunStart`: Create test run record
48
+ * - `onTestCaseReady`: Track test start time
49
+ * - `onTestCaseResult`: Record individual test result (immediate)
50
+ * - `onTestRunEnd`: Complete test run with final stats
51
+ *
52
+ * 4. **Platform Detection**: Automatically detects platform from SDK client
53
+ * - No manual configuration needed
54
+ * - Stored when dashcam URL is registered
55
+ */
56
+
57
+ // Shared state that can be imported by both the reporter and setup files
58
+ export const pluginState = {
59
+ testRun: null,
60
+ testRunId: null,
61
+ client: null,
62
+ startTime: null,
63
+ testCases: new Map(),
64
+ token: null,
65
+ detectedPlatform: null,
66
+ pendingTestCaseRecords: new Set(),
67
+ ciProvider: null,
68
+ gitInfo: {},
69
+ apiKey: null,
70
+ apiRoot: null,
71
+ // Dashcam URL tracking (in-memory, no files needed!)
72
+ dashcamUrls: new Map(), // testId -> dashcamUrl
73
+ lastDashcamUrl: null, // Fallback for when test ID isn't available
74
+ // Suite-level test run tracking
75
+ suiteTestRuns: new Map(), // suiteId -> { runId, testRunDbId, token }
76
+ };
77
+
78
+ // Export functions that can be used by the reporter or tests
79
+ export function registerDashcamUrl(testId, url, platform) {
80
+ console.log(`[Plugin] Registering dashcam URL for test ${testId}:`, url);
81
+ pluginState.dashcamUrls.set(testId, { url, platform });
82
+ pluginState.lastDashcamUrl = url;
83
+ }
84
+
85
+ export function getDashcamUrl(testId) {
86
+ return pluginState.dashcamUrls.get(testId);
87
+ }
88
+
89
+ export function clearDashcamUrls() {
90
+ pluginState.dashcamUrls.clear();
91
+ pluginState.lastDashcamUrl = null;
92
+ }
93
+
94
+ export function getSuiteTestRun(suiteId) {
95
+ return pluginState.suiteTestRuns.get(suiteId);
96
+ }
97
+
98
+ export function setSuiteTestRun(suiteId, runData) {
99
+ console.log(`[Plugin] Setting test run for suite ${suiteId}:`, runData);
100
+ pluginState.suiteTestRuns.set(suiteId, runData);
101
+ }
102
+
103
+ export function clearSuiteTestRun(suiteId) {
104
+ pluginState.suiteTestRuns.delete(suiteId);
105
+ }
106
+
107
+ export function getPluginState() {
108
+ return pluginState;
109
+ }
110
+
111
+ // Export API helper functions for direct use from tests
112
+ export async function authenticateWithApiKey(apiKey, apiRoot) {
113
+ const url = `${apiRoot}/auth/exchange-api-key`;
114
+ const response = await withTimeout(
115
+ fetch(url, {
116
+ method: "POST",
117
+ headers: {
118
+ "Content-Type": "application/json",
119
+ },
120
+ body: JSON.stringify({ apiKey }),
121
+ }),
122
+ 10000,
123
+ "Authentication",
124
+ );
125
+
126
+ if (!response.ok) {
127
+ throw new Error(
128
+ `Authentication failed: ${response.status} ${response.statusText}`,
129
+ );
130
+ }
131
+
132
+ const data = await response.json();
133
+ return data.token;
134
+ }
135
+
136
+ export async function createTestRunDirect(token, apiRoot, testRunData) {
137
+ const url = `${apiRoot}/api/v1/testdriver/test-run-create`;
138
+ const response = await withTimeout(
139
+ fetch(url, {
140
+ method: "POST",
141
+ headers: {
142
+ "Content-Type": "application/json",
143
+ Authorization: `Bearer ${token}`,
144
+ },
145
+ body: JSON.stringify(testRunData),
146
+ }),
147
+ 10000,
148
+ "Create Test Run",
149
+ );
150
+
151
+ if (!response.ok) {
152
+ const errorText = await response.text();
153
+ throw new Error(
154
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
155
+ );
156
+ }
157
+
158
+ return await response.json();
159
+ }
160
+
161
+ export async function recordTestCaseDirect(token, apiRoot, testCaseData) {
162
+ const url = `${apiRoot}/api/v1/testdriver/test-case-create`;
163
+ const response = await withTimeout(
164
+ fetch(url, {
165
+ method: "POST",
166
+ headers: {
167
+ "Content-Type": "application/json",
168
+ Authorization: `Bearer ${token}`,
169
+ },
170
+ body: JSON.stringify(testCaseData),
171
+ }),
172
+ 10000,
173
+ "Record Test Case",
174
+ );
175
+
176
+ if (!response.ok) {
177
+ const errorText = await response.text();
178
+ throw new Error(
179
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
180
+ );
181
+ }
182
+
183
+ return await response.json();
184
+ }
185
+
186
+ /**
187
+ * Create the TestDriver Vitest plugin
188
+ * This sets up global state and provides the registration API
189
+ */
190
+ export default function testDriverPlugin(options = {}) {
191
+ // Initialize plugin state with options
192
+ pluginState.apiKey = options.apiKey;
193
+ pluginState.apiRoot =
194
+ options.apiRoot || process.env.TD_API_ROOT || "http://localhost:1337";
195
+ pluginState.ciProvider = detectCI();
196
+ pluginState.gitInfo = getGitInfo();
197
+
198
+ // Note: globalThis setup happens in vitestSetup.mjs for worker processes
199
+ console.log(
200
+ "[TestDriver Plugin] Initialized with API root:",
201
+ pluginState.apiRoot,
202
+ );
203
+
204
+ return new TestDriverReporter(options);
205
+ }
206
+
207
+ /**
208
+ * TestDriver Reporter Class
209
+ * Handles Vitest test lifecycle events
210
+ */
211
+ class TestDriverReporter {
212
+ constructor(options = {}) {
213
+ this.options = options;
214
+ console.log("[TestDriver Reporter] Created");
215
+ }
216
+
217
+ async onInit(ctx) {
218
+ this.ctx = ctx;
219
+ console.log("[TestDriver Reporter] onInit called");
220
+
221
+ // Initialize test run
222
+ await this.initializeTestRun();
223
+ }
224
+
225
+ async initializeTestRun() {
226
+ console.log("[TestDriver Reporter] Initializing test run...");
227
+
228
+ // Check if we should enable the reporter
229
+ if (!pluginState.apiKey) {
230
+ console.log(
231
+ "[TestDriver Reporter] No API key provided, skipping test recording",
232
+ );
233
+ return;
234
+ }
235
+
236
+ try {
237
+ // Exchange API key for JWT token
238
+ await authenticate();
239
+
240
+ // Generate unique run ID
241
+ pluginState.testRunId = generateRunId();
242
+ pluginState.startTime = Date.now();
243
+
244
+ // Create test run via direct API call
245
+ const testRunData = {
246
+ runId: pluginState.testRunId,
247
+ suiteName: getSuiteName(),
248
+ ...pluginState.gitInfo,
249
+ };
250
+
251
+ // Only add ciProvider if it's not null
252
+ if (pluginState.ciProvider) {
253
+ testRunData.ciProvider = pluginState.ciProvider;
254
+ }
255
+
256
+ // Platform will be set from the first test result file
257
+ // Default to linux if no tests write platform info
258
+ testRunData.platform = "linux";
259
+
260
+ pluginState.testRun = await createTestRun(testRunData);
261
+
262
+ // Store in environment variables for worker processes to access
263
+ process.env.TD_TEST_RUN_ID = pluginState.testRunId;
264
+ process.env.TD_TEST_RUN_DB_ID = pluginState.testRun.data?.id || "";
265
+ process.env.TD_TEST_RUN_TOKEN = pluginState.token;
266
+
267
+ // Also store in shared state module (won't work across processes but good for main)
268
+ setTestRunInfo({
269
+ testRun: pluginState.testRun,
270
+ testRunId: pluginState.testRunId,
271
+ token: pluginState.token,
272
+ apiKey: pluginState.apiKey,
273
+ apiRoot: pluginState.apiRoot,
274
+ startTime: pluginState.startTime,
275
+ });
276
+
277
+ console.log(
278
+ `[TestDriver Reporter] Test run created: ${pluginState.testRunId}`,
279
+ );
280
+ } catch (error) {
281
+ console.error(
282
+ "[TestDriver Reporter] Failed to initialize:",
283
+ error.message,
284
+ );
285
+ pluginState.apiKey = null;
286
+ pluginState.token = null;
287
+ }
288
+ }
289
+
290
+ async onTestRunEnd(testModules, unhandledErrors, reason) {
291
+ console.log("[TestDriver Reporter] Test run ending with reason:", reason);
292
+
293
+ if (!pluginState.apiKey) {
294
+ console.log("[TestDriver Reporter] Skipping completion - no API key");
295
+ return;
296
+ }
297
+
298
+ if (!pluginState.testRun) {
299
+ console.log(
300
+ "[TestDriver Reporter] Skipping completion - no test run created",
301
+ );
302
+ return;
303
+ }
304
+
305
+ try {
306
+ // Calculate statistics from testModules
307
+ const stats = calculateStatsFromModules(testModules);
308
+
309
+ console.log(`[TestDriver Reporter] Stats:`, stats);
310
+
311
+ // Determine overall status based on reason and stats
312
+ let status = "passed";
313
+ if (reason === "failed" || stats.failedTests > 0) {
314
+ status = "failed";
315
+ } else if (reason === "interrupted") {
316
+ status = "cancelled";
317
+ } else if (stats.totalTests === 0) {
318
+ status = "cancelled";
319
+ }
320
+
321
+ // Complete test run via API
322
+ console.log(
323
+ `[TestDriver Reporter] Completing test run ${pluginState.testRunId} with status: ${status}`,
324
+ );
325
+
326
+ const completeData = {
327
+ runId: pluginState.testRunId,
328
+ status,
329
+ totalTests: stats.totalTests,
330
+ passedTests: stats.passedTests,
331
+ failedTests: stats.failedTests,
332
+ skippedTests: stats.skippedTests,
333
+ duration: Date.now() - pluginState.startTime,
334
+ };
335
+
336
+ // Update platform if detected from test results
337
+ const platform = getPlatform();
338
+ if (platform) {
339
+ completeData.platform = platform;
340
+ console.log(
341
+ `[TestDriver Reporter] Updating test run with platform: ${platform}`,
342
+ );
343
+ }
344
+
345
+ // Wait for any pending operations (shouldn't be any, but just in case)
346
+ if (pluginState.pendingTestCaseRecords.size > 0) {
347
+ console.log(
348
+ `[TestDriver Reporter] Waiting for ${pluginState.pendingTestCaseRecords.size} pending operations...`,
349
+ );
350
+ await Promise.all(Array.from(pluginState.pendingTestCaseRecords));
351
+ }
352
+
353
+ // Test cases are reported directly from teardownTest
354
+ console.log(
355
+ `[TestDriver Reporter] All test cases reported from teardown`,
356
+ );
357
+
358
+ const completeResponse = await completeTestRun(completeData);
359
+ console.log(
360
+ `[TestDriver Reporter] ✅ Test run completion API response:`,
361
+ completeResponse,
362
+ );
363
+
364
+ console.log(
365
+ `[TestDriver Reporter] Test run completed: ${stats.passedTests}/${stats.totalTests} passed`,
366
+ );
367
+ } catch (error) {
368
+ console.error(
369
+ "[TestDriver Reporter] Failed to complete test run:",
370
+ error.message,
371
+ );
372
+ console.error("[TestDriver Reporter] Error stack:", error.stack);
373
+ }
374
+ }
375
+
376
+ onTestCaseReady(test) {
377
+ if (!pluginState.apiKey || !pluginState.testRun) return;
378
+
379
+ pluginState.testCases.set(test.id, {
380
+ test,
381
+ startTime: Date.now(),
382
+ });
383
+
384
+ // Try to detect platform from test context
385
+ detectPlatformFromTest(test);
386
+ }
387
+
388
+ async onTestCaseResult(test) {
389
+ if (!pluginState.apiKey || !pluginState.testRun) return;
390
+
391
+ const result = test.result();
392
+ const status =
393
+ result.state === "passed"
394
+ ? "passed"
395
+ : result.state === "skipped"
396
+ ? "skipped"
397
+ : "failed";
398
+
399
+ console.log(
400
+ `[TestDriver Reporter] Test case completed: ${test.name} (${status})`,
401
+ );
402
+
403
+ // Read test metadata from file (cross-process communication)
404
+ let dashcamUrl = null;
405
+ let testFile = "unknown";
406
+ let testOrder = 0;
407
+ let duration = result.duration || 0;
408
+
409
+ const testResultFile = path.join(
410
+ os.tmpdir(),
411
+ "testdriver-results",
412
+ `${test.id}.json`,
413
+ );
414
+
415
+ try {
416
+ if (fs.existsSync(testResultFile)) {
417
+ const testResult = JSON.parse(fs.readFileSync(testResultFile, "utf-8"));
418
+ dashcamUrl = testResult.dashcamUrl || null;
419
+ const platform = testResult.platform || null;
420
+ testFile =
421
+ testResult.testFile ||
422
+ test.file?.filepath ||
423
+ test.file?.name ||
424
+ "unknown";
425
+ testOrder =
426
+ testResult.testOrder !== undefined ? testResult.testOrder : 0;
427
+ duration = testResult.duration || result.duration || 0;
428
+
429
+ console.log(
430
+ `[TestDriver Reporter] ✅ Read from file - dashcam: ${dashcamUrl}, platform: ${platform}, testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms`,
431
+ );
432
+
433
+ // Update test run platform from first test that reports it
434
+ if (platform && !pluginState.detectedPlatform) {
435
+ pluginState.detectedPlatform = platform;
436
+ console.log(
437
+ `[TestDriver Reporter] 🖥️ Detected platform from test: ${platform}`,
438
+ );
439
+ }
440
+
441
+ // Clean up the file after reading
442
+ try {
443
+ fs.unlinkSync(testResultFile);
444
+ } catch {
445
+ // Ignore cleanup errors
446
+ }
447
+ } else {
448
+ console.log(
449
+ `[TestDriver Reporter] ⚠️ No result file found for test: ${test.id}`,
450
+ );
451
+ // Fallback to test object properties - try multiple sources
452
+ // In Vitest, the file path is typically on the module, not the test itself
453
+ const module = test.module || test.suite;
454
+ testFile =
455
+ test.file?.filepath ||
456
+ test.file?.name ||
457
+ module?.file?.filepath ||
458
+ module?.file?.name ||
459
+ test.location?.file ||
460
+ "unknown";
461
+ console.log(
462
+ `[TestDriver Reporter] 📂 Resolved testFile for skipped test: ${testFile}`,
463
+ );
464
+ }
465
+ } catch (error) {
466
+ console.error(
467
+ `[TestDriver Reporter] ❌ Failed to read test result file:`,
468
+ error.message,
469
+ );
470
+ // Fallback to test object properties - try multiple sources
471
+ const module = test.module || test.suite;
472
+ testFile =
473
+ test.file?.filepath ||
474
+ test.file?.name ||
475
+ module?.file?.filepath ||
476
+ module?.file?.name ||
477
+ test.location?.file ||
478
+ "unknown";
479
+ }
480
+
481
+ // Get test run info from environment variables
482
+ const testRunId = process.env.TD_TEST_RUN_ID;
483
+ const token = process.env.TD_TEST_RUN_TOKEN;
484
+
485
+ if (!testRunId || !token) {
486
+ console.warn(
487
+ `[TestDriver Reporter] ⚠️ Test run not initialized, skipping test case recording for: ${test.name}`,
488
+ );
489
+ return;
490
+ }
491
+
492
+ try {
493
+ let errorMessage = null;
494
+ let errorStack = null;
495
+
496
+ if (
497
+ result.state === "failed" &&
498
+ result.errors &&
499
+ result.errors.length > 0
500
+ ) {
501
+ const error = result.errors[0];
502
+ errorMessage = error.message;
503
+ errorStack = error.stack;
504
+ }
505
+
506
+ const suiteName = test.suite?.name;
507
+ const startTime = Date.now() - duration; // Calculate start time from duration
508
+
509
+ // Record test case with all metadata
510
+ const testCaseData = {
511
+ runId: testRunId,
512
+ testName: test.name,
513
+ testFile: testFile,
514
+ testOrder: testOrder,
515
+ status,
516
+ startTime: startTime,
517
+ endTime: Date.now(),
518
+ duration: duration,
519
+ retries: result.retryCount || 0,
520
+ };
521
+
522
+ // Only include replayUrl if we have a valid dashcam URL
523
+ if (dashcamUrl) {
524
+ testCaseData.replayUrl = dashcamUrl;
525
+ }
526
+
527
+ if (suiteName) testCaseData.suiteName = suiteName;
528
+ if (errorMessage) testCaseData.errorMessage = errorMessage;
529
+ if (errorStack) testCaseData.errorStack = errorStack;
530
+
531
+ console.log(
532
+ `[TestDriver Reporter] Recording test case: ${test.name} (${status}) with testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms, replay: ${dashcamUrl ? "yes" : "no"}`,
533
+ );
534
+
535
+ const testCaseResponse = await recordTestCaseDirect(
536
+ token,
537
+ pluginState.apiRoot,
538
+ testCaseData,
539
+ );
540
+
541
+ const testCaseDbId = testCaseResponse.data?.id;
542
+ const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
543
+
544
+ console.log(
545
+ `[TestDriver Reporter] ✅ Reported test case to API${dashcamUrl ? " with dashcam URL" : ""}`,
546
+ );
547
+ console.log(
548
+ `[TestDriver Reporter] 🔗 View test: ${pluginState.apiRoot.replace("testdriver-api.onrender.com", "app.testdriver.ai")}/test-runs/${testRunDbId}/${testCaseDbId}`,
549
+ );
550
+ } catch (error) {
551
+ console.error(
552
+ `[TestDriver Reporter] ❌ Failed to report test case:`,
553
+ error.message,
554
+ );
555
+ }
556
+ }
557
+ }
558
+
559
+ // ============================================================================
560
+ // Helper Functions
561
+ // ============================================================================
562
+
563
+ function generateRunId() {
564
+ return `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
565
+ }
566
+
567
+ function getSuiteName() {
568
+ return process.env.npm_package_name || path.basename(process.cwd());
569
+ }
570
+
571
+ function getPlatform() {
572
+ // First try to get platform from SDK client detected during test execution
573
+ if (pluginState.detectedPlatform) {
574
+ console.log(
575
+ `[TestDriver Plugin] Using platform from SDK client: ${pluginState.detectedPlatform}`,
576
+ );
577
+ return pluginState.detectedPlatform;
578
+ }
579
+
580
+ console.log(`[TestDriver Plugin] Platform not yet detected from client`);
581
+ return null;
582
+ }
583
+
584
+ function detectPlatformFromTest(test) {
585
+ // Check if testdriver client is accessible via test context
586
+ const client = test.context?.testdriver || test.meta?.testdriver;
587
+
588
+ if (client && client.os) {
589
+ // Normalize platform value
590
+ let platform = client.os.toLowerCase();
591
+ if (platform === "darwin" || platform === "mac") platform = "mac";
592
+ else if (platform === "win32" || platform === "windows")
593
+ platform = "windows";
594
+ else if (platform === "linux") platform = "linux";
595
+
596
+ pluginState.detectedPlatform = platform;
597
+ console.log(
598
+ `[TestDriver Plugin] Detected platform from test context: ${platform}`,
599
+ );
600
+ }
601
+ }
602
+
603
+ function calculateStatsFromModules(testModules) {
604
+ let totalTests = 0;
605
+ let passedTests = 0;
606
+ let failedTests = 0;
607
+ let skippedTests = 0;
608
+
609
+ for (const testModule of testModules) {
610
+ for (const testCase of testModule.children.allTests()) {
611
+ totalTests++;
612
+ const result = testCase.result();
613
+ if (result.state === "passed") passedTests++;
614
+ else if (result.state === "failed") failedTests++;
615
+ else if (result.state === "skipped") skippedTests++;
616
+ }
617
+ }
618
+
619
+ return { totalTests, passedTests, failedTests, skippedTests };
620
+ }
621
+
622
+ function detectCI() {
623
+ if (process.env.GITHUB_ACTIONS) return "github";
624
+ if (process.env.GITLAB_CI) return "gitlab";
625
+ if (process.env.CIRCLECI) return "circle";
626
+ if (process.env.TRAVIS) return "travis";
627
+ if (process.env.JENKINS_URL) return "jenkins";
628
+ if (process.env.BUILDKITE) return "buildkite";
629
+ return null;
630
+ }
631
+
632
+ function getGitInfo() {
633
+ const info = {};
634
+
635
+ if (process.env.GITHUB_ACTIONS) {
636
+ if (process.env.GITHUB_REPOSITORY)
637
+ info.repo = process.env.GITHUB_REPOSITORY;
638
+ if (process.env.GITHUB_REF_NAME) info.branch = process.env.GITHUB_REF_NAME;
639
+ if (process.env.GITHUB_SHA) info.commit = process.env.GITHUB_SHA;
640
+ if (process.env.GITHUB_ACTOR) info.author = process.env.GITHUB_ACTOR;
641
+ } else if (process.env.GITLAB_CI) {
642
+ if (process.env.CI_PROJECT_PATH) info.repo = process.env.CI_PROJECT_PATH;
643
+ if (process.env.CI_COMMIT_BRANCH)
644
+ info.branch = process.env.CI_COMMIT_BRANCH;
645
+ if (process.env.CI_COMMIT_SHA) info.commit = process.env.CI_COMMIT_SHA;
646
+ if (process.env.GITLAB_USER_LOGIN)
647
+ info.author = process.env.GITLAB_USER_LOGIN;
648
+ } else if (process.env.CIRCLECI) {
649
+ if (
650
+ process.env.CIRCLE_PROJECT_USERNAME &&
651
+ process.env.CIRCLE_PROJECT_REPONAME
652
+ ) {
653
+ info.repo = `${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}`;
654
+ }
655
+ if (process.env.CIRCLE_BRANCH) info.branch = process.env.CIRCLE_BRANCH;
656
+ if (process.env.CIRCLE_SHA1) info.commit = process.env.CIRCLE_SHA1;
657
+ if (process.env.CIRCLE_USERNAME) info.author = process.env.CIRCLE_USERNAME;
658
+ }
659
+
660
+ return info;
661
+ }
662
+
663
+ // ============================================================================
664
+ // API Methods
665
+ // ============================================================================
666
+
667
+ async function authenticate() {
668
+ const url = `${pluginState.apiRoot}/auth/exchange-api-key`;
669
+ const response = await withTimeout(
670
+ fetch(url, {
671
+ method: "POST",
672
+ headers: {
673
+ "Content-Type": "application/json",
674
+ },
675
+ body: JSON.stringify({
676
+ apiKey: pluginState.apiKey,
677
+ }),
678
+ }),
679
+ 10000,
680
+ "Internal Authentication",
681
+ );
682
+
683
+ if (!response.ok) {
684
+ throw new Error(
685
+ `Authentication failed: ${response.status} ${response.statusText}`,
686
+ );
687
+ }
688
+
689
+ const data = await response.json();
690
+ pluginState.token = data.token;
691
+ }
692
+
693
+ async function createTestRun(data) {
694
+ const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-create`;
695
+ const response = await withTimeout(
696
+ fetch(url, {
697
+ method: "POST",
698
+ headers: {
699
+ "Content-Type": "application/json",
700
+ Authorization: `Bearer ${pluginState.token}`,
701
+ },
702
+ body: JSON.stringify(data),
703
+ }),
704
+ 10000,
705
+ "Internal Create Test Run",
706
+ );
707
+
708
+ if (!response.ok) {
709
+ const errorText = await response.text();
710
+ throw new Error(
711
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
712
+ );
713
+ }
714
+
715
+ return await response.json();
716
+ }
717
+
718
+ async function completeTestRun(data) {
719
+ const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-complete`;
720
+ const response = await withTimeout(
721
+ fetch(url, {
722
+ method: "POST",
723
+ headers: {
724
+ "Content-Type": "application/json",
725
+ Authorization: `Bearer ${pluginState.token}`,
726
+ },
727
+ body: JSON.stringify(data),
728
+ }),
729
+ 10000,
730
+ "Internal Complete Test Run",
731
+ );
732
+
733
+ if (!response.ok) {
734
+ const errorText = await response.text();
735
+ throw new Error(
736
+ `API error: ${response.status} ${response.statusText} - ${errorText}`,
737
+ );
738
+ }
739
+
740
+ return await response.json();
741
+ }
742
+
743
+ // Global state setup moved to setup file (vitestSetup.mjs)
744
+ // The setup file imports the exported functions and makes them available globally in worker processes