testdriverai 7.2.50 → 7.2.52

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.
package/agents.md CHANGED
@@ -88,11 +88,11 @@ The SDK has TypeScript types in `sdk.d.ts`. Key methods:
88
88
  |--------|---------|
89
89
  | `find(description)` | Find element by natural language |
90
90
  | `findAll(description)` | Find all matching elements |
91
- | `assert(assertion)` | AI-powered assertion |
92
- | `type(text)` | Type text |
91
+ | `assert(assertion)` | AI-powered assertion || `screenshot()` | Capture and save screenshot locally || `type(text)` | Type text |
93
92
  | `pressKeys([keys])` | Press keyboard keys |
94
93
  | `scroll(direction)` | Scroll the page |
95
94
  | `exec(language, code)` | Execute code in sandbox |
95
+ | `screenshot(scale, silent, mouse)` | Capture screenshot as base64 PNG |
96
96
  | `ai(task)` | AI exploratory loop (see note below) |
97
97
 
98
98
  ### About `ai()` - Use for Exploration, Not Final Tests
@@ -146,6 +146,39 @@ await element.mouseUp(); // release mouse
146
146
  element.found(); // check if found (boolean)
147
147
  ```
148
148
 
149
+ ### Screenshots for Debugging
150
+
151
+ **Use `screenshot()` liberally during development** to see exactly what the sandbox screen looks like. Screenshots are saved locally and organized by test file.
152
+
153
+ ```javascript
154
+ // Capture a screenshot - saved to .testdriverai/screenshots/<test-file>/
155
+ const screenshotPath = await testdriver.screenshot();
156
+ console.log('Screenshot saved to:', screenshotPath);
157
+
158
+ // Include mouse cursor in screenshot
159
+ await testdriver.screenshot(1, false, true);
160
+ ```
161
+
162
+ **When to use screenshots:**
163
+ - After `provision.chrome()` to verify the page loaded correctly
164
+ - Before/after clicking elements to see state changes
165
+ - When a `find()` fails to see what the AI is actually seeing
166
+ - Before `assert()` calls to debug assertion failures
167
+ - When tests behave unexpectedly
168
+
169
+ **Screenshot file organization:**
170
+ ```
171
+ .testdriverai/
172
+ screenshots/
173
+ login.test/ # Folder per test file
174
+ screenshot-1737633600000.png
175
+ screenshot-1737633605000.png
176
+ checkout.test/
177
+ screenshot-1737633700000.png
178
+ ```
179
+
180
+ > **Note:** The screenshot folder for each test file is automatically cleared when the test starts, so you only see screenshots from the most recent run.
181
+
149
182
  ## Best Workflow: Two-File Pattern
150
183
 
151
184
  **The most efficient workflow for building tests uses two files.** This prevents having to restart from scratch when experimenting with new steps.
@@ -308,6 +341,9 @@ it("should incrementally build test", async (context) => {
308
341
  const testdriver = TestDriver(context);
309
342
  await testdriver.provision.chrome({ url: 'https://example.com' });
310
343
 
344
+ // Take a screenshot to see the initial state
345
+ await testdriver.screenshot();
346
+
311
347
  // Step 1: Find and inspect
312
348
  const element = await testdriver.find("Some button");
313
349
  console.log("Element found:", element.found());
@@ -317,6 +353,9 @@ it("should incrementally build test", async (context) => {
317
353
  // Step 2: Interact
318
354
  await element.click();
319
355
 
356
+ // Screenshot after interaction to see the result
357
+ await testdriver.screenshot();
358
+
320
359
  // Step 3: Assert and log
321
360
  const result = await testdriver.assert("Something happened");
322
361
  console.log("Assertion result:", result);
@@ -382,6 +421,20 @@ const output = await testdriver.exec("sh", "ls -la", 5000);
382
421
  const date = await testdriver.exec("pwsh", "Get-Date", 5000);
383
422
  ```
384
423
 
424
+ ### Capturing Screenshots
425
+ ```javascript
426
+ // Capture a screenshot and save to file
427
+ const screenshot = await testdriver.screenshot();
428
+ const filepath = 'screenshot.png';
429
+ fs.writeFileSync(filepath, Buffer.from(screenshot, 'base64'));
430
+ console.log('Screenshot saved to:', filepath);
431
+
432
+ // Capture with mouse cursor visible
433
+ const screenshotWithMouse = await testdriver.screenshot(1, false, true);
434
+ fs.writeFileSync('screenshot-with-mouse.png', Buffer.from(screenshotWithMouse, 'base64'));
435
+ console.log('Screenshot with mouse saved to: screenshot-with-mouse.png');
436
+ ```
437
+
385
438
  ## Tips for Agents
386
439
 
387
440
  1. **Always check `sdk.d.ts`** for method signatures and types
@@ -390,3 +443,13 @@ const date = await testdriver.exec("pwsh", "Get-Date", 5000);
390
443
  4. **Log element properties** to understand what the AI sees
391
444
  5. **Use `assert()` with specific, descriptive natural language**
392
445
  6. **Start simple** - get one step working before adding more
446
+ 7. **Take screenshots liberally** - use `await testdriver.screenshot()` after key steps to debug what the sandbox actually shows. Check `.testdriverai/screenshots/<test-file>/` to review them.
447
+ 8. **Always `await` async methods** - TestDriver will warn if you forget, but for TypeScript projects, add `@typescript-eslint/no-floating-promises` to your ESLint config to catch missing `await` at compile time:
448
+ ```json
449
+ // eslint.config.js (for TypeScript projects)
450
+ {
451
+ "rules": {
452
+ "@typescript-eslint/no-floating-promises": "error"
453
+ }
454
+ }
455
+ ```
package/docs/docs.json CHANGED
@@ -82,6 +82,7 @@
82
82
  "/v7/mouse-up",
83
83
  "/v7/press-keys",
84
84
  "/v7/right-click",
85
+ "/v7/screenshot",
85
86
  "/v7/type",
86
87
  "/v7/scroll"
87
88
  ]
@@ -120,7 +120,6 @@ describe("Chrome Extension Test", () => {
120
120
  const testdriver = TestDriver(context);
121
121
 
122
122
  // Clone extension from GitHub
123
- await testdriver.ready();
124
123
  await testdriver.exec(
125
124
  'sh',
126
125
  'git clone https://github.com/user/my-extension.git /tmp/my-extension',
@@ -284,7 +283,6 @@ describe("VS Code Test", () => {
284
283
  const testdriver = TestDriver(context);
285
284
 
286
285
  // Create a test project
287
- await testdriver.ready();
288
286
  await testdriver.exec('sh', 'mkdir -p /tmp/test-project && echo "print(1)" > /tmp/test-project/test.py', 10000);
289
287
 
290
288
  // Launch VS Code
@@ -0,0 +1,151 @@
1
+ ---
2
+ title: "screenshot()"
3
+ sidebarTitle: "screenshot"
4
+ description: "Capture and save screenshots during test execution"
5
+ icon: "camera"
6
+ ---
7
+
8
+ ## Overview
9
+
10
+ Capture a screenshot of the current screen and automatically save it to a local file. Screenshots are organized by test file for easy debugging and review.
11
+
12
+ ## Syntax
13
+
14
+ ```javascript
15
+ const filePath = await testdriver.screenshot(scale, silent, mouse)
16
+ ```
17
+
18
+ ## Parameters
19
+
20
+ <ParamField path="scale" type="number" default="1">
21
+ Scale factor for the screenshot (1 = original size, 0.5 = half size, 2 = double size)
22
+ </ParamField>
23
+
24
+ <ParamField path="silent" type="boolean" default="false">
25
+ Whether to suppress the log message showing where the screenshot was saved
26
+ </ParamField>
27
+
28
+ <ParamField path="mouse" type="boolean" default="false">
29
+ Whether to include the mouse cursor in the screenshot
30
+ </ParamField>
31
+
32
+ ## Returns
33
+
34
+ `Promise<string>` - The absolute file path where the screenshot was saved
35
+
36
+ ## File Organization
37
+
38
+ Screenshots are automatically saved to `.testdriverai/screenshots/<test-file-name>/` in your project root:
39
+
40
+ ```
41
+ .testdriverai/
42
+ screenshots/
43
+ login.test/
44
+ screenshot-1737633600000.png
45
+ screenshot-1737633605000.png
46
+ checkout.test/
47
+ screenshot-1737633700000.png
48
+ ```
49
+
50
+ <Note>
51
+ The screenshot folder for each test file is automatically cleared when the test starts. This ensures you only see screenshots from the most recent test run.
52
+ </Note>
53
+
54
+ ## Examples
55
+
56
+ ### Basic Screenshot
57
+
58
+ ```javascript
59
+ // Capture a screenshot with default settings
60
+ const screenshotPath = await testdriver.screenshot();
61
+ console.log('Screenshot saved to:', screenshotPath);
62
+ ```
63
+
64
+ ### Screenshot with Mouse Cursor
65
+
66
+ ```javascript
67
+ // Capture a screenshot showing the mouse cursor position
68
+ const screenshotPath = await testdriver.screenshot(1, false, true);
69
+ ```
70
+
71
+ ### Silent Screenshot
72
+
73
+ ```javascript
74
+ // Capture without logging the save location
75
+ await testdriver.screenshot(1, true);
76
+ ```
77
+
78
+ ### Debugging with Screenshots
79
+
80
+ ```javascript
81
+ import { describe, expect, it } from "vitest";
82
+ import { TestDriver } from "testdriverai/lib/vitest/hooks.mjs";
83
+
84
+ describe("Login Flow", () => {
85
+ it("should log in successfully", async (context) => {
86
+ const testdriver = TestDriver(context);
87
+
88
+ await testdriver.provision.chrome({
89
+ url: 'https://myapp.com/login',
90
+ });
91
+
92
+ // Capture initial state
93
+ await testdriver.screenshot();
94
+
95
+ // Fill in login form
96
+ const emailInput = await testdriver.find("email input");
97
+ await emailInput.click();
98
+ await testdriver.type("user@example.com");
99
+
100
+ // Capture state after typing
101
+ await testdriver.screenshot();
102
+
103
+ const passwordInput = await testdriver.find("password input");
104
+ await passwordInput.click();
105
+ await testdriver.type("password123");
106
+
107
+ // Capture before clicking login
108
+ await testdriver.screenshot();
109
+
110
+ const loginButton = await testdriver.find("Login button");
111
+ await loginButton.click();
112
+
113
+ // Capture after login attempt
114
+ await testdriver.screenshot();
115
+
116
+ const result = await testdriver.assert("dashboard is visible");
117
+ expect(result).toBeTruthy();
118
+ });
119
+ });
120
+ ```
121
+
122
+ ## Best Practices
123
+
124
+ <AccordionGroup>
125
+ <Accordion title="Use screenshots for debugging flaky tests">
126
+ When a test fails intermittently, add screenshots at key steps to capture the actual screen state. This helps identify timing issues or unexpected UI states.
127
+ </Accordion>
128
+
129
+ <Accordion title="Capture before assertions">
130
+ Take a screenshot before making assertions. If the assertion fails, you'll have a visual record of what the screen looked like.
131
+
132
+ ```javascript
133
+ await testdriver.screenshot();
134
+ const result = await testdriver.assert("checkout button is visible");
135
+ ```
136
+ </Accordion>
137
+
138
+ <Accordion title="Add to .gitignore">
139
+ Add `.testdriverai/screenshots/` to your `.gitignore` to avoid committing screenshots to version control:
140
+
141
+ ```
142
+ # .gitignore
143
+ .testdriverai/screenshots/
144
+ ```
145
+ </Accordion>
146
+ </AccordionGroup>
147
+
148
+ ## Related
149
+
150
+ - [assert()](/v7/assert) - Make AI-powered assertions
151
+ - [find()](/v7/find) - Locate elements on screen
package/eslint.config.js CHANGED
@@ -45,6 +45,12 @@ module.exports = [
45
45
  ...globals.node,
46
46
  },
47
47
  },
48
+ rules: {
49
+ // Warn about floating promises (unawaited async calls)
50
+ // This catches missing `await` on async methods like click(), assert(), etc.
51
+ // Note: For TypeScript projects, use @typescript-eslint/no-floating-promises instead
52
+ "require-await": "warn",
53
+ },
48
54
  },
49
55
  {
50
56
  // this needs to be it's own object for some reason
@@ -17,6 +17,9 @@ describe("Assert Test", () => {
17
17
  url: 'http://testdriver-sandbox.vercel.app/login',
18
18
  });
19
19
 
20
+ // Take a screenshot
21
+ await testdriver.screenshot();
22
+
20
23
  // Assert the TestDriver.ai Sandbox login page is displayed
21
24
  const result = await testdriver.assert(
22
25
  "the TestDriver.ai Sandbox login page is displayed",
@@ -17,9 +17,6 @@ describe("Chrome Extension Test", () => {
17
17
 
18
18
  const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, cacheKey: new Date().getTime().toString() });
19
19
 
20
- // Wait for connection to be ready before running exec
21
- await testdriver.ready();
22
-
23
20
  // Determine OS-specific paths and commands
24
21
  const shell = testdriver.os === 'windows' ? 'pwsh' : 'sh';
25
22
  const extensionsDir = testdriver.os === 'windows'
@@ -0,0 +1,23 @@
1
+ /**
2
+ * TestDriver SDK - Assert Test (Vitest)
3
+ * Converted from: testdriver/acceptance/assert.yaml
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { TestDriver } from "../lib/vitest/hooks.mjs";
8
+
9
+ describe("Assert Test", () => {
10
+ it("should assert the testdriver login page shows", async (context) => {
11
+ const testdriver = TestDriver(context, {
12
+ ip: context.ip || process.env.TD_IP,
13
+ });
14
+
15
+ // Assert the TestDriver.ai Sandbox login page is displayed
16
+ const result = await testdriver.assert(
17
+ "A desktop is visible",
18
+ );
19
+
20
+ expect(result).toBeTruthy();
21
+ });
22
+ });
23
+
@@ -9,7 +9,6 @@
9
9
  * test('my test', async (context) => {
10
10
  * const testdriver = TestDriver(context, { headless: true });
11
11
  *
12
- * await testdriver.ready();
13
12
  * await testdriver.provision.chrome({ url: 'https://example.com' });
14
13
  * await testdriver.find('button').click();
15
14
  * });
@@ -254,46 +253,50 @@ export function TestDriver(context, options = {}) {
254
253
  // Pass test file name to SDK for debugger display
255
254
  testdriver.testFile = testFile;
256
255
 
257
- // Auto-connect if enabled (default: true)
258
- const autoConnect = config.autoConnect !== undefined ? config.autoConnect : true;
259
256
  const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === 'true';
260
257
 
261
- if (autoConnect) {
262
- testdriver.__connectionPromise = (async () => {
263
- if (debugConsoleSpy) {
264
- console.log('[DEBUG] Before auth - sandbox.instanceSocketConnected:', testdriver.sandbox?.instanceSocketConnected);
265
- }
266
-
267
- await testdriver.auth();
268
- await testdriver.connect();
269
-
270
- if (debugConsoleSpy) {
271
- console.log('[DEBUG] After connect - sandbox.instanceSocketConnected:', testdriver.sandbox?.instanceSocketConnected);
272
- console.log('[DEBUG] After connect - sandbox.send:', typeof testdriver.sandbox?.send);
273
- }
274
-
275
- // Set up console spy using vi.spyOn (test-isolated)
276
- setupConsoleSpy(testdriver, context.task.id);
277
-
278
- // Create the log file on the remote machine
279
- const shell = testdriver.os === "windows" ? "pwsh" : "sh";
280
- const logPath = testdriver.os === "windows"
281
- ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
282
- : "/tmp/testdriver.log";
283
-
284
- const createLogCmd = testdriver.os === "windows"
285
- ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
286
- : `touch ${logPath}`;
287
-
288
- await testdriver.exec(shell, createLogCmd, 10000, true);
289
-
290
- // Add automatic log tracking when dashcam starts
291
- // Store original start method
292
-
258
+ testdriver.__connectionPromise = (async () => {
259
+ if (debugConsoleSpy) {
260
+ console.log('[DEBUG] Before auth - sandbox.instanceSocketConnected:', testdriver.sandbox?.instanceSocketConnected);
261
+ }
262
+
263
+ await testdriver.auth();
264
+ await testdriver.connect();
265
+
266
+ // Clear the connection promise now that we're connected
267
+ // This prevents deadlock when exec() is called below (exec() lazy-awaits __connectionPromise)
268
+ testdriver.__connectionPromise = null;
269
+
270
+ if (debugConsoleSpy) {
271
+ console.log('[DEBUG] After connect - sandbox.instanceSocketConnected:', testdriver.sandbox?.instanceSocketConnected);
272
+ console.log('[DEBUG] After connect - sandbox.send:', typeof testdriver.sandbox?.send);
273
+ }
274
+
275
+ // Set up console spy using vi.spyOn (test-isolated)
276
+ setupConsoleSpy(testdriver, context.task.id);
277
+
278
+ // Create the log file on the remote machine
279
+ const shell = testdriver.os === "windows" ? "pwsh" : "sh";
280
+ const logPath = testdriver.os === "windows"
281
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
282
+ : "/tmp/testdriver.log";
283
+
284
+ const createLogCmd = testdriver.os === "windows"
285
+ ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
286
+ : `touch ${logPath}`;
287
+
288
+ await testdriver.exec(shell, createLogCmd, 10000, true);
289
+
290
+ // Only set up dashcam if enabled (default: true)
291
+ if (testdriver.dashcamEnabled) {
292
+ // Add testdriver log to dashcam tracking
293
293
  await testdriver.dashcam.addFileLog(logPath, "TestDriver Log");
294
+
295
+ // Start dashcam recording (always, regardless of provision method)
296
+ await testdriver.dashcam.start();
297
+ }
294
298
 
295
299
  })();
296
- }
297
300
 
298
301
  // Register cleanup handler with dashcam.stop()
299
302
  // We always register a new cleanup handler because on retry we need to clean up the new instance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.2.50",
3
+ "version": "7.2.52",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",
package/sdk.d.ts CHANGED
@@ -244,6 +244,8 @@ export interface TestDriverOptions {
244
244
  cacheKey?: string;
245
245
  /** Reconnect to the last used sandbox instead of creating a new one. When true, provision methods (chrome, vscode, installer, etc.) will be skipped since the application is already running. Throws error if no previous sandbox exists. */
246
246
  reconnect?: boolean;
247
+ /** Enable/disable Dashcam video recording (default: true) */
248
+ dashcam?: boolean;
247
249
  /** Redraw configuration for screen change detection */
248
250
  redraw?: boolean | {
249
251
  /** Enable redraw detection (default: true) */
@@ -810,6 +812,11 @@ export default class TestDriverSDK {
810
812
  */
811
813
  readonly dashcam: DashcamAPI;
812
814
 
815
+ /**
816
+ * Whether Dashcam recording is enabled (default: true)
817
+ */
818
+ readonly dashcamEnabled: boolean;
819
+
813
820
  /**
814
821
  * Wait for the sandbox to be ready
815
822
  * Called automatically by provision methods
@@ -1129,20 +1136,20 @@ export default class TestDriverSDK {
1129
1136
  // Utility Methods
1130
1137
 
1131
1138
  /**
1132
- * Capture a screenshot of the current screen
1139
+ * Capture a screenshot of the current screen and save it to .testdriverai/screenshots
1133
1140
  * @param scale - Scale factor for the screenshot (default: 1 = original size)
1134
1141
  * @param silent - Whether to suppress logging (default: false)
1135
1142
  * @param mouse - Whether to include mouse cursor (default: false)
1136
- * @returns Base64 encoded PNG screenshot
1143
+ * @returns The file path where the screenshot was saved
1137
1144
  *
1138
1145
  * @example
1139
- * // Capture a screenshot
1140
- * const screenshot = await client.screenshot();
1141
- * fs.writeFileSync('screenshot.png', Buffer.from(screenshot, 'base64'));
1146
+ * // Capture a screenshot (saves to .testdriverai/screenshots)
1147
+ * const screenshotPath = await client.screenshot();
1148
+ * console.log('Screenshot saved to:', screenshotPath);
1142
1149
  *
1143
1150
  * @example
1144
1151
  * // Capture with mouse cursor visible
1145
- * const screenshot = await client.screenshot(1, false, true);
1152
+ * const screenshotPath = await client.screenshot(1, false, true);
1146
1153
  */
1147
1154
  screenshot(
1148
1155
  scale?: number,
package/sdk.js CHANGED
@@ -1258,6 +1258,9 @@ class TestDriverSDK {
1258
1258
  // Store reconnect preference from options
1259
1259
  this.reconnect = options.reconnect !== undefined ? options.reconnect : false;
1260
1260
 
1261
+ // Store dashcam preference (default: true)
1262
+ this.dashcamEnabled = options.dashcam !== false;
1263
+
1261
1264
  // Cache threshold configuration
1262
1265
  // threshold = pixel difference allowed (0.05 = 5% difference, 95% similarity)
1263
1266
  // By default, cache is DISABLED (threshold = -1) to avoid unnecessary AI costs
@@ -1333,6 +1336,13 @@ class TestDriverSDK {
1333
1336
 
1334
1337
  // Set up dashcam API lazily
1335
1338
  this._dashcam = null;
1339
+
1340
+ // Last-promise tracking for unawaited promise detection
1341
+ this._lastPromiseSettled = true;
1342
+ this._lastCommandName = null;
1343
+
1344
+ // Set up command methods that lazy-await connection
1345
+ this._setupCommandMethods();
1336
1346
  }
1337
1347
 
1338
1348
  /**
@@ -1344,19 +1354,36 @@ class TestDriverSDK {
1344
1354
  await this.__connectionPromise;
1345
1355
  }
1346
1356
  if (!this.connected) {
1347
- throw new Error('Not connected to sandbox. Call connect() first or use autoConnect option.');
1357
+ throw new Error('Not connected to sandbox. Call connect() first.');
1348
1358
  }
1349
1359
  }
1350
1360
 
1351
1361
  /**
1352
1362
  * Get or create the Dashcam instance
1353
- * @returns {Dashcam} Dashcam instance
1363
+ * @returns {Dashcam} Dashcam instance (or no-op stub if dashcam is disabled)
1354
1364
  */
1355
1365
  get dashcam() {
1356
1366
  if (!this._dashcam) {
1357
- const { Dashcam } = require("./lib/core/index.js");
1358
- // Don't pass apiKey - let Dashcam use its default key
1359
- this._dashcam = new Dashcam(this);
1367
+ // If dashcam is disabled, return a no-op stub
1368
+ if (!this.dashcamEnabled) {
1369
+ this._dashcam = {
1370
+ start: async () => {},
1371
+ stop: async () => null,
1372
+ auth: async () => {},
1373
+ addFileLog: async () => {},
1374
+ addWebLog: async () => {},
1375
+ addApplicationLog: async () => {},
1376
+ addLog: async () => {},
1377
+ isRecording: async () => false,
1378
+ getElapsedTime: () => null,
1379
+ recording: false,
1380
+ url: null,
1381
+ };
1382
+ } else {
1383
+ const { Dashcam } = require("./lib/core/index.js");
1384
+ // Don't pass apiKey - let Dashcam use its default key
1385
+ this._dashcam = new Dashcam(this);
1386
+ }
1360
1387
  }
1361
1388
  return this._dashcam;
1362
1389
  }
@@ -1406,40 +1433,16 @@ class TestDriverSDK {
1406
1433
  * @returns {Promise<void>}
1407
1434
  */
1408
1435
  chrome: async (options = {}) => {
1409
- // Automatically wait for connection to be ready
1410
- await this.ready();
1411
-
1412
1436
  const {
1413
1437
  url = 'http://testdriver-sandbox.vercel.app/',
1414
1438
  maximized = true,
1415
1439
  guest = false,
1416
1440
  } = options;
1417
1441
 
1418
- // If dashcam is available, add web logs for all websites using "**" pattern
1442
+ // If dashcam is available, add web logs for all websites
1443
+ // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
1419
1444
  if (this._dashcam) {
1420
-
1421
- // Create the log file on the remote machine
1422
- const shell = this.os === "windows" ? "pwsh" : "sh";
1423
- const logPath = this.os === "windows"
1424
- ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1425
- : "/tmp/testdriver.log";
1426
-
1427
- const createLogCmd = this.os === "windows"
1428
- ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1429
- : `touch ${logPath}`;
1430
-
1431
- await this.exec(shell, createLogCmd, 60000, true);
1432
-
1433
- // Track all websites by default with "**" pattern
1434
- await this._dashcam.addWebLog('**', 'Web Logs');
1435
-
1436
- await this._dashcam.addFileLog(logPath, "TestDriver Log");
1437
-
1438
- }
1439
-
1440
- // Automatically start dashcam if not already recording
1441
- if (!this._dashcam || !this._dashcam.recording) {
1442
- await this.dashcam.start();
1445
+ await this._dashcam.addWebLog('**', 'Web Logs');
1443
1446
  }
1444
1447
 
1445
1448
  // Set up Chrome profile with preferences
@@ -1571,9 +1574,6 @@ class TestDriverSDK {
1571
1574
  * });
1572
1575
  */
1573
1576
  chromeExtension: async (options = {}) => {
1574
- // Automatically wait for connection to be ready
1575
- await this.ready();
1576
-
1577
1577
  const {
1578
1578
  extensionPath: providedExtensionPath,
1579
1579
  extensionId,
@@ -1694,27 +1694,10 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1694
1694
  console.log(`[provision.chromeExtension] Extension ${extensionId} extracted to ${extensionPath}`);
1695
1695
  }
1696
1696
 
1697
- // If dashcam is available, set up file and web logging
1697
+ // If dashcam is available, add web logs for all websites
1698
+ // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
1698
1699
  if (this._dashcam) {
1699
- // Create the log file on the remote machine
1700
- const logPath = this.os === "windows"
1701
- ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1702
- : "/tmp/testdriver.log";
1703
-
1704
- const createLogCmd = this.os === "windows"
1705
- ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1706
- : `touch ${logPath}`;
1707
-
1708
- await this.exec(shell, createLogCmd, 60000, true);
1709
-
1710
- // Track all websites by default with "**" pattern
1711
1700
  await this._dashcam.addWebLog('**', 'Web Logs');
1712
- await this._dashcam.addFileLog(logPath, "TestDriver Log");
1713
- }
1714
-
1715
- // Automatically start dashcam if not already recording
1716
- if (!this._dashcam || !this._dashcam.recording) {
1717
- await this.dashcam.start();
1718
1701
  }
1719
1702
 
1720
1703
  // Set up Chrome profile with preferences
@@ -1827,9 +1810,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1827
1810
  * @returns {Promise<void>}
1828
1811
  */
1829
1812
  vscode: async (options = {}) => {
1830
- // Automatically wait for connection to be ready
1831
- await this.ready();
1832
-
1833
1813
  const {
1834
1814
  workspace = null,
1835
1815
  extensions = [],
@@ -1837,27 +1817,10 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1837
1817
 
1838
1818
  const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1839
1819
 
1840
- // If dashcam is available, set up file and web logging
1820
+ // If dashcam is available, add web logs for all websites
1821
+ // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
1841
1822
  if (this._dashcam) {
1842
- // Create the log file on the remote machine
1843
- const logPath = this.os === "windows"
1844
- ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1845
- : "/tmp/testdriver.log";
1846
-
1847
- const createLogCmd = this.os === "windows"
1848
- ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1849
- : `touch ${logPath}`;
1850
-
1851
- await this.exec(shell, createLogCmd, 60000, true);
1852
-
1853
- // Track all websites by default with "**" pattern
1854
1823
  await this._dashcam.addWebLog('**', 'Web Logs');
1855
- await this._dashcam.addFileLog(logPath, "TestDriver Log");
1856
- }
1857
-
1858
- // Automatically start dashcam if not already recording
1859
- if (!this._dashcam || !this._dashcam.recording) {
1860
- await this.dashcam.start();
1861
1824
  }
1862
1825
 
1863
1826
  // Install extensions if provided
@@ -1920,9 +1883,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1920
1883
  * await testdriver.exec('sh', `chmod +x "${filePath}" && "${filePath}" &`, 10000);
1921
1884
  */
1922
1885
  installer: async (options = {}) => {
1923
- // Automatically wait for connection to be ready
1924
- await this.ready();
1925
-
1926
1886
  const {
1927
1887
  url,
1928
1888
  filename,
@@ -1936,26 +1896,10 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1936
1896
 
1937
1897
  const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1938
1898
 
1939
- // If dashcam is available, set up file and web logging
1899
+ // If dashcam is available, add web logs for all websites
1900
+ // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
1940
1901
  if (this._dashcam) {
1941
- const logPath = this.os === "windows"
1942
- ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1943
- : "/tmp/testdriver.log";
1944
-
1945
- const createLogCmd = this.os === "windows"
1946
- ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1947
- : `touch ${logPath}`;
1948
-
1949
- await this.exec(shell, createLogCmd, 60000, true);
1950
-
1951
- // Track all websites by default with "**" pattern
1952
1902
  await this._dashcam.addWebLog('**', 'Web Logs');
1953
- await this._dashcam.addFileLog(logPath, "TestDriver Log");
1954
- }
1955
-
1956
- // Automatically start dashcam if not already recording
1957
- if (!this._dashcam || !this._dashcam.recording) {
1958
- await this.dashcam.start();
1959
1903
  }
1960
1904
 
1961
1905
  // Determine download directory
@@ -2085,9 +2029,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2085
2029
  * @returns {Promise<void>}
2086
2030
  */
2087
2031
  electron: async (options = {}) => {
2088
- // Automatically wait for connection to be ready
2089
- await this.ready();
2090
-
2091
2032
  const { appPath, args = [] } = options;
2092
2033
 
2093
2034
  if (!appPath) {
@@ -2096,26 +2037,10 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2096
2037
 
2097
2038
  const shell = this.os === 'windows' ? 'pwsh' : 'sh';
2098
2039
 
2099
- // If dashcam is available, set up file and web logging
2040
+ // If dashcam is available, add web logs for all websites
2041
+ // Note: File log and dashcam.start() are handled by the connection promise in hooks.mjs
2100
2042
  if (this._dashcam) {
2101
- const logPath = this.os === "windows"
2102
- ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
2103
- : "/tmp/testdriver.log";
2104
-
2105
- const createLogCmd = this.os === "windows"
2106
- ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
2107
- : `touch ${logPath}`;
2108
-
2109
- await this.exec(shell, createLogCmd, 60000, true);
2110
-
2111
- // Track all websites by default with "**" pattern
2112
2043
  await this._dashcam.addWebLog('**', 'Web Logs');
2113
- await this._dashcam.addFileLog(logPath, "TestDriver Log");
2114
- }
2115
-
2116
- // Automatically start dashcam if not already recording
2117
- if (!this._dashcam || !this._dashcam.recording) {
2118
- await this.dashcam.start();
2119
2044
  }
2120
2045
 
2121
2046
  const argsString = args.join(' ');
@@ -2190,6 +2115,15 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2190
2115
  );
2191
2116
  }
2192
2117
 
2118
+ // Clean up screenshots folder for this test file before running
2119
+ if (this.testFile) {
2120
+ const testFileName = path.basename(this.testFile, path.extname(this.testFile));
2121
+ const screenshotsDir = path.join(process.cwd(), ".testdriverai", "screenshots", testFileName);
2122
+ if (fs.existsSync(screenshotsDir)) {
2123
+ fs.rmSync(screenshotsDir, { recursive: true, force: true });
2124
+ }
2125
+ }
2126
+
2193
2127
  // Authenticate first if not already authenticated
2194
2128
  if (!this.authenticated) {
2195
2129
  await this.auth();
@@ -2317,8 +2251,8 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2317
2251
  this.agent.commands = commandsResult.commands;
2318
2252
  this.agent.redraw = commandsResult.redraw;
2319
2253
 
2320
- // Dynamically create command methods based on available commands
2321
- this._setupCommandMethods();
2254
+ // Command methods are already set up in constructor with lazy-await
2255
+ // They will use this.commands which is now populated
2322
2256
 
2323
2257
  this.connected = true;
2324
2258
  this.analytics.track("sdk.connect", {
@@ -2408,9 +2342,33 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2408
2342
  * await element.click();
2409
2343
  */
2410
2344
  find(description, options) {
2411
- this._ensureConnected();
2412
- const element = new Element(description, this, this.system, this.commands);
2413
- const findPromise = element.find(null, options);
2345
+ // Wrap in async IIFE to support lazy-await and promise tracking
2346
+ const findPromise = (async () => {
2347
+ // Lazy-await: wait for connection if still pending
2348
+ if (this.__connectionPromise) {
2349
+ await this.__connectionPromise;
2350
+ }
2351
+
2352
+ // Warn if previous command may not have been awaited
2353
+ if (this._lastCommandName && !this._lastPromiseSettled) {
2354
+ console.warn(
2355
+ `⚠️ Warning: Previous ${this._lastCommandName}() may not have been awaited.\n` +
2356
+ ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
2357
+ ` Unawaited promises can cause race conditions and flaky tests.`
2358
+ );
2359
+ }
2360
+
2361
+ this._ensureConnected();
2362
+
2363
+ // Track this promise for unawaited detection
2364
+ this._lastCommandName = 'find';
2365
+ this._lastPromiseSettled = false;
2366
+
2367
+ const element = new Element(description, this, this.system, this.commands);
2368
+ const result = await element.find(null, options);
2369
+ this._lastPromiseSettled = true;
2370
+ return result;
2371
+ })();
2414
2372
 
2415
2373
  // Create a chainable promise that allows direct method chaining
2416
2374
  // e.g., await testdriver.find("button").click()
@@ -2440,8 +2398,26 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2440
2398
  * }
2441
2399
  */
2442
2400
  async findAll(description, options) {
2401
+ // Lazy-await: wait for connection if still pending
2402
+ if (this.__connectionPromise) {
2403
+ await this.__connectionPromise;
2404
+ }
2405
+
2406
+ // Warn if previous command may not have been awaited
2407
+ if (this._lastCommandName && !this._lastPromiseSettled) {
2408
+ console.warn(
2409
+ `⚠️ Warning: Previous ${this._lastCommandName}() may not have been awaited.\n` +
2410
+ ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
2411
+ ` Unawaited promises can cause race conditions and flaky tests.`
2412
+ );
2413
+ }
2414
+
2443
2415
  this._ensureConnected();
2444
2416
 
2417
+ // Track this promise for unawaited detection
2418
+ this._lastCommandName = 'findAll';
2419
+ this._lastPromiseSettled = false;
2420
+
2445
2421
  // Capture absolute timestamp at the very start of the command
2446
2422
  // Frontend will calculate relative time using: timestamp - replay.clientStartDate
2447
2423
  const absoluteTimestamp = Date.now();
@@ -2589,6 +2565,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2589
2565
  this.emitter.emit(events.log.debug, ` Time: ${duration}ms`);
2590
2566
  }
2591
2567
 
2568
+ this._lastPromiseSettled = true;
2592
2569
  return elements;
2593
2570
  } else {
2594
2571
  const duration = Date.now() - startTime;
@@ -2625,6 +2602,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2625
2602
  }
2626
2603
 
2627
2604
  // No elements found - return empty array
2605
+ this._lastPromiseSettled = true;
2628
2606
  return [];
2629
2607
  }
2630
2608
  } catch (error) {
@@ -2657,6 +2635,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2657
2635
  });
2658
2636
  }
2659
2637
 
2638
+ this._lastPromiseSettled = true;
2660
2639
  return [];
2661
2640
  }
2662
2641
  }
@@ -2706,251 +2685,74 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2706
2685
  * @private
2707
2686
  */
2708
2687
  _setupCommandMethods() {
2709
- // Mapping from command names to SDK method names with type definitions
2710
- // Each command supports both positional args (legacy) and object args (new)
2688
+ // Mapping from internal command names to SDK method names
2711
2689
  const commandMapping = {
2712
- "hover-text": {
2713
- name: "hoverText",
2714
- /**
2715
- * Hover over text on screen
2716
- * @deprecated Use find() and element.click() instead
2717
- * @param {Object|string} options - Options object or text (legacy positional)
2718
- * @param {string} options.text - Text to find and hover over
2719
- * @param {string|null} [options.description] - Optional description of the element
2720
- * @param {ClickAction} [options.action='click'] - Action to perform
2721
- * @param {number} [options.timeout=5000] - Timeout in milliseconds
2722
- * @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
2723
- */
2724
- doc: "Hover over text on screen (deprecated - use find() instead)",
2725
- },
2726
- "hover-image": {
2727
- name: "hoverImage",
2728
- /**
2729
- * Hover over an image on screen
2730
- * @deprecated Use find() and element.click() instead
2731
- * @param {Object|string} options - Options object or description (legacy positional)
2732
- * @param {string} options.description - Description of the image to find
2733
- * @param {ClickAction} [options.action='click'] - Action to perform
2734
- * @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
2735
- */
2736
- doc: "Hover over an image on screen (deprecated - use find() instead)",
2737
- },
2738
- "match-image": {
2739
- name: "matchImage",
2740
- /**
2741
- * Match and interact with an image template
2742
- * @param {Object|string} options - Options object or path (legacy positional)
2743
- * @param {string} options.path - Path to the image template
2744
- * @param {ClickAction} [options.action='click'] - Action to perform
2745
- * @param {boolean} [options.invert=false] - Invert the match
2746
- * @returns {Promise<boolean>}
2747
- */
2748
- doc: "Match and interact with an image template",
2749
- },
2750
- type: {
2751
- name: "type",
2752
- /**
2753
- * Type text
2754
- * @param {string|number} text - Text to type
2755
- * @param {Object} [options] - Additional options
2756
- * @param {number} [options.delay=250] - Delay between keystrokes in milliseconds
2757
- * @param {boolean} [options.secret=false] - If true, text is treated as sensitive (not logged or stored)
2758
- * @returns {Promise<void>}
2759
- */
2760
- doc: "Type text (use { secret: true } for passwords)",
2761
- },
2762
- "press-keys": {
2763
- name: "pressKeys",
2764
- /**
2765
- * Press keyboard keys
2766
- * @param {KeyboardKey[]} keys - Array of keys to press
2767
- * @param {Object} [options] - Additional options (reserved for future use)
2768
- * @returns {Promise<void>}
2769
- */
2770
- doc: "Press keyboard keys",
2771
- },
2772
- click: {
2773
- name: "click",
2774
- /**
2775
- * Click at coordinates
2776
- * @param {Object|number} options - Options object or x coordinate (legacy positional)
2777
- * @param {number} options.x - X coordinate
2778
- * @param {number} options.y - Y coordinate
2779
- * @param {ClickAction} [options.action='click'] - Type of click action
2780
- * @returns {Promise<void>}
2781
- */
2782
- doc: "Click at coordinates",
2783
- },
2784
- hover: {
2785
- name: "hover",
2786
- /**
2787
- * Hover at coordinates
2788
- * @param {Object|number} options - Options object or x coordinate (legacy positional)
2789
- * @param {number} options.x - X coordinate
2790
- * @param {number} options.y - Y coordinate
2791
- * @returns {Promise<void>}
2792
- */
2793
- doc: "Hover at coordinates",
2794
- },
2795
- scroll: {
2796
- name: "scroll",
2797
- /**
2798
- * Scroll the page
2799
- * @param {ScrollDirection} [direction='down'] - Direction to scroll
2800
- * @param {Object} [options] - Additional options
2801
- * @param {number} [options.amount=300] - Amount to scroll in pixels
2802
- * @returns {Promise<void>}
2803
- */
2804
- doc: "Scroll the page",
2805
- },
2806
- wait: {
2807
- name: "wait",
2808
- /**
2809
- * Wait for specified time
2810
- * @deprecated Consider using element polling with find() instead of arbitrary waits
2811
- * @param {number} [timeout=3000] - Time to wait in milliseconds
2812
- * @param {Object} [options] - Additional options (reserved for future use)
2813
- * @returns {Promise<void>}
2814
- */
2815
- doc: "Wait for specified time (deprecated - consider element polling instead)",
2816
- },
2817
- "wait-for-text": {
2818
- name: "waitForText",
2819
- /**
2820
- * Wait for text to appear on screen
2821
- * @deprecated Use find() in a polling loop instead
2822
- * @param {Object|string} options - Options object or text (legacy positional)
2823
- * @param {string} options.text - Text to wait for
2824
- * @param {number} [options.timeout=5000] - Timeout in milliseconds
2825
- * @returns {Promise<void>}
2826
- */
2827
- doc: "Wait for text to appear on screen (deprecated - use find() in a loop instead)",
2828
- },
2829
- "wait-for-image": {
2830
- name: "waitForImage",
2831
- /**
2832
- * Wait for image to appear on screen
2833
- * @deprecated Use find() in a polling loop instead
2834
- * @param {Object|string} options - Options object or description (legacy positional)
2835
- * @param {string} options.description - Description of the image
2836
- * @param {number} [options.timeout=10000] - Timeout in milliseconds
2837
- * @returns {Promise<void>}
2838
- */
2839
- doc: "Wait for image to appear on screen (deprecated - use find() in a loop instead)",
2840
- },
2841
- "scroll-until-text": {
2842
- name: "scrollUntilText",
2843
- /**
2844
- * Scroll until text is found
2845
- * @param {Object|string} options - Options object or text (legacy positional)
2846
- * @param {string} options.text - Text to find
2847
- * @param {ScrollDirection} [options.direction='down'] - Scroll direction
2848
- * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
2849
- * @param {boolean} [options.invert=false] - Invert the match
2850
- * @returns {Promise<void>}
2851
- */
2852
- doc: "Scroll until text is found",
2853
- },
2854
- "scroll-until-image": {
2855
- name: "scrollUntilImage",
2856
- /**
2857
- * Scroll until image is found
2858
- * @param {Object|string} [options] - Options object or description (legacy positional)
2859
- * @param {string} [options.description] - Description of the image
2860
- * @param {ScrollDirection} [options.direction='down'] - Scroll direction
2861
- * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
2862
- * @param {string} [options.method='mouse'] - Scroll method
2863
- * @param {string} [options.path] - Path to image template
2864
- * @param {boolean} [options.invert=false] - Invert the match
2865
- * @returns {Promise<void>}
2866
- */
2867
- doc: "Scroll until image is found",
2868
- },
2869
- "focus-application": {
2870
- name: "focusApplication",
2871
- /**
2872
- * Focus an application by name
2873
- * @param {string} name - Application name
2874
- * @param {Object} [options] - Additional options (reserved for future use)
2875
- * @returns {Promise<string>}
2876
- */
2877
- doc: "Focus an application by name",
2878
- },
2879
- extract: {
2880
- name: "extract",
2881
- /**
2882
- * Extract information from the screen using AI
2883
- * @param {Object|string} options - Options object or description (legacy positional)
2884
- * @param {string} options.description - What to extract
2885
- * @returns {Promise<string>}
2886
- */
2887
- doc: "Extract information from the screen",
2888
- },
2889
- assert: {
2890
- name: "assert",
2891
- /**
2892
- * Make an AI-powered assertion
2893
- * @param {string} assertion - Assertion to check
2894
- * @param {Object} [options] - Additional options (reserved for future use)
2895
- * @returns {Promise<boolean>}
2896
- */
2897
- doc: "Make an AI-powered assertion",
2898
- },
2899
- exec: {
2900
- name: "exec",
2901
- /**
2902
- * Execute code in the sandbox
2903
- * @param {Object|ExecLanguage} options - Options object or language (legacy positional)
2904
- * @param {ExecLanguage} [options.language='pwsh'] - Language ('js', 'pwsh', or 'sh')
2905
- * @param {string} options.code - Code to execute
2906
- * @param {number} [options.timeout] - Timeout in milliseconds
2907
- * @param {boolean} [options.silent=false] - Suppress output
2908
- * @returns {Promise<string>}
2909
- */
2910
- doc: "Execute code in the sandbox",
2911
- },
2690
+ "hover-text": "hoverText",
2691
+ "hover-image": "hoverImage",
2692
+ "match-image": "matchImage",
2693
+ "type": "type",
2694
+ "press-keys": "pressKeys",
2695
+ "click": "click",
2696
+ "hover": "hover",
2697
+ "scroll": "scroll",
2698
+ "wait": "wait",
2699
+ "wait-for-text": "waitForText",
2700
+ "wait-for-image": "waitForImage",
2701
+ "scroll-until-text": "scrollUntilText",
2702
+ "scroll-until-image": "scrollUntilImage",
2703
+ "focus-application": "focusApplication",
2704
+ "extract": "extract",
2705
+ "assert": "assert",
2706
+ "exec": "exec",
2912
2707
  };
2913
2708
 
2914
- // Create SDK methods dynamically from commands
2915
- Object.keys(this.commands).forEach((commandName) => {
2916
- const command = this.commands[commandName];
2917
- const methodInfo = commandMapping[commandName];
2918
-
2919
- if (!methodInfo) {
2920
- // Skip commands not in mapping
2921
- return;
2922
- }
2709
+ // Create SDK methods that lazy-await connection then forward to this.commands
2710
+ for (const [commandName, methodName] of Object.entries(commandMapping)) {
2711
+ this[methodName] = async function (...args) {
2712
+ // Lazy-await: wait for connection if still pending
2713
+ if (this.__connectionPromise) {
2714
+ await this.__connectionPromise;
2715
+ }
2923
2716
 
2924
- const methodName = methodInfo.name;
2717
+ // Warn if previous command may not have been awaited
2718
+ if (this._lastCommandName && !this._lastPromiseSettled) {
2719
+ console.warn(
2720
+ `⚠️ Warning: Previous ${this._lastCommandName}() may not have been awaited.\n` +
2721
+ ` Add "await" before the call: await testdriver.${this._lastCommandName}(...)\n` +
2722
+ ` Unawaited promises can cause race conditions and flaky tests.`
2723
+ );
2724
+ }
2925
2725
 
2926
- // Create the wrapper method with proper stack trace handling
2927
- this[methodName] = async function (...args) {
2928
2726
  this._ensureConnected();
2929
2727
 
2930
2728
  // Capture the call site for better error reporting
2931
2729
  const callSite = {};
2932
2730
  Error.captureStackTrace(callSite, this[methodName]);
2933
2731
 
2732
+ // Track this promise for unawaited detection
2733
+ this._lastCommandName = methodName;
2734
+ this._lastPromiseSettled = false;
2735
+
2934
2736
  try {
2935
- return await command(...args);
2737
+ const result = await this.commands[commandName](...args);
2738
+ this._lastPromiseSettled = true;
2739
+ return result;
2936
2740
  } catch (error) {
2741
+ this._lastPromiseSettled = true;
2937
2742
  // Ensure we have a proper Error object with a message
2938
2743
  let properError = error;
2939
2744
  if (!(error instanceof Error)) {
2940
- // If it's not an Error object, create one with a proper message
2941
2745
  const errorMessage =
2942
2746
  error?.message || error?.reason || JSON.stringify(error);
2943
2747
  properError = new Error(errorMessage);
2944
- // Preserve additional properties
2945
2748
  if (error?.code) properError.code = error.code;
2946
2749
  if (error?.fullError) properError.fullError = error.fullError;
2947
2750
  }
2948
2751
 
2949
2752
  // Replace the stack trace to point to the actual caller instead of SDK internals
2950
2753
  if (Error.captureStackTrace && callSite.stack) {
2951
- // Preserve the error message but use the captured call site stack
2952
2754
  const errorMessage = properError.stack?.split("\n")[0];
2953
- const callerStack = callSite.stack?.split("\n").slice(1); // Skip "Error" line
2755
+ const callerStack = callSite.stack?.split("\n").slice(1);
2954
2756
  properError.stack = errorMessage + "\n" + callerStack.join("\n");
2955
2757
  }
2956
2758
  throw properError;
@@ -2962,7 +2764,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2962
2764
  value: methodName,
2963
2765
  writable: false,
2964
2766
  });
2965
- });
2767
+ }
2966
2768
  }
2967
2769
 
2968
2770
  // ====================================
@@ -2970,24 +2772,49 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2970
2772
  // ====================================
2971
2773
 
2972
2774
  /**
2973
- * Capture a screenshot of the current screen
2775
+ * Capture a screenshot of the current screen and save it to .testdriverai/screenshots
2974
2776
  * @param {number} [scale=1] - Scale factor for the screenshot (1 = original size)
2975
2777
  * @param {boolean} [silent=false] - Whether to suppress logging
2976
2778
  * @param {boolean} [mouse=false] - Whether to include mouse cursor
2977
- * @returns {Promise<string>} Base64 encoded PNG screenshot
2779
+ * @returns {Promise<string>} The file path where the screenshot was saved
2978
2780
  *
2979
2781
  * @example
2980
- * // Capture a screenshot
2981
- * const screenshot = await client.screenshot();
2982
- * fs.writeFileSync('screenshot.png', Buffer.from(screenshot, 'base64'));
2782
+ * // Capture a screenshot (saves to .testdriverai/screenshots)
2783
+ * const screenshotPath = await client.screenshot();
2784
+ * console.log('Screenshot saved to:', screenshotPath);
2983
2785
  *
2984
2786
  * @example
2985
2787
  * // Capture with mouse cursor visible
2986
- * const screenshot = await client.screenshot(1, false, true);
2788
+ * const screenshotPath = await client.screenshot(1, false, true);
2987
2789
  */
2988
2790
  async screenshot(scale = 1, silent = false, mouse = false) {
2989
2791
  this._ensureConnected();
2990
- return await this.system.captureScreenBase64(scale, silent, mouse);
2792
+ const base64Data = await this.system.captureScreenBase64(scale, silent, mouse);
2793
+
2794
+ // Save to .testdriverai/screenshots/<test-file-name> directory
2795
+ let screenshotsDir = path.join(process.cwd(), ".testdriverai", "screenshots");
2796
+ if (this.testFile) {
2797
+ const testFileName = path.basename(this.testFile, path.extname(this.testFile));
2798
+ screenshotsDir = path.join(screenshotsDir, testFileName);
2799
+ }
2800
+ if (!fs.existsSync(screenshotsDir)) {
2801
+ fs.mkdirSync(screenshotsDir, { recursive: true });
2802
+ }
2803
+
2804
+ const filename = `screenshot-${Date.now()}.png`;
2805
+ const filePath = path.join(screenshotsDir, filename);
2806
+
2807
+ // Remove data:image/png;base64, prefix if present
2808
+ const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, "");
2809
+ const buffer = Buffer.from(cleanBase64, "base64");
2810
+
2811
+ fs.writeFileSync(filePath, buffer);
2812
+
2813
+ if (!silent) {
2814
+ this.emitter.emit("log:info", `📸 Screenshot saved to: ${filePath}`);
2815
+ }
2816
+
2817
+ return filePath;
2991
2818
  }
2992
2819
 
2993
2820
  /**
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Manual test to verify unawaited promise detection
3
+ *
4
+ * Run with: vitest run test/manual-unawaited-promise.test.mjs
5
+ *
6
+ * Expected: You should see a warning like:
7
+ * ⚠️ Warning: Previous find() may not have been awaited.
8
+ */
9
+ import { describe, expect, it } from "vitest";
10
+ import { TestDriver } from "../lib/vitest/hooks.mjs";
11
+
12
+ describe("Unawaited Promise Detection", () => {
13
+ it("should warn when a promise is not awaited", async (context) => {
14
+ const testdriver = TestDriver(context);
15
+
16
+ await testdriver.provision.chrome({
17
+ url: 'https://example.com',
18
+ });
19
+
20
+ // INTENTIONALLY missing await - should trigger warning on next call
21
+ testdriver.find("some button");
22
+
23
+ // This second call should print a warning about the previous unawaited find()
24
+ const element = await testdriver.find("Example Domain heading");
25
+
26
+ console.log("Element found:", element.found());
27
+
28
+ // If we got here without error and saw the warning, the feature works!
29
+ expect(true).toBeTruthy();
30
+ });
31
+ });