testdriverai 7.0.0 → 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 (112) hide show
  1. package/AGENTS.md +550 -0
  2. package/CODEOWNERS +0 -1
  3. package/README.md +126 -0
  4. package/agent/index.js +43 -18
  5. package/agent/lib/commands.js +794 -135
  6. package/agent/lib/redraw.js +124 -39
  7. package/agent/lib/sandbox.js +10 -1
  8. package/agent/lib/sdk.js +21 -0
  9. package/docs/MIGRATION.md +425 -0
  10. package/docs/PRESETS.md +210 -0
  11. package/docs/docs.json +91 -37
  12. package/docs/guide/best-practices-polling.mdx +154 -0
  13. package/docs/v7/api/dashcam.mdx +497 -0
  14. package/docs/v7/api/doubleClick.mdx +102 -0
  15. package/docs/v7/api/mouseDown.mdx +161 -0
  16. package/docs/v7/api/mouseUp.mdx +164 -0
  17. package/docs/v7/api/rightClick.mdx +123 -0
  18. package/docs/v7/getting-started/configuration.mdx +380 -0
  19. package/docs/v7/getting-started/quickstart.mdx +273 -140
  20. package/docs/v7/guides/best-practices.mdx +486 -0
  21. package/docs/v7/guides/caching-ai.mdx +215 -0
  22. package/docs/v7/guides/caching-selectors.mdx +292 -0
  23. package/docs/v7/guides/caching.mdx +366 -0
  24. package/docs/v7/guides/ci-cd/azure.mdx +587 -0
  25. package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
  26. package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
  27. package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
  28. package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
  29. package/docs/v7/guides/ci-cd/travis.mdx +438 -0
  30. package/docs/v7/guides/debugging.mdx +349 -0
  31. package/docs/v7/guides/faq.mdx +393 -0
  32. package/docs/v7/guides/performance.mdx +517 -0
  33. package/docs/v7/guides/troubleshooting.mdx +526 -0
  34. package/docs/v7/guides/vitest-plugin.mdx +477 -0
  35. package/docs/v7/guides/vitest.mdx +535 -0
  36. package/docs/v7/platforms/linux.mdx +308 -0
  37. package/docs/v7/platforms/macos.mdx +433 -0
  38. package/docs/v7/platforms/windows.mdx +430 -0
  39. package/docs/v7/presets/chrome-extension.mdx +223 -0
  40. package/docs/v7/presets/chrome.mdx +287 -0
  41. package/docs/v7/presets/electron.mdx +435 -0
  42. package/docs/v7/presets/vscode.mdx +398 -0
  43. package/docs/v7/presets/webapp.mdx +396 -0
  44. package/docs/v7/progressive-apis/CORE.md +459 -0
  45. package/docs/v7/progressive-apis/HOOKS.md +360 -0
  46. package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
  47. package/docs/v7/progressive-apis/PROVISION.md +266 -0
  48. package/interfaces/vitest-plugin.mjs +186 -100
  49. package/package.json +12 -1
  50. package/sdk.d.ts +335 -42
  51. package/sdk.js +756 -95
  52. package/src/core/Dashcam.js +469 -0
  53. package/src/core/index.d.ts +150 -0
  54. package/src/core/index.js +12 -0
  55. package/src/presets/index.mjs +331 -0
  56. package/src/vitest/extended.mjs +108 -0
  57. package/src/vitest/hooks.d.ts +119 -0
  58. package/src/vitest/hooks.mjs +298 -0
  59. package/src/vitest/index.mjs +64 -0
  60. package/src/vitest/lifecycle.mjs +277 -0
  61. package/src/vitest/utils.mjs +150 -0
  62. package/test/dashcam.test.js +137 -0
  63. package/testdriver/acceptance-sdk/assert.test.mjs +13 -31
  64. package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
  65. package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
  66. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +7 -19
  67. package/testdriver/acceptance-sdk/element-not-found.test.mjs +6 -19
  68. package/testdriver/acceptance-sdk/exec-js.test.mjs +6 -18
  69. package/testdriver/acceptance-sdk/exec-output.test.mjs +8 -20
  70. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +13 -25
  71. package/testdriver/acceptance-sdk/focus-window.test.mjs +8 -20
  72. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +5 -20
  73. package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
  74. package/testdriver/acceptance-sdk/hover-image.test.mjs +10 -19
  75. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +7 -19
  76. package/testdriver/acceptance-sdk/hover-text.test.mjs +5 -19
  77. package/testdriver/acceptance-sdk/match-image.test.mjs +7 -19
  78. package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
  79. package/testdriver/acceptance-sdk/press-keys.test.mjs +5 -19
  80. package/testdriver/acceptance-sdk/prompt.test.mjs +6 -18
  81. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +6 -20
  82. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +6 -18
  83. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +9 -23
  84. package/testdriver/acceptance-sdk/scroll.test.mjs +12 -21
  85. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +124 -352
  86. package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
  87. package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
  88. package/testdriver/acceptance-sdk/type.test.mjs +19 -58
  89. package/vitest.config.mjs +1 -0
  90. package/.vscode/mcp.json +0 -9
  91. package/MIGRATION.md +0 -389
  92. package/PLUGIN_MIGRATION.md +0 -222
  93. package/PROMPT_CACHE.md +0 -200
  94. package/SDK_LOGGING.md +0 -222
  95. package/SDK_MIGRATION.md +0 -474
  96. package/SDK_README.md +0 -1122
  97. package/debug-screenshot-1763401388589.png +0 -0
  98. package/examples/run-tests-with-recording.sh +0 -70
  99. package/examples/screenshot-example.js +0 -63
  100. package/examples/sdk-awesome-logs-demo.js +0 -177
  101. package/examples/sdk-cache-thresholds.js +0 -96
  102. package/examples/sdk-element-properties.js +0 -155
  103. package/examples/sdk-simple-example.js +0 -65
  104. package/examples/test-recording-example.test.js +0 -166
  105. package/mcp-server/AI_GUIDELINES.md +0 -57
  106. package/test-find-api.js +0 -73
  107. package/test-prompt-cache.js +0 -96
  108. package/test-sandbox-render.js +0 -28
  109. package/test-sdk-methods.js +0 -15
  110. package/test-sdk-refactor.js +0 -53
  111. package/test-stack-trace.mjs +0 -57
  112. package/testdriver/acceptance-sdk/setup/lifecycleHelpers.mjs +0 -239
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Test Helpers and Utilities
3
- * Shared functions for SDK tests
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)
4
6
  */
5
7
 
6
8
  import crypto from "crypto";
@@ -9,36 +11,62 @@ import fs from "fs";
9
11
  import os from "os";
10
12
  import path, { dirname } from "path";
11
13
  import { fileURLToPath } from "url";
14
+
15
+ // Import TestDriver SDK locally (for repo development)
12
16
  import TestDriver from "../../../sdk.js";
17
+
18
+ // Import plugin helpers
13
19
  import {
14
20
  addDashcamLog,
15
21
  authDashcam,
16
22
  launchChrome,
23
+ launchChromeExtension,
24
+ launchChromeForTesting,
17
25
  runPostrun,
18
26
  runPrerun,
27
+ runPrerunChromeExtension,
28
+ runPrerunChromeForTesting,
19
29
  startDashcam,
20
30
  stopDashcam,
21
31
  waitForPage,
22
- } from "./lifecycleHelpers.mjs";
32
+ } from "../../../src/vitest/lifecycle.mjs";
23
33
 
24
- // Re-export lifecycle helpers for backward compatibility
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
25
42
  export {
26
43
  addDashcamLog,
27
44
  authDashcam,
28
45
  launchChrome,
46
+ launchChromeExtension,
47
+ launchChromeForTesting,
29
48
  runPostrun,
30
49
  runPrerun,
50
+ runPrerunChromeExtension,
51
+ runPrerunChromeForTesting,
31
52
  startDashcam,
32
53
  stopDashcam,
33
54
  waitForPage
34
55
  };
35
56
 
57
+ // Re-export plugin utilities for backward compatibility
58
+ export {
59
+ retryAsync,
60
+ setupEventLogging,
61
+ sleep,
62
+ waitFor
63
+ };
64
+
36
65
  // Get the directory of the current module
37
66
  const __filename = fileURLToPath(import.meta.url);
38
67
  const __dirname = dirname(__filename);
39
68
 
40
69
  // Load environment variables from .env file in the project root
41
- // Go up 3 levels from setup/ to reach the project root
42
70
  const envPath = path.resolve(__dirname, "../../../.env");
43
71
  config({ path: envPath });
44
72
 
@@ -46,12 +74,12 @@ config({ path: envPath });
46
74
  console.log("🔧 Environment variables loaded from:", envPath);
47
75
  console.log(" TD_API_KEY:", process.env.TD_API_KEY ? "✓ Set" : "✗ Not set");
48
76
  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
- );
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
+ // =============================================================================
53
82
 
54
- // Global test results storage
55
83
  const testResults = {
56
84
  tests: [],
57
85
  startTime: Date.now(),
@@ -64,16 +92,10 @@ const testResults = {
64
92
  * @param {string|null} dashcamUrl - Dashcam URL if available
65
93
  * @param {Object} sessionInfo - Session information
66
94
  */
67
- export function storeTestResult(
68
- testName,
69
- testFile,
70
- dashcamUrl,
71
- sessionInfo = {},
72
- ) {
95
+ export function storeTestResult(testName, testFile, dashcamUrl, sessionInfo = {}) {
73
96
  console.log(`📝 Storing test result: ${testName}`);
74
97
  console.log(` Dashcam URL: ${dashcamUrl || "none"}`);
75
98
 
76
- // Extract replay object ID from dashcam URL
77
99
  let replayObjectId = null;
78
100
  if (dashcamUrl) {
79
101
  const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
@@ -113,7 +135,6 @@ export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
113
135
  const results = getTestResults();
114
136
  const dir = path.dirname(outputPath);
115
137
 
116
- // Create directory if it doesn't exist
117
138
  if (!fs.existsSync(dir)) {
118
139
  fs.mkdirSync(dir, { recursive: true });
119
140
  }
@@ -121,7 +142,6 @@ export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
121
142
  fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
122
143
  console.log(`\n📊 Test results saved to: ${outputPath}`);
123
144
 
124
- // Also print dashcam URLs to console
125
145
  console.log("\n🎥 Dashcam URLs:");
126
146
  results.tests.forEach((test) => {
127
147
  if (test.dashcamUrl) {
@@ -132,161 +152,48 @@ export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
132
152
  return results;
133
153
  }
134
154
 
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
- }
155
+ // =============================================================================
156
+ // REPO-SPECIFIC CLIENT CREATION
157
+ // =============================================================================
228
158
 
229
159
  /**
230
- * Create a configured TestDriver client
160
+ * Create a configured TestDriver client for this repo's tests
161
+ * Uses local SDK path for development
231
162
  * @param {Object} options - Additional options
232
- * @param {Object} options.task - Vitest task context (from beforeEach/it context)
233
163
  * @returns {TestDriver} Configured client
234
164
  */
235
165
  export function createTestClient(options = {}) {
236
- // Check if API key is set
237
166
  if (!process.env.TD_API_KEY) {
238
167
  console.error("\n❌ Error: TD_API_KEY is not set!");
239
168
  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
- );
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");
246
171
  console.error(" 3. Export it in your shell: export TD_API_KEY=your_key\n");
247
172
  throw new Error("TD_API_KEY environment variable is required");
248
173
  }
249
174
 
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
175
+ const osConfig = process.env.TEST_PLATFORM || "linux";
258
176
  const { task, ...clientOptions } = options;
177
+ const taskId = task?.id || task?.name || null;
259
178
 
260
179
  const client = new TestDriver(process.env.TD_API_KEY, {
261
180
  resolution: "1366x768",
262
181
  analytics: true,
263
- os: os, // Use OS from environment variable (windows or linux)
182
+ os: osConfig,
264
183
  apiKey: process.env.TD_API_KEY,
265
184
  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,
185
+ newSandbox: true,
186
+ ...clientOptions,
271
187
  });
272
188
 
273
- console.log(
274
- "🔧 createTestClient: SDK created, cacheThresholds =",
275
- client.cacheThresholds,
276
- );
277
-
189
+ console.log("🔧 createTestClient: SDK created, cacheThresholds =", client.cacheThresholds);
278
190
  console.log(`[TestHelpers] Client OS configured as: ${client.os}`);
279
191
 
280
- // Set Vitest task ID if available (for log filtering in parallel tests)
281
192
  if (taskId) {
282
193
  console.log(`[TestHelpers] Storing task ID on client: ${taskId}`);
283
- // Store task ID directly on client for later use in teardown
284
194
  client.vitestTaskId = taskId;
285
- } else {
286
- console.log(`[TestHelpers] No task ID available`);
287
195
  }
288
196
 
289
- // Enable detailed event logging if requested
290
197
  if (process.env.DEBUG_EVENTS === "true") {
291
198
  setupEventLogging(client);
292
199
  }
@@ -294,124 +201,67 @@ export function createTestClient(options = {}) {
294
201
  return client;
295
202
  }
296
203
 
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
- }
204
+ // =============================================================================
205
+ // REPO-SPECIFIC LOGIN HELPER (for TestDriver Sandbox)
206
+ // =============================================================================
346
207
 
347
208
  /**
348
- * Setup function to run before each test
349
- * Authenticates and connects to sandbox
209
+ * Perform login flow on TestDriver Sandbox
210
+ * This is specific to http://testdriver-sandbox.vercel.app/login
350
211
  * @param {TestDriver} client - TestDriver client
351
- * @param {Object} options - Connection options
352
- * @returns {Promise<Object>} Sandbox instance
212
+ * @param {string} username - Username (default: 'standard_user')
213
+ * @param {string} password - Password (default: retrieved from screen)
353
214
  */
354
- export async function setupTest(client, options = {}) {
355
- await client.auth();
356
- const instance = await client.connect({
357
- ...options,
358
- });
215
+ export async function performLogin(client, username = "standard_user", password = null) {
216
+ await client.focusApplication("Google Chrome");
359
217
 
360
- // Set up console interceptor after connection (needs sandbox to be connected)
361
- if (client.vitestTaskId) {
362
- setupConsoleInterceptor(client, client.vitestTaskId);
218
+ if (!password) {
219
+ password = await client.remember("the password");
363
220
  }
364
221
 
365
- // Run prerun lifecycle if enabled
366
- if (options.prerun !== false) {
367
- await runPrerun(client);
368
- }
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);
369
227
 
370
- return instance;
228
+ await client.pressKeys(["tab"]);
229
+ await client.type(password, { secret: true });
230
+
231
+ await client.pressKeys(["tab"]);
232
+ await client.pressKeys(["enter"]);
371
233
  }
372
234
 
235
+ // =============================================================================
236
+ // SUITE TEST RUN MANAGEMENT (CI/CD Integration)
237
+ // =============================================================================
238
+
373
239
  /**
374
240
  * Initialize a test run for the entire suite
375
- * Should be called once in beforeEach
376
241
  * @param {Object} suiteTask - Vitest suite task context
377
242
  * @returns {Promise<Object>} Test run info { runId, testRunDbId, token }
378
243
  */
379
244
  export async function initializeSuiteTestRun(suiteTask) {
380
245
  const apiKey = process.env.TD_API_KEY;
381
- const apiRoot =
382
- process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
246
+ const apiRoot = process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
383
247
 
384
248
  if (!apiKey || !globalThis.__testdriverPlugin) {
385
- console.log(
386
- `[TestHelpers] Skipping suite test run initialization - no API key or plugin`,
387
- );
249
+ console.log(`[TestHelpers] Skipping suite test run initialization - no API key or plugin`);
388
250
  return null;
389
251
  }
390
252
 
391
- // Check if test run already exists for this suite
392
- const existingRun = globalThis.__testdriverPlugin.getSuiteTestRun(
393
- suiteTask.id,
394
- );
253
+ const existingRun = globalThis.__testdriverPlugin.getSuiteTestRun(suiteTask.id);
395
254
  if (existingRun) {
396
- console.log(
397
- `[TestHelpers] Test run already exists for suite: ${existingRun.runId}`,
398
- );
255
+ console.log(`[TestHelpers] Test run already exists for suite: ${existingRun.runId}`);
399
256
  return existingRun;
400
257
  }
401
258
 
402
259
  try {
403
- console.log(
404
- `[TestHelpers] Initializing test run for suite: ${suiteTask.name}`,
405
- );
260
+ console.log(`[TestHelpers] Initializing test run for suite: ${suiteTask.name}`);
406
261
 
407
- // Authenticate
408
- const token = await globalThis.__testdriverPlugin.authenticateWithApiKey(
409
- apiKey,
410
- apiRoot,
411
- );
262
+ const token = await globalThis.__testdriverPlugin.authenticateWithApiKey(apiKey, apiRoot);
412
263
  console.log(`[TestHelpers] ✅ Authenticated for suite`);
413
264
 
414
- // Create test run for the suite
415
265
  const runId = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
416
266
  const testFile = suiteTask.file?.name || "unknown";
417
267
  const testRunData = {
@@ -419,46 +269,53 @@ export async function initializeSuiteTestRun(suiteTask) {
419
269
  suiteName: suiteTask.name || testFile,
420
270
  };
421
271
 
422
- const testRunResponse =
423
- await globalThis.__testdriverPlugin.createTestRunDirect(
424
- token,
425
- apiRoot,
426
- testRunData,
427
- );
272
+ const testRunResponse = await globalThis.__testdriverPlugin.createTestRunDirect(
273
+ token,
274
+ apiRoot,
275
+ testRunData,
276
+ );
428
277
  const testRunDbId = testRunResponse.data?.id;
429
278
 
430
279
  const runInfo = { runId, testRunDbId, token };
431
-
432
- // Store in plugin state
433
280
  globalThis.__testdriverPlugin.setSuiteTestRun(suiteTask.id, runInfo);
434
281
 
435
- // Set environment variables for the reporter to use
436
282
  process.env.TD_TEST_RUN_ID = runId;
437
283
  process.env.TD_TEST_RUN_DB_ID = testRunDbId;
438
284
  process.env.TD_TEST_RUN_TOKEN = token;
439
285
 
440
- console.log(
441
- `[TestHelpers] ✅ Created test run for suite: ${runId} (DB ID: ${testRunDbId})`,
442
- );
443
-
286
+ console.log(`[TestHelpers] ✅ Created test run for suite: ${runId} (DB ID: ${testRunDbId})`);
444
287
  return runInfo;
445
288
  } catch (error) {
446
- console.error(
447
- `[TestHelpers] ❌ Failed to initialize suite test run:`,
448
- error.message,
449
- );
289
+ console.error(`[TestHelpers] ❌ Failed to initialize suite test run:`, error.message);
450
290
  return null;
451
291
  }
452
292
  }
453
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
+
454
315
  /**
455
316
  * Teardown function to run after each test
456
317
  * @param {TestDriver} client - TestDriver client
457
318
  * @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
319
  * @returns {Promise<Object>} Session info including dashcam URL
463
320
  */
464
321
  export async function teardownTest(client, options = {}) {
@@ -467,14 +324,10 @@ export async function teardownTest(client, options = {}) {
467
324
  console.log("🧹 Running teardown...");
468
325
 
469
326
  try {
470
- // Run postrun lifecycle if enabled and dashcamUrl not already provided
471
327
  if (options.postrun !== false && !dashcamUrl) {
472
328
  dashcamUrl = await runPostrun(client);
473
329
 
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}
330
+ if (dashcamUrl && options.task) {
478
331
  const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
479
332
  const replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
480
333
 
@@ -483,20 +336,15 @@ export async function teardownTest(client, options = {}) {
483
336
  console.log(`📝 Replay Object ID: ${replayObjectId}`);
484
337
  }
485
338
 
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
- }
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}`);
494
342
  }
495
343
  } else {
496
344
  console.log("⏭️ Postrun skipped (disabled in options)");
497
345
  }
498
346
 
499
- // Write test result to a file for the reporter to pick up (cross-process communication)
347
+ // Write test result to temp file for reporter
500
348
  if (options.task) {
501
349
  const testResultFile = path.join(
502
350
  os.tmpdir(),
@@ -505,58 +353,42 @@ export async function teardownTest(client, options = {}) {
505
353
  );
506
354
 
507
355
  try {
508
- // Ensure directory exists
509
356
  const dir = path.dirname(testResultFile);
510
357
  if (!fs.existsSync(dir)) {
511
358
  fs.mkdirSync(dir, { recursive: true });
512
359
  }
513
360
 
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)
361
+ const testFile = options.task.file?.filepath || options.task.file?.name || "unknown";
519
362
  let testOrder = 0;
520
363
  if (options.task.suite && options.task.suite.tasks) {
521
364
  testOrder = options.task.suite.tasks.indexOf(options.task);
522
365
  }
523
366
 
524
- // Note: Duration is calculated by Vitest and passed via result.duration
525
- // We don't need to track start time ourselves
367
+ const result = options.task.result?.();
368
+ const duration = result?.duration || 0;
526
369
 
527
- // Write test result with dashcam URL, platform, and metadata
528
370
  const testResult = {
529
371
  testId: options.task.id,
530
372
  testName: options.task.name,
531
373
  testFile: testFile,
532
374
  testOrder: testOrder,
533
375
  dashcamUrl: dashcamUrl,
534
- replayObjectId: dashcamUrl
535
- ? dashcamUrl.match(/\/replay\/([^?]+)/)?.[1]
536
- : null,
537
- platform: client.os, // Include platform from SDK client (source of truth)
376
+ replayObjectId: dashcamUrl ? dashcamUrl.match(/\/replay\/([^?]+)/)?.[1] : null,
377
+ platform: client.os,
538
378
  timestamp: Date.now(),
379
+ duration: duration,
539
380
  };
540
381
 
541
382
  fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
542
- console.log(
543
- `[TestHelpers] ✅ Wrote test result to file: ${testResultFile} (testFile: ${testFile}, testOrder: ${testOrder})`,
544
- );
383
+ console.log(`[TestHelpers] ✅ Wrote test result to file: ${testResultFile}`);
545
384
  } catch (error) {
546
- console.error(
547
- `[TestHelpers] ❌ Failed to write test result file:`,
548
- error.message,
549
- );
385
+ console.error(`[TestHelpers] ❌ Failed to write test result file:`, error.message);
550
386
  }
551
387
  }
552
388
  } catch (error) {
553
389
  console.error("❌ Error in postrun:", error);
554
390
  console.error("❌ Error stack:", error.stack);
555
391
  } finally {
556
- // Remove console interceptor before disconnecting
557
- removeConsoleInterceptor(client);
558
-
559
- // Only disconnect if not explicitly disabled
560
392
  if (options.disconnect !== false) {
561
393
  console.log("🔌 Disconnecting client...");
562
394
  try {
@@ -564,14 +396,12 @@ export async function teardownTest(client, options = {}) {
564
396
  console.log("✅ Client disconnected");
565
397
  } catch (disconnectError) {
566
398
  console.error("❌ Error disconnecting:", disconnectError.message);
567
- // Don't throw - we're already in cleanup
568
399
  }
569
400
  } else {
570
401
  console.log("⏭️ Disconnect skipped (disabled in options)");
571
402
  }
572
403
  }
573
404
 
574
- // Extract replay object ID from dashcam URL
575
405
  let replayObjectId = null;
576
406
  if (dashcamUrl) {
577
407
  const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
@@ -586,63 +416,5 @@ export async function teardownTest(client, options = {}) {
586
416
  };
587
417
 
588
418
  console.log("📊 Session info:", JSON.stringify(sessionInfo, null, 2));
589
-
590
419
  return sessionInfo;
591
420
  }
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
- }