testdriverai 7.2.20 → 7.2.22

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.
@@ -1,541 +0,0 @@
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 path, { dirname } from "path";
9
- import { fileURLToPath } from "url";
10
- import TestDriver from "../../../sdk.js";
11
- import {
12
- addDashcamLog,
13
- authDashcam,
14
- launchChrome,
15
- runPostrun,
16
- runPrerun,
17
- startDashcam,
18
- stopDashcam,
19
- waitForPage,
20
- } from "./lifecycleHelpers.mjs";
21
-
22
- // Re-export lifecycle helpers for backward compatibility
23
- export {
24
- addDashcamLog,
25
- authDashcam,
26
- launchChrome,
27
- runPostrun,
28
- runPrerun,
29
- startDashcam,
30
- stopDashcam,
31
- waitForPage
32
- };
33
-
34
- // Get the directory of the current module
35
- const __filename = fileURLToPath(import.meta.url);
36
- const __dirname = dirname(__filename);
37
-
38
- // Load environment variables from .env file in the project root
39
- // Go up 3 levels from setup/ to reach the project root
40
- const envPath = path.resolve(__dirname, "../../../.env");
41
- config({ path: envPath });
42
-
43
- // Log loaded env vars for debugging
44
- console.log("🔧 Environment variables loaded from:", envPath);
45
- console.log(" TD_API_KEY:", process.env.TD_API_KEY ? "✓ Set" : "✗ Not set");
46
- console.log(" TD_API_ROOT:", process.env.TD_API_ROOT || "Not set");
47
- console.log(
48
- " TD_OS:",
49
- process.env.TD_OS || "Not set (will default to linux)",
50
- );
51
-
52
- /**
53
- * Intercept console logs and forward to TestDriver sandbox
54
- * @param {TestDriver} client - TestDriver client instance
55
- * @param {string} taskId - Unique task identifier for this test
56
- */
57
- function setupConsoleInterceptor(client, taskId) {
58
- // Store original console methods
59
- const originalConsole = {
60
- log: console.log,
61
- error: console.error,
62
- warn: console.warn,
63
- info: console.info,
64
- };
65
-
66
- // Create wrapper that forwards to sandbox
67
- const createInterceptor = (level, originalMethod) => {
68
- return function (...args) {
69
- // Call original console method first
70
- originalMethod.apply(console, args);
71
-
72
- // Forward to sandbox if connected
73
- if (client.sandbox && client.sandbox.instanceSocketConnected) {
74
- try {
75
- // Format the log message
76
- const message = args
77
- .map((arg) =>
78
- typeof arg === "object"
79
- ? JSON.stringify(arg, null, 2)
80
- : String(arg),
81
- )
82
- .join(" ");
83
-
84
- // Preserve ANSI color codes and emojis for rich sandbox output
85
- // (don't add level prefix - sdk-log-formatter handles styling)
86
- const logOutput = message;
87
-
88
- client.sandbox.send({
89
- type: "output",
90
- output: Buffer.from(logOutput, "utf8").toString("base64"),
91
- });
92
- } catch (error) {
93
- // Silently fail to avoid breaking the test
94
- // Use original console to avoid infinite loop
95
- originalConsole.error(
96
- `[TestHelpers] Failed to forward log to sandbox:`,
97
- error.message,
98
- );
99
- }
100
- }
101
- };
102
- };
103
-
104
- // Replace console methods with interceptors
105
- console.log = createInterceptor("log", originalConsole.log);
106
- console.error = createInterceptor("error", originalConsole.error);
107
- console.warn = createInterceptor("warn", originalConsole.warn);
108
- console.info = createInterceptor("info", originalConsole.info);
109
-
110
- // Store original methods and taskId on client for cleanup
111
- client._consoleInterceptor = {
112
- taskId,
113
- original: originalConsole,
114
- };
115
-
116
- // Use original console for this message
117
- originalConsole.log(
118
- `[TestHelpers] Console interceptor enabled for task: ${taskId}`,
119
- );
120
- }
121
-
122
- /**
123
- * Remove console interceptor and restore original console methods
124
- * @param {TestDriver} client - TestDriver client instance
125
- */
126
- function removeConsoleInterceptor(client) {
127
- if (client._consoleInterceptor) {
128
- const { original, taskId } = client._consoleInterceptor;
129
-
130
- // Restore original console methods
131
- console.log = original.log;
132
- console.error = original.error;
133
- console.warn = original.warn;
134
- console.info = original.info;
135
-
136
- // Use original console for cleanup message
137
- original.log(
138
- `[TestHelpers] Console interceptor removed for task: ${taskId}`,
139
- );
140
-
141
- // Clean up reference
142
- delete client._consoleInterceptor;
143
- }
144
- }
145
-
146
- /**
147
- * Create a configured TestDriver client
148
- * @param {Object} options - Additional options
149
- * @param {Object} options.task - Vitest task context (from beforeEach/it context)
150
- * @returns {TestDriver} Configured client
151
- */
152
- export function createTestClient(options = {}) {
153
- // Check if API key is set
154
- if (!process.env.TD_API_KEY) {
155
- console.error("\n❌ Error: TD_API_KEY is not set!");
156
- console.error("Please set it in one of the following ways:");
157
- console.error(
158
- " 1. Create a .env file in the project root with: TD_API_KEY=your_key",
159
- );
160
- console.error(
161
- " 2. Pass it as an environment variable: TD_API_KEY=your_key npm run test:sdk",
162
- );
163
- console.error(" 3. Export it in your shell: export TD_API_KEY=your_key\n");
164
- throw new Error("TD_API_KEY environment variable is required");
165
- }
166
-
167
- // Determine OS from TEST_PLATFORM or TD_OS
168
- const os = process.env.TEST_PLATFORM || "linux";
169
-
170
- // Extract task context if provided - we use taskId but remove task from clientOptions
171
- let taskId = options.task?.id || options.task?.name || null;
172
-
173
- // Remove task from options before passing to TestDriver (eslint wants us to use 'task')
174
- // eslint-disable-next-line no-unused-vars
175
- const { task, ...clientOptions } = options;
176
-
177
- const client = new TestDriver(process.env.TD_API_KEY, {
178
- resolution: "1366x768",
179
- analytics: true,
180
- os: os, // Use OS from environment variable (windows or linux)
181
- apiKey: process.env.TD_API_KEY,
182
- apiRoot: process.env.TD_API_ROOT || "https://testdriver-api.onrender.com",
183
- // headless: false,
184
- newSandbox: true,
185
- // ip: '18.217.194.23'
186
- // ...clientOptions,
187
- // cache: false,
188
- });
189
-
190
- console.log(
191
- "🔧 createTestClient: SDK created, cacheThresholds =",
192
- client.cacheThresholds,
193
- );
194
-
195
- console.log(`[TestHelpers] Client OS configured as: ${client.os}`);
196
-
197
- // Set Vitest task ID if available (for log filtering in parallel tests)
198
- if (taskId) {
199
- console.log(`[TestHelpers] Storing task ID on client: ${taskId}`);
200
- // Store task ID directly on client for later use in teardown
201
- client.vitestTaskId = taskId;
202
- } else {
203
- console.log(`[TestHelpers] No task ID available`);
204
- }
205
-
206
- // Enable detailed event logging if requested
207
- if (process.env.DEBUG_EVENTS === "true") {
208
- setupEventLogging(client);
209
- }
210
-
211
- return client;
212
- }
213
-
214
- /**
215
- * Set up detailed event logging for debugging
216
- * @param {TestDriver} client - TestDriver client
217
- */
218
- export function setupEventLogging(client) {
219
- const emitter = client.getEmitter();
220
-
221
- // Log all events
222
- emitter.on("**", function (data) {
223
- const event = this.event;
224
- if (event.startsWith("log:debug")) return; // Skip debug logs
225
- console.log(`[EVENT] ${event}`, data || "");
226
- });
227
-
228
- // Log command lifecycle
229
- emitter.on("command:start", (data) => {
230
- console.log("🚀 Command started:", data);
231
- });
232
-
233
- emitter.on("command:success", (data) => {
234
- console.log("✅ Command succeeded:", data);
235
- });
236
-
237
- emitter.on("command:error", (data) => {
238
- console.error("❌ Command error:", data);
239
- });
240
-
241
- // Log sandbox events
242
- emitter.on("sandbox:connected", () => {
243
- console.log("🔌 Sandbox connected");
244
- });
245
-
246
- emitter.on("sandbox:authenticated", () => {
247
- console.log("🔐 Sandbox authenticated");
248
- });
249
-
250
- emitter.on("sandbox:error", (error) => {
251
- console.error("⚠️ Sandbox error:", error);
252
- });
253
-
254
- // Log SDK API calls
255
- emitter.on("sdk:request", (data) => {
256
- console.log("📤 SDK Request:", data);
257
- });
258
-
259
- emitter.on("sdk:response", (data) => {
260
- console.log("📥 SDK Response:", data);
261
- });
262
- }
263
-
264
- /**
265
- * Setup function to run before each test
266
- * Authenticates and connects to sandbox
267
- * @param {TestDriver} client - TestDriver client
268
- * @param {Object} options - Connection options
269
- * @returns {Promise<Object>} Sandbox instance
270
- */
271
- export async function setupTest(client, options = {}) {
272
- await client.auth();
273
- const instance = await client.connect({
274
- ...options,
275
- });
276
-
277
- // Set up console interceptor after connection (needs sandbox to be connected)
278
- if (client.vitestTaskId) {
279
- setupConsoleInterceptor(client, client.vitestTaskId);
280
- }
281
-
282
- // Run prerun lifecycle if enabled
283
- if (options.prerun !== false) {
284
- await runPrerun(client);
285
- }
286
-
287
- return instance;
288
- }
289
-
290
- /**
291
- * Initialize a test run for the entire suite
292
- * Should be called once in beforeEach
293
- * @param {Object} suiteTask - Vitest suite task context
294
- * @returns {Promise<Object>} Test run info { runId, testRunDbId, token }
295
- */
296
- export async function initializeSuiteTestRun(suiteTask) {
297
- const apiKey = process.env.TD_API_KEY;
298
- const apiRoot =
299
- process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
300
-
301
- if (!apiKey || !globalThis.__testdriverPlugin) {
302
- console.log(
303
- `[TestHelpers] Skipping suite test run initialization - no API key or plugin`,
304
- );
305
- return null;
306
- }
307
-
308
- // Check if test run already exists for this suite
309
- const existingRun = globalThis.__testdriverPlugin.getSuiteTestRun(
310
- suiteTask.id,
311
- );
312
- if (existingRun) {
313
- console.log(
314
- `[TestHelpers] Test run already exists for suite: ${existingRun.runId}`,
315
- );
316
- return existingRun;
317
- }
318
-
319
- try {
320
- console.log(
321
- `[TestHelpers] Initializing test run for suite: ${suiteTask.name}`,
322
- );
323
-
324
- // Authenticate
325
- const token = await globalThis.__testdriverPlugin.authenticateWithApiKey(
326
- apiKey,
327
- apiRoot,
328
- );
329
- console.log(`[TestHelpers] ✅ Authenticated for suite`);
330
-
331
- // Create test run for the suite
332
- const runId = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
333
- const testFile = suiteTask.file?.name || "unknown";
334
- const testRunData = {
335
- runId,
336
- suiteName: suiteTask.name || testFile,
337
- };
338
-
339
- const testRunResponse =
340
- await globalThis.__testdriverPlugin.createTestRunDirect(
341
- token,
342
- apiRoot,
343
- testRunData,
344
- );
345
- const testRunDbId = testRunResponse.data?.id;
346
-
347
- const runInfo = { runId, testRunDbId, token };
348
-
349
- // Store in plugin state
350
- globalThis.__testdriverPlugin.setSuiteTestRun(suiteTask.id, runInfo);
351
-
352
- // Set environment variables for the reporter to use
353
- process.env.TD_TEST_RUN_ID = runId;
354
- process.env.TD_TEST_RUN_DB_ID = testRunDbId;
355
- process.env.TD_TEST_RUN_TOKEN = token;
356
-
357
- console.log(
358
- `[TestHelpers] ✅ Created test run for suite: ${runId} (DB ID: ${testRunDbId})`,
359
- );
360
-
361
- return runInfo;
362
- } catch (error) {
363
- console.error(
364
- `[TestHelpers] ❌ Failed to initialize suite test run:`,
365
- error.message,
366
- );
367
- return null;
368
- }
369
- }
370
-
371
- /**
372
- * Teardown function to run after each test
373
- * @param {TestDriver} client - TestDriver client
374
- * @param {Object} options - Teardown options
375
- * @param {Object} options.task - Vitest task context (optional, for storing in task.meta)
376
- * @param {string} options.dashcamUrl - Dashcam URL if already retrieved
377
- * @param {boolean} options.postrun - Whether to run postrun lifecycle (default: true)
378
- * @param {boolean} options.disconnect - Whether to disconnect client (default: true)
379
- * @returns {Promise<Object>} Session info including dashcam URL
380
- */
381
- export async function teardownTest(client, options = {}) {
382
- let dashcamUrl = options.dashcamUrl || null;
383
-
384
- console.log("🧹 Running teardown...");
385
-
386
- try {
387
- // Run postrun lifecycle if enabled and dashcamUrl not already provided
388
- if (options.postrun !== false && !dashcamUrl) {
389
- dashcamUrl = await runPostrun(client);
390
-
391
- // Store dashcamUrl in client for reporter access
392
- if (dashcamUrl) {
393
- // Extract replay object ID from URL
394
- // URL format: https://app.testdriver.ai/replay/{replayObjectId}?share={shareToken}
395
- const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
396
- const replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
397
-
398
- console.log(`🎥 Dashcam URL: ${dashcamUrl}`);
399
- if (replayObjectId) {
400
- console.log(`📝 Replay Object ID: ${replayObjectId}`);
401
- }
402
-
403
- // Store dashcam URL in task meta
404
- if (options.task) {
405
- options.task.meta.testdriverDashcamUrl = dashcamUrl;
406
- options.task.meta.testdriverReplayObjectId = replayObjectId;
407
- console.log(
408
- `[TestHelpers] ✅ Stored dashcam URL in task.meta for test: ${options.task.name}`,
409
- );
410
- }
411
- }
412
- } else {
413
- console.log("⏭️ Postrun skipped (disabled in options)");
414
- }
415
-
416
- // Use Vitest's task.meta for cross-process communication with the reporter
417
- if (options.task) {
418
- try {
419
- // Get test file path - make it relative to project root
420
- const absolutePath =
421
- options.task.file?.filepath || options.task.file?.name || "unknown";
422
- const projectRoot = process.cwd();
423
- const testFile = absolutePath !== "unknown"
424
- ? path.relative(projectRoot, absolutePath)
425
- : absolutePath;
426
-
427
- // Calculate test order (index within parent suite)
428
- let testOrder = 0;
429
- if (options.task.suite && options.task.suite.tasks) {
430
- testOrder = options.task.suite.tasks.indexOf(options.task);
431
- }
432
-
433
- // Set metadata on task for the reporter to pick up
434
- options.task.meta.dashcamUrl = dashcamUrl;
435
- options.task.meta.platform = client.os; // Include platform from SDK client (source of truth)
436
- options.task.meta.testFile = testFile;
437
- options.task.meta.testOrder = testOrder;
438
- options.task.meta.sessionId = client.getSessionId?.() || null;
439
- } catch (error) {
440
- console.error(
441
- `[TestHelpers] ❌ Failed to set test metadata:`,
442
- error.message,
443
- );
444
- }
445
- }
446
- } catch (error) {
447
- console.error("❌ Error in postrun:", error);
448
- console.error("❌ Error stack:", error.stack);
449
- } finally {
450
- // Remove console interceptor before disconnecting
451
- removeConsoleInterceptor(client);
452
-
453
- // Only disconnect if not explicitly disabled
454
- if (options.disconnect !== false) {
455
- console.log("🔌 Disconnecting client...");
456
- try {
457
- await client.disconnect();
458
- } catch (disconnectError) {
459
- console.error("❌ Error disconnecting:", disconnectError.message);
460
- // Don't throw - we're already in cleanup
461
- }
462
- } else {
463
- console.log("⏭️ Disconnect skipped (disabled in options)");
464
- }
465
- }
466
-
467
- // Extract replay object ID from dashcam URL
468
- let replayObjectId = null;
469
- if (dashcamUrl) {
470
- const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
471
- replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
472
- }
473
-
474
- const sessionInfo = {
475
- sessionId: client.getSessionId(),
476
- dashcamUrl: dashcamUrl,
477
- replayObjectId: replayObjectId,
478
- instance: client.getInstance(),
479
- };
480
-
481
- console.log("📊 Session info:", JSON.stringify(sessionInfo, null, 2));
482
-
483
- return sessionInfo;
484
- }
485
-
486
- /**
487
- * Perform login flow (reusable snippet)
488
- * @param {TestDriver} client - TestDriver client
489
- * @param {string} username - Username (default: 'standard_user')
490
- * @param {string} password - Password (default: retrieved from screen)
491
- */
492
- export async function performLogin(
493
- client,
494
- username = "standard_user",
495
- password = null,
496
- ) {
497
- await client.focusApplication("Google Chrome");
498
-
499
- // Get password from screen if not provided
500
- if (!password) {
501
- password = await client.extract("the password");
502
- }
503
-
504
- const usernameField = await client.find(
505
- "Username, label above the username input field on the login form",
506
- );
507
- await usernameField.click();
508
- await client.type(username);
509
-
510
- // Enter password (marked as secret so it's not logged or stored)
511
- await client.pressKeys(["tab"]);
512
- await client.type(password, { secret: true });
513
-
514
- // Submit form
515
- await client.pressKeys(["tab"]);
516
- await client.pressKeys(["enter"]);
517
- }
518
-
519
- /**
520
- * Wait with retry logic
521
- * @param {Function} fn - Async function to retry
522
- * @param {number} retries - Number of retries (default: 3)
523
- * @param {number} delay - Delay between retries in ms (default: 1000)
524
- * @returns {Promise} Result of successful execution
525
- */
526
- export async function retryAsync(fn, retries = 3, delay = 1000) {
527
- let lastError;
528
-
529
- for (let i = 0; i < retries; i++) {
530
- try {
531
- return await fn();
532
- } catch (error) {
533
- lastError = error;
534
- if (i < retries - 1) {
535
- await new Promise((resolve) => setTimeout(resolve, delay));
536
- }
537
- }
538
- }
539
-
540
- throw lastError;
541
- }
@@ -1,40 +0,0 @@
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
- }