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
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Vitest Hooks for TestDriver
3
+ *
4
+ * Provides lifecycle management for TestDriver in Vitest tests.
5
+ *
6
+ * @example
7
+ * import { TestDriver } from 'testdriverai/vitest/hooks';
8
+ *
9
+ * test('my test', async (context) => {
10
+ * const testdriver = TestDriver(context, { headless: true });
11
+ *
12
+ * await testdriver.ready();
13
+ * await testdriver.provision.chrome({ url: 'https://example.com' });
14
+ * await testdriver.find('button').click();
15
+ * });
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import os from 'os';
20
+ import path from 'path';
21
+ import TestDriverSDK from '../../sdk.js';
22
+
23
+ /**
24
+ * Intercept console logs and write to a log file on the remote machine
25
+ * This allows test logs to appear in Dashcam recordings
26
+ * @param {TestDriver} client - TestDriver client instance
27
+ * @param {string} taskId - Unique task identifier for this test
28
+ */
29
+ function setupConsoleInterceptor(client, taskId) {
30
+ // Store original console methods
31
+ const originalConsole = {
32
+ log: console.log,
33
+ error: console.error,
34
+ warn: console.warn,
35
+ info: console.info,
36
+ };
37
+
38
+ // Determine log file path based on OS
39
+ const logPath = client.os === "windows"
40
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
41
+ : "/tmp/testdriver.log";
42
+
43
+ // Store log path on client for later use
44
+ client._testLogPath = logPath;
45
+
46
+ // Track if we're currently writing to avoid infinite loops
47
+ let isWriting = false;
48
+
49
+ // Create wrapper that writes to log file
50
+ const createInterceptor = (level, originalMethod) => {
51
+ return function (...args) {
52
+ // Call original console method first
53
+ originalMethod.apply(console, args);
54
+
55
+ // Skip if already writing to avoid infinite loops
56
+ if (isWriting) return;
57
+
58
+ // Format the log message
59
+ const message = args
60
+ .map((arg) =>
61
+ typeof arg === "object"
62
+ ? JSON.stringify(arg, null, 2)
63
+ : String(arg),
64
+ )
65
+ .join(" ");
66
+
67
+ // Also send to sandbox for immediate visibility
68
+ if (client.sandbox && client.sandbox.instanceSocketConnected) {
69
+
70
+ client.sandbox.send({
71
+ type: "output",
72
+ output: Buffer.from(message, "utf8").toString("base64"),
73
+ });
74
+ }
75
+ };
76
+ };
77
+
78
+ // Replace console methods with interceptors
79
+ console.log = createInterceptor("log", originalConsole.log);
80
+ console.error = createInterceptor("error", originalConsole.error);
81
+ console.warn = createInterceptor("warn", originalConsole.warn);
82
+ console.info = createInterceptor("info", originalConsole.info);
83
+
84
+ // Store original methods and taskId on client for cleanup
85
+ client._consoleInterceptor = {
86
+ taskId,
87
+ original: originalConsole,
88
+ };
89
+
90
+ // Use original console for this message
91
+ originalConsole.log(
92
+ `[useTestDriver] Console interceptor enabled for task: ${taskId}`,
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Remove console interceptor and restore original console methods
98
+ * @param {TestDriver} client - TestDriver client instance
99
+ */
100
+ function removeConsoleInterceptor(client) {
101
+ if (client._consoleInterceptor) {
102
+ const { original, taskId } = client._consoleInterceptor;
103
+
104
+ // Restore original console methods
105
+ console.log = original.log;
106
+ console.error = original.error;
107
+ console.warn = original.warn;
108
+ console.info = original.info;
109
+
110
+ // Use original console for cleanup message
111
+ original.log(
112
+ `[useTestDriver] Console interceptor removed for task: ${taskId}`,
113
+ );
114
+
115
+ // Clean up reference
116
+ delete client._consoleInterceptor;
117
+ }
118
+ }
119
+
120
+ // Weak maps to store instances per test context
121
+ const testDriverInstances = new WeakMap();
122
+ const lifecycleHandlers = new WeakMap();
123
+
124
+ /**
125
+ * Create a TestDriver client in a Vitest test with automatic lifecycle management
126
+ *
127
+ * @param {object} context - Vitest test context (from async (context) => {})
128
+ * @param {object} options - TestDriver options (passed directly to TestDriver constructor)
129
+ * @param {string} [options.apiKey] - TestDriver API key (defaults to process.env.TD_API_KEY)
130
+ * @param {boolean} [options.headless] - Run sandbox in headless mode
131
+ * @param {boolean} [options.newSandbox] - Create new sandbox
132
+ * @param {boolean} [options.autoConnect=true] - Automatically connect to sandbox
133
+ * @returns {TestDriver} TestDriver client instance
134
+ *
135
+ * @example
136
+ * test('my test', async (context) => {
137
+ * const testdriver = TestDriver(context, { headless: true });
138
+ *
139
+ * // provision.chrome() automatically calls ready() and starts dashcam
140
+ * await testdriver.provision.chrome({ url: 'https://example.com' });
141
+ *
142
+ * await testdriver.find('Login button').click();
143
+ * });
144
+ */
145
+ export function TestDriver(context, options = {}) {
146
+ if (!context || !context.task) {
147
+ throw new Error('TestDriver() requires Vitest context. Pass the context parameter from your test function: test("name", async (context) => { ... })');
148
+ }
149
+
150
+ // Return existing instance if already created for this test
151
+ if (testDriverInstances.has(context.task)) {
152
+ return testDriverInstances.get(context.task);
153
+ }
154
+
155
+ // Get global plugin options if available
156
+ const pluginOptions = globalThis.__testdriverPlugin?.state?.testDriverOptions || {};
157
+
158
+ // Merge options: plugin global options < test-specific options
159
+ const mergedOptions = { ...pluginOptions, ...options };
160
+
161
+ // Extract TestDriver-specific options
162
+ const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
163
+
164
+ // Build config for TestDriverSDK constructor
165
+ const config = { ...mergedOptions };
166
+ delete config.apiKey;
167
+
168
+ // Use TD_API_ROOT from environment if not provided in config
169
+ if (!config.apiRoot && process.env.TD_API_ROOT) {
170
+ config.apiRoot = process.env.TD_API_ROOT;
171
+ }
172
+
173
+ const testdriver = new TestDriverSDK(apiKey, config);
174
+ testdriver.__vitestContext = context.task;
175
+ testDriverInstances.set(context.task, testdriver);
176
+
177
+ // Auto-connect if enabled (default: true)
178
+ const autoConnect = config.autoConnect !== undefined ? config.autoConnect : true;
179
+ if (autoConnect) {
180
+ testdriver.__connectionPromise = (async () => {
181
+ try {
182
+ console.log('[testdriver] Connecting to sandbox...');
183
+ await testdriver.auth();
184
+ await testdriver.connect();
185
+ console.log('[testdriver] ✅ Connected to sandbox');
186
+
187
+ // Set up console interceptor after connection
188
+ setupConsoleInterceptor(testdriver, context.task.id);
189
+
190
+ // Create the log file on the remote machine
191
+ const shell = testdriver.os === "windows" ? "pwsh" : "sh";
192
+ const logPath = testdriver.os === "windows"
193
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
194
+ : "/tmp/testdriver.log";
195
+
196
+ const createLogCmd = testdriver.os === "windows"
197
+ ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
198
+ : `touch ${logPath}`;
199
+
200
+ await testdriver.exec(shell, createLogCmd, 10000, true);
201
+ console.log('[testdriver] ✅ Created log file:', logPath);
202
+
203
+ // Add automatic log tracking when dashcam starts
204
+ // Store original start method
205
+ const originalDashcamStart = testdriver.dashcam.start.bind(testdriver.dashcam);
206
+ testdriver.dashcam.start = async function() {
207
+ // Call original start (which handles auth)
208
+ await originalDashcamStart();
209
+
210
+ // Add log file tracking after dashcam starts
211
+ try {
212
+ await testdriver.dashcam.addFileLog(logPath, "TestDriver Log");
213
+ console.log('[testdriver] ✅ Added log file to dashcam tracking');
214
+ } catch (error) {
215
+ console.warn('[testdriver] ⚠️ Failed to add log tracking:', error.message);
216
+ }
217
+ };
218
+ } catch (error) {
219
+ console.error('[testdriver] Error during setup:', error);
220
+ throw error;
221
+ }
222
+ })();
223
+ }
224
+
225
+ // Register cleanup handler with dashcam.stop()
226
+ if (!lifecycleHandlers.has(context.task)) {
227
+ const cleanup = async () => {
228
+ console.log('[testdriver] Cleaning up TestDriver client...');
229
+ try {
230
+ // Stop dashcam if it was started
231
+ if (testdriver._dashcam && testdriver._dashcam.recording) {
232
+ try {
233
+ const dashcamUrl = await testdriver.dashcam.stop();
234
+ console.log('🎥 Dashcam URL:', dashcamUrl);
235
+
236
+ // Write dashcam URL to file for the reporter (cross-process communication)
237
+ if (dashcamUrl) {
238
+ const testId = context.task.id;
239
+ const platform = testdriver.os || 'linux';
240
+ const testFile = context.task.file?.filepath || context.task.file?.name || 'unknown';
241
+
242
+ // Create results directory if it doesn't exist
243
+ const resultsDir = path.join(os.tmpdir(), 'testdriver-results');
244
+ if (!fs.existsSync(resultsDir)) {
245
+ fs.mkdirSync(resultsDir, { recursive: true });
246
+ }
247
+
248
+ // Write test result file
249
+ const testResultFile = path.join(resultsDir, `${testId}.json`);
250
+ const testResult = {
251
+ dashcamUrl,
252
+ platform,
253
+ testFile,
254
+ testOrder: 0,
255
+ sessionId: testdriver.getSessionId(),
256
+ };
257
+
258
+ fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
259
+ console.log(`[testdriver] ✅ Wrote dashcam URL to ${testResultFile}`);
260
+
261
+ // Also register in memory if plugin is available
262
+ if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
263
+ globalThis.__testdriverPlugin.registerDashcamUrl(testId, dashcamUrl, platform);
264
+ console.log(`[testdriver] ✅ Registered dashcam URL in memory for test ${testId}`);
265
+ }
266
+ }
267
+ } catch (error) {
268
+ // Log more detailed error information for debugging
269
+ console.error('❌ Failed to stop dashcam:', error.name || error.constructor?.name || 'Error');
270
+ if (error.message) console.error(' Message:', error.message);
271
+ // NotFoundError during cleanup is expected if sandbox already terminated
272
+ if (error.name === 'NotFoundError' || error.responseData?.error === 'NotFoundError') {
273
+ console.log(' ℹ️ Sandbox session already terminated - dashcam stop skipped');
274
+ }
275
+ }
276
+ }
277
+
278
+ // Remove console interceptor before disconnecting
279
+ removeConsoleInterceptor(testdriver);
280
+
281
+ // Wait for connection to finish if it was initiated
282
+ if (testdriver.__connectionPromise) {
283
+ await testdriver.__connectionPromise.catch(() => {}); // Ignore connection errors during cleanup
284
+ }
285
+ await testdriver.disconnect();
286
+ console.log('✅ Client disconnected');
287
+ } catch (error) {
288
+ console.error('Error disconnecting client:', error);
289
+ }
290
+ };
291
+ lifecycleHandlers.set(context.task, cleanup);
292
+
293
+ // Vitest will call this automatically after the test
294
+ context.onTestFinished?.(cleanup);
295
+ }
296
+
297
+ return testdriver;
298
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * TestDriver Vitest Integration
3
+ *
4
+ * Main entry point for the TestDriver Vitest plugin.
5
+ *
6
+ * @example
7
+ * // Basic usage - auto-managed lifecycle
8
+ * import { TestDriver } from 'testdriverai/vitest';
9
+ *
10
+ * test('my test', async (context) => {
11
+ * const testdriver = TestDriver(context, { headless: true });
12
+ * await testdriver.provision.chrome({ url: 'https://example.com' });
13
+ * await testdriver.find('Login button').click();
14
+ * });
15
+ *
16
+ * @example
17
+ * // With extended test functions (it.once for setup steps)
18
+ * import { describe, it, expect, TestDriver } from 'testdriverai/vitest';
19
+ *
20
+ * describe('My Suite', () => {
21
+ * it.once('launch app', async (context) => {
22
+ * const testdriver = TestDriver(context);
23
+ * await testdriver.provision.chrome({ url: 'https://example.com' });
24
+ * });
25
+ *
26
+ * it('click button', async (context) => {
27
+ * const testdriver = TestDriver(context);
28
+ * await testdriver.find('Button').click();
29
+ * });
30
+ * });
31
+ *
32
+ * @example
33
+ * // Using lifecycle helpers directly
34
+ * import { TestDriver, launchChrome, waitForPage } from 'testdriverai/vitest';
35
+ *
36
+ * test('custom setup', async (context) => {
37
+ * const testdriver = TestDriver(context);
38
+ * await testdriver.ready();
39
+ * await launchChrome(testdriver, 'https://example.com', { guest: true });
40
+ * await waitForPage(testdriver, 'Welcome');
41
+ * });
42
+ */
43
+
44
+ // Core TestDriver hook
45
+ export { TestDriver } from './hooks.mjs';
46
+
47
+ // Extended Vitest functions
48
+ export {
49
+ afterAll, beforeAll, describe, expect, getTestDriver,
50
+ isReconnected, it,
51
+ test
52
+ } from './extended.mjs';
53
+
54
+ // Lifecycle helpers
55
+ export {
56
+ addDashcamLog, authDashcam, launchChrome, launchChromeExtension, launchChromeForTesting, runPostrun, runPrerun, runPrerunChromeExtension, runPrerunChromeForTesting, startDashcam,
57
+ stopDashcam, waitForPage
58
+ } from './lifecycle.mjs';
59
+
60
+ // Utility functions
61
+ export {
62
+ generateTestId, retryAsync, setupEventLogging, sleep, waitFor
63
+ } from './utils.mjs';
64
+
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Lifecycle Helpers for TestDriver Vitest Plugin
3
+ *
4
+ * Provides reusable lifecycle hook functions for common test patterns.
5
+ * These are thin wrappers around the Dashcam class.
6
+ *
7
+ * @example
8
+ * import { launchChrome, waitForPage } from 'testdriverai/vitest';
9
+ *
10
+ * test('my test', async (context) => {
11
+ * const testdriver = TestDriver(context);
12
+ * await testdriver.provision.chrome({ url: 'https://example.com' });
13
+ *
14
+ * // Or use manual lifecycle helpers
15
+ * await launchChrome(testdriver, 'https://other-site.com');
16
+ * await waitForPage(testdriver, 'Welcome');
17
+ * });
18
+ */
19
+
20
+ import Dashcam from '../core/Dashcam.js';
21
+
22
+ // Module-level cache to maintain Dashcam instance state across helper calls
23
+ const dashcamInstances = new WeakMap();
24
+
25
+ /**
26
+ * Get or create Dashcam instance for a client
27
+ * @private
28
+ * @param {TestDriver} client
29
+ * @param {object} options
30
+ * @returns {Dashcam}
31
+ */
32
+ function getDashcam(client, options = {}) {
33
+ if (!dashcamInstances.has(client)) {
34
+ dashcamInstances.set(client, new Dashcam(client, options));
35
+ }
36
+ return dashcamInstances.get(client);
37
+ }
38
+
39
+ /**
40
+ * Authenticate dashcam with API key
41
+ * @param {TestDriver} client - TestDriver client
42
+ * @param {string} apiKey - Dashcam API key (default from environment)
43
+ */
44
+ export async function authDashcam(client, apiKey) {
45
+ const dashcam = getDashcam(client, { apiKey });
46
+ await dashcam.auth();
47
+ }
48
+
49
+ /**
50
+ * Add log file tracking to dashcam
51
+ * @param {TestDriver} client - TestDriver client
52
+ * @param {string} logName - Name for the log in dashcam (default: "TestDriver Log")
53
+ */
54
+ export async function addDashcamLog(client, logName = "TestDriver Log") {
55
+ const dashcam = getDashcam(client);
56
+ const logPath = client.os === "windows"
57
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
58
+ : "/tmp/testdriver.log";
59
+
60
+ // Create log file first
61
+ const shell = client.os === "windows" ? "pwsh" : "sh";
62
+ if (client.os === "windows") {
63
+ await client.exec(shell, `New-Item -ItemType File -Path "${logPath}" -Force`, 10000, true);
64
+ } else {
65
+ await client.exec(shell, `touch ${logPath}`, 10000, true);
66
+ }
67
+
68
+ await dashcam.addFileLog(logPath, logName);
69
+ }
70
+
71
+ /**
72
+ * Start dashcam recording
73
+ * @param {TestDriver} client - TestDriver client
74
+ */
75
+ export async function startDashcam(client) {
76
+ const dashcam = getDashcam(client);
77
+ await dashcam.start();
78
+ }
79
+
80
+ /**
81
+ * Stop dashcam recording and retrieve URL
82
+ * @param {TestDriver} client - TestDriver client
83
+ * @returns {Promise<string|null>} Dashcam URL if available
84
+ */
85
+ export async function stopDashcam(client) {
86
+ console.log("🎬 Stopping dashcam and retrieving URL...");
87
+ const dashcam = getDashcam(client);
88
+ const url = await dashcam.stop();
89
+
90
+ if (url) {
91
+ console.log("✅ Found dashcam URL:", url);
92
+ console.log("🎥 Dashcam URL:", url);
93
+ } else {
94
+ console.warn("⚠️ No replay URL found in dashcam output");
95
+ }
96
+
97
+ return url;
98
+ }
99
+
100
+ /**
101
+ * Launch Chrome browser
102
+ * @param {TestDriver} client - TestDriver client
103
+ * @param {string} url - URL to open
104
+ * @param {object} options - Browser options
105
+ * @param {boolean} options.guest - Launch in guest mode (default: true)
106
+ * @param {boolean} options.maximized - Start maximized (default: true)
107
+ */
108
+ export async function launchChrome(client, url = "about:blank", options = {}) {
109
+ const { guest = true, maximized = true } = options;
110
+ const shell = client.os === "windows" ? "pwsh" : "sh";
111
+
112
+ const guestFlag = guest ? "--guest" : "";
113
+ const maxFlag = maximized ? "--start-maximized" : "";
114
+
115
+ if (client.os === "windows") {
116
+ const args = [maxFlag, guestFlag, `"${url}"`].filter(Boolean).join('", "');
117
+ await client.exec(
118
+ "pwsh",
119
+ `Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "${args}"`,
120
+ 30000,
121
+ );
122
+ } else {
123
+ const flags = [maxFlag, "--disable-fre", "--no-default-browser-check", "--no-first-run", guestFlag].filter(Boolean).join(" ");
124
+ await client.exec(
125
+ shell,
126
+ `google-chrome ${flags} "${url}" >/dev/null 2>&1 &`,
127
+ 30000,
128
+ );
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Launch Chrome for Testing browser
134
+ * @param {TestDriver} client - TestDriver client
135
+ * @param {string} url - URL to open
136
+ * @param {object} options - Browser options
137
+ */
138
+ export async function launchChromeForTesting(client, url = "about:blank", options = {}) {
139
+ const { guest = true, maximized = true } = options;
140
+ const shell = client.os === "windows" ? "pwsh" : "sh";
141
+
142
+ const guestFlag = guest ? "--guest" : "";
143
+ const maxFlag = maximized ? "--start-maximized" : "";
144
+
145
+ if (client.os === "windows") {
146
+ // Fallback to regular Chrome on Windows
147
+ await launchChrome(client, url, options);
148
+ } else {
149
+ const flags = [maxFlag, "--disable-fre", "--no-default-browser-check", "--no-first-run", guestFlag].filter(Boolean).join(" ");
150
+ await client.exec(
151
+ shell,
152
+ `chrome-for-testing ${flags} "${url}" >/dev/null 2>&1 &`,
153
+ 30000,
154
+ );
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Launch Chrome with a Chrome extension loaded
160
+ * @param {TestDriver} client - TestDriver client
161
+ * @param {string} extensionId - Chrome Web Store extension ID
162
+ * @param {string} url - URL to open
163
+ * @example
164
+ * // Launch with uBlock Origin extension
165
+ * await launchChromeExtension(client, "cjpalhdlnbpafiamejdnhcphjbkeiagm");
166
+ */
167
+ export async function launchChromeExtension(client, extensionId, url = "about:blank") {
168
+ const shell = client.os === "windows" ? "pwsh" : "sh";
169
+
170
+ if (client.os === "windows") {
171
+ await client.exec(
172
+ "pwsh",
173
+ `Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=${extensionId}", "${url}"`,
174
+ 30000,
175
+ );
176
+ } else {
177
+ await client.exec(
178
+ shell,
179
+ `chrome-for-testing --start-maximized --disable-fre --no-default-browser-check --no-first-run --load-extension=${extensionId} "${url}" >/dev/null 2>&1 &`,
180
+ 30000,
181
+ );
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Wait for page to load by polling for text
187
+ * @param {TestDriver} client - TestDriver client
188
+ * @param {string} text - Text to wait for
189
+ * @param {number} maxAttempts - Maximum number of attempts (default: 60)
190
+ * @param {number} pollInterval - Interval between polls in ms (default: 5000)
191
+ * @returns {Promise<boolean>} True if text was found
192
+ */
193
+ export async function waitForPage(client, text, maxAttempts = 60, pollInterval = 5000) {
194
+ console.log("Waiting for page to load, looking for text:", text);
195
+ for (let i = 0; i < maxAttempts; i++) {
196
+ const element = await client.find(text);
197
+ if (element.found()) {
198
+ return true;
199
+ }
200
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
201
+ }
202
+ return false;
203
+ }
204
+
205
+ /**
206
+ * Run standard prerun lifecycle hooks
207
+ * Authenticates, starts dashcam, launches Chrome
208
+ * @param {TestDriver} client - TestDriver client
209
+ * @param {object} options - Options
210
+ * @param {string} options.url - URL to open (default: sandbox)
211
+ * @param {string} options.waitForText - Text to wait for after page load
212
+ */
213
+ export async function runPrerun(client, options = {}) {
214
+ const {
215
+ url = "http://testdriver-sandbox.vercel.app/",
216
+ waitForText = "TestDriver.ai Sandbox"
217
+ } = options;
218
+
219
+ await authDashcam(client);
220
+ await addDashcamLog(client);
221
+ await startDashcam(client);
222
+ await launchChrome(client, url);
223
+ if (waitForText) {
224
+ await waitForPage(client, waitForText);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Run prerun with Chrome for Testing
230
+ * @param {TestDriver} client - TestDriver client
231
+ * @param {object} options - Options
232
+ */
233
+ export async function runPrerunChromeForTesting(client, options = {}) {
234
+ const {
235
+ url = "http://testdriver-sandbox.vercel.app/",
236
+ waitForText = "TestDriver.ai Sandbox"
237
+ } = options;
238
+
239
+ await authDashcam(client);
240
+ await addDashcamLog(client);
241
+ await startDashcam(client);
242
+ await launchChromeForTesting(client, url);
243
+ if (waitForText) {
244
+ await waitForPage(client, waitForText);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Run prerun with Chrome extension
250
+ * @param {TestDriver} client - TestDriver client
251
+ * @param {string} extensionId - Chrome extension ID
252
+ * @param {object} options - Options
253
+ */
254
+ export async function runPrerunChromeExtension(client, extensionId, options = {}) {
255
+ const {
256
+ url = "http://testdriver-sandbox.vercel.app/",
257
+ waitForText = "TestDriver.ai Sandbox"
258
+ } = options;
259
+
260
+ await authDashcam(client);
261
+ await addDashcamLog(client);
262
+ await startDashcam(client);
263
+ await launchChromeExtension(client, extensionId, url);
264
+ if (waitForText) {
265
+ await waitForPage(client, waitForText);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Run standard postrun lifecycle hooks
271
+ * Stops dashcam and returns URL
272
+ * @param {TestDriver} client - TestDriver client
273
+ * @returns {Promise<string|null>} Dashcam URL if available
274
+ */
275
+ export async function runPostrun(client) {
276
+ return await stopDashcam(client);
277
+ }