testdriverai 7.2.80 → 7.2.82

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.
@@ -139,30 +139,32 @@ import { TestDriver } from "testdriverai/vitest/hooks";
139
139
 
140
140
  describe("My Test Suite", () => {
141
141
  it("should do something", async (context) => {
142
- // Initialize TestDriver
142
+ // Initialize TestDriver - screenshots are captured automatically before/after each command
143
143
  const testdriver = TestDriver(context);
144
144
 
145
145
  // Start with provision - this launches the sandbox and browser
146
146
  await testdriver.provision.chrome({
147
147
  url: "https://example.com",
148
148
  });
149
- await testdriver.screenshot(); // Capture initial page state
150
149
 
151
150
  // Find elements and interact
151
+ // Note: Screenshots are automatically captured before/after find() and click()
152
152
  const button = await testdriver.find("Sign In button");
153
- await testdriver.screenshot(); // Capture before click
154
153
  await button.click();
155
154
  await testdriver.wait(2000); // Wait for state change
156
- await testdriver.screenshot(); // Capture after click
157
155
 
158
156
  // Assert using natural language
159
- await testdriver.screenshot(); // Capture before assertion
157
+ // Screenshots are automatically captured before/after assert()
160
158
  const result = await testdriver.assert("the dashboard is visible");
161
159
  expect(result).toBeTruthy();
162
160
  });
163
161
  });
164
162
  ```
165
163
 
164
+ <Note>
165
+ **Automatic Screenshots**: TestDriver captures screenshots before and after every command by default. Screenshots are saved with descriptive names like `001-click-before-L42-submit-button.png` that include the line number from your test file.
166
+ </Note>
167
+
166
168
  ## Provisioning Options
167
169
 
168
170
  Most tests start with `testdriver.provision`.
@@ -220,54 +222,21 @@ await element.mouseUp(); // release mouse
220
222
  element.found(); // check if found (boolean)
221
223
  ```
222
224
 
223
- ### Screenshots for Debugging
224
-
225
- **Use `screenshot()` liberally throughout your tests** to capture the screen state at key moments. This makes debugging much easier when tests fail - you can see exactly what the screen looked like at each step.
225
+ ### Automatic Screenshots (Enabled by Default)
226
226
 
227
- ```javascript
228
- // Capture a screenshot - saved to .testdriver/screenshots/<test-file>/
229
- const screenshotPath = await testdriver.screenshot();
230
- console.log("Screenshot saved to:", screenshotPath);
227
+ TestDriver **automatically captures screenshots before and after every command** by default. This creates a complete visual timeline without any additional code. Screenshots are named with the line number from your test file, making it easy to trace issues:
231
228
 
232
- // Include mouse cursor in screenshot
233
- await testdriver.screenshot(1, false, true);
234
229
  ```
235
-
236
- **When to add screenshots:**
237
- - After provisioning (initial page load)
238
- - Before and after clicking important elements
239
- - After typing text into fields
240
- - Before assertions (to see what the AI is evaluating)
241
- - After any action that changes the page state
242
- - When debugging a flaky or failing test
243
-
244
- **⚠️ Important: Add delays before screenshots after actions**
245
-
246
- When you click or interact with an element that triggers a state change (page navigation, modal opening, content loading), **add a short delay before taking a screenshot** to allow the application state to update:
247
-
248
- ```javascript
249
- await element.click();
250
- await testdriver.wait(2000); // Wait 2-3 seconds for state change
251
- await testdriver.screenshot(); // Now capture the updated state
230
+ .testdriver/screenshots/login.test/
231
+ 001-find-before-L15-email-input.png
232
+ 002-find-after-L15-email-input.png
233
+ 003-click-before-L16-email-input.png
234
+ 004-click-after-L16-email-input.png
235
+ 005-type-before-L17-userexamplecom.png
236
+ 006-type-after-L17-userexamplecom.png
252
237
  ```
253
238
 
254
- This is especially important for:
255
- - Navigation clicks (page transitions)
256
- - Button clicks that open modals or dialogs
257
- - Form submissions
258
- - Actions that trigger AJAX requests or animations
259
- - Any interaction where visual feedback takes time to appear
260
-
261
- **Screenshot file organization:**
262
-
263
- ```
264
- .testdriver/
265
- screenshots/
266
- login.test/ # Folder per test file
267
- screenshot-1737633600000.png
268
- checkout.test/
269
- screenshot-1737633700000.png
270
- ```
239
+ **Filename format:** `<seq>-<action>-<phase>-L<line>-<description>.png`
271
240
 
272
241
  > **Note:** The screenshot folder for each test file is automatically cleared when the test starts.
273
242
 
@@ -295,30 +264,30 @@ session_start({ type: "chrome", url: "https://your-app.com/login", testFile: "te
295
264
  → Response includes: "ACTION REQUIRED: Append this code..."
296
265
  → ⚠️ IMMEDIATELY write to tests/login.test.mjs:
297
266
  await testdriver.provision.chrome({ url: "https://your-app.com/login" });
298
- await testdriver.screenshot(); // Capture initial page state
299
267
  ```
300
268
 
301
269
  This provisions a sandbox with Chrome and navigates to your URL. You'll see a screenshot of the initial page.
302
270
 
271
+ > **Note**: Screenshots are captured automatically before/after each command. The generated code no longer includes manual `screenshot()` calls.
272
+
303
273
  ### Step 2: Interact with the App
304
274
 
305
- Find elements and interact with them. **Write code to file after EACH action, including screenshots for debugging:**
275
+ Find elements and interact with them. **Write code to file after EACH action:**
306
276
 
307
277
  ```
308
278
  find_and_click({ description: "email input field" })
309
279
  → Returns: screenshot with element highlighted
310
280
  → ⚠️ IMMEDIATELY append to test file:
311
281
  await testdriver.find("email input field").click();
312
- await testdriver.wait(2000); // Wait for state change
313
- await testdriver.screenshot(); // Capture after click
314
282
 
315
283
  type({ text: "user@example.com" })
316
284
  → Returns: screenshot showing typed text
317
285
  → ⚠️ IMMEDIATELY append to test file:
318
286
  await testdriver.type("user@example.com");
319
- await testdriver.screenshot(); // Capture after typing
320
287
  ```
321
288
 
289
+ > **Note**: Screenshots are automatically captured before/after each command. Each screenshot filename includes the line number (e.g., `001-click-before-L42-email-input.png`).
290
+
322
291
  ### Step 3: Verify Actions Succeeded (For Your Understanding)
323
292
 
324
293
  After actions, use `check` to verify they worked. This is for YOUR understanding - does NOT generate code:
@@ -336,7 +305,6 @@ Use `assert` for pass/fail conditions. This DOES generate code for the test file
336
305
  assert({ assertion: "the dashboard is visible" })
337
306
  → Returns: pass/fail with screenshot
338
307
  → ⚠️ IMMEDIATELY append to test file:
339
- await testdriver.screenshot(); // Capture before assertion
340
308
  const assertResult = await testdriver.assert("the dashboard is visible");
341
309
  expect(assertResult).toBeTruthy();
342
310
  ```
@@ -372,28 +340,97 @@ Analyze the output, fix any issues, and iterate until the test passes.
372
340
  | `assert` | AI-powered boolean assertion - GENERATES CODE for test files |
373
341
  | `exec` | Execute JavaScript, shell, or PowerShell in sandbox |
374
342
  | `screenshot` | Capture screenshot - **only use when user explicitly asks** |
375
- | `list_local_screenshots` | List screenshots saved in `.testdriver` directory |
343
+ | `list_local_screenshots` | List/filter screenshots by line, action, phase, regex, etc. |
376
344
  | `view_local_screenshot` | View a local screenshot (returns image to AI + displays to user) |
377
345
 
378
346
  ### Debugging with Local Screenshots
379
347
 
380
- After test runs (successful or failed), you can view saved screenshots to understand test behavior:
348
+ After test runs (successful or failed), you can view saved screenshots to understand test behavior.
349
+
350
+ **Screenshot filename format:** `<seq>-<action>-<phase>-L<line>-<description>.png`
351
+ Example: `001-click-before-L42-submit-button.png`
381
352
 
382
- **1. List available screenshots:**
353
+ **1. List all screenshots from a test:**
383
354
 
384
355
  ```
385
356
  list_local_screenshots({ directory: "login.test" })
386
357
  ```
387
358
 
388
- This returns all screenshots from the specified test file, sorted by modification time (newest first).
359
+ **2. Filter by line number (find what happened at a specific line):**
360
+
361
+ ```
362
+ // Find screenshots from line 42
363
+ list_local_screenshots({ line: 42 })
364
+
365
+ // Find screenshots from lines 10-20
366
+ list_local_screenshots({ lineRange: { start: 10, end: 20 } })
367
+ ```
389
368
 
390
- **2. View specific screenshots:**
369
+ **3. Filter by action type:**
391
370
 
392
371
  ```
393
- view_local_screenshot({ path: ".testdriver/screenshots/login.test/after-click.png" })
372
+ // Find all click screenshots
373
+ list_local_screenshots({ action: "click" })
374
+
375
+ // Find all assertions
376
+ list_local_screenshots({ action: "assert" })
394
377
  ```
395
378
 
396
- This displays the screenshot to both you (the AI) and the user via MCP App.
379
+ **4. Filter by phase (before/after):**
380
+
381
+ ```
382
+ // See state BEFORE actions (useful for debugging what was visible)
383
+ list_local_screenshots({ phase: "before" })
384
+
385
+ // See state AFTER actions (useful for verifying results)
386
+ list_local_screenshots({ phase: "after" })
387
+ ```
388
+
389
+ **5. Filter by regex pattern:**
390
+
391
+ ```
392
+ // Find screenshots related to login
393
+ list_local_screenshots({ pattern: "login|signin" })
394
+
395
+ // Find button-related screenshots
396
+ list_local_screenshots({ pattern: "button.*click" })
397
+ ```
398
+
399
+ **6. Filter by sequence number:**
400
+
401
+ ```
402
+ // Find screenshots 1-5 (first 5 actions)
403
+ list_local_screenshots({ sequenceRange: { start: 1, end: 5 } })
404
+ ```
405
+
406
+ **7. Sort results:**
407
+
408
+ ```
409
+ // Sort by execution order (useful for understanding flow)
410
+ list_local_screenshots({ sortBy: "sequence" })
411
+
412
+ // Sort by line number (useful for tracing back to code)
413
+ list_local_screenshots({ sortBy: "line" })
414
+
415
+ // Sort by modified time (default - newest first)
416
+ list_local_screenshots({ sortBy: "modified" })
417
+ ```
418
+
419
+ **8. Combine filters:**
420
+
421
+ ```
422
+ // Find click screenshots at line 42
423
+ list_local_screenshots({ directory: "checkout.test", line: 42, action: "click" })
424
+
425
+ // Find all "before" screenshots in lines 10-30
426
+ list_local_screenshots({ lineRange: { start: 10, end: 30 }, phase: "before" })
427
+ ```
428
+
429
+ **9. View a screenshot:**
430
+
431
+ ```
432
+ view_local_screenshot({ path: ".testdriver/screenshots/login.test/001-click-before-L42-submit-button.png" })
433
+ ```
397
434
 
398
435
  **When to use screenshot viewing:**
399
436
 
@@ -402,17 +439,18 @@ This displays the screenshot to both you (the AI) and the user via MCP App.
402
439
  - **Comparing test runs** - View screenshots from multiple runs to identify flaky behavior
403
440
  - **Verifying test logic** - Before running a test, view screenshots from previous runs to understand the UI flow
404
441
 
405
- **Workflow example:**
442
+ **Debugging workflow example:**
406
443
 
407
444
  ```
408
- # Test failed, let's debug
409
- list_local_screenshots({ directory: "checkout.test" })
445
+ # Test failed at line 42, let's see what happened
446
+ list_local_screenshots({ line: 42 })
410
447
 
411
- # View the last few screenshots to see what happened
412
- view_local_screenshot({ path: ".testdriver/screenshots/checkout.test/screenshot-1737633620000.png" })
413
- view_local_screenshot({ path: ".testdriver/screenshots/checkout.test/before-assertion.png" })
448
+ # View the before/after state at that line
449
+ view_local_screenshot({ path: ".testdriver/screenshots/checkout.test/005-click-before-L42-submit-button.png" })
450
+ view_local_screenshot({ path: ".testdriver/screenshots/checkout.test/006-click-after-L42-submit-button.png" })
414
451
 
415
- # Analyze the UI state and update test code accordingly
452
+ # Check what the screen looked like before the failing action
453
+ list_local_screenshots({ directory: "checkout.test", phase: "before", limit: 10 })
416
454
  ```
417
455
 
418
456
  ### Tips for MCP Workflow
@@ -437,28 +475,28 @@ view_local_screenshot({ path: ".testdriver/screenshots/checkout.test/before-asse
437
475
 
438
476
  ```javascript
439
477
  // Development workflow example
478
+ // Note: Screenshots are automatically captured before/after each command!
440
479
  it("should incrementally build test", async (context) => {
441
480
  const testdriver = TestDriver(context);
442
481
  await testdriver.provision.chrome({ url: "https://example.com" });
443
- await testdriver.screenshot(); // Capture initial state
482
+ // Automatic screenshot: 001-provision-after-L3-chrome.png
444
483
 
445
484
  // Step 1: Find and inspect
446
485
  const element = await testdriver.find("Some button");
447
486
  console.log("Element found:", element.found());
448
487
  console.log("Coordinates:", element.x, element.y);
449
488
  console.log("Confidence:", element.confidence);
450
- await testdriver.screenshot(); // Capture after find
489
+ // Automatic screenshot: 002-find-after-L7-some-button.png
451
490
 
452
491
  // Step 2: Interact
453
492
  await element.click();
454
- await testdriver.wait(2000); // Wait for state change
455
- await testdriver.screenshot(); // Capture after click
493
+ // Automatic screenshot: 003-click-after-L13-element.png
456
494
 
457
- // Step 3: Assert and log
458
- await testdriver.screenshot(); // Capture before assertion
495
+ // Step 3: Assert
459
496
  const result = await testdriver.assert("Something happened");
460
497
  console.log("Assertion result:", result);
461
498
  expect(result).toBeTruthy();
499
+ // Automatic screenshot: 004-assert-after-L17-something-happened.png
462
500
 
463
501
  // Then add more steps...
464
502
  });
@@ -476,6 +514,7 @@ const testdriver = TestDriver(context, {
476
514
  resolution: "1366x768", // Sandbox resolution
477
515
  cache: true, // Enable element caching (default: true)
478
516
  cacheKey: "my-test", // Cache key for element finding
517
+ autoScreenshots: true, // Capture screenshots before/after each command (default: true)
479
518
  });
480
519
  ```
481
520
 
@@ -550,36 +589,36 @@ const date = await testdriver.exec("pwsh", "Get-Date", 5000);
550
589
 
551
590
  ### Capturing Screenshots
552
591
 
553
- **Add screenshots liberally throughout your tests** for debugging. When a test fails, you'll have a visual trail showing exactly what happened at each step.
554
-
555
- ```javascript
556
- // Basic screenshot - automatically saved to .testdriver/screenshots/<test-file>/
557
- await testdriver.screenshot();
558
-
559
- // Capture with mouse cursor visible
560
- await testdriver.screenshot(1, false, true);
592
+ **Screenshots are captured automatically** before and after each SDK command (click, type, find, assert, etc.). Each screenshot filename includes:
593
+ - Sequential number for chronological ordering
594
+ - Action name (e.g., `click`, `find`, `assert`)
595
+ - Phase (`before` or `after`)
596
+ - Line number from your test file
597
+ - Description from the command
561
598
 
562
- // Recommended pattern: screenshot after every significant action
563
- await testdriver.provision.chrome({ url: "https://example.com" });
564
- await testdriver.screenshot(); // After page load
599
+ Example filenames:
600
+ - `001-provision-after-L8-chrome.png`
601
+ - `002-find-before-L12-login-button.png`
602
+ - `003-click-after-L12-element.png`
565
603
 
566
- await testdriver.find("Login button").click();
567
- await testdriver.wait(2000); // Wait for state change
568
- await testdriver.screenshot(); // After click
604
+ Screenshots are saved to `.testdriver/screenshots/<test-file>/`.
569
605
 
570
- await testdriver.type("user@example.com");
571
- await testdriver.screenshot(); // After typing
606
+ To disable automatic screenshots:
607
+ ```javascript
608
+ const testdriver = TestDriver(context, { autoScreenshots: false });
609
+ ```
572
610
 
573
- await testdriver.screenshot(); // Before assertion
574
- const result = await testdriver.assert("dashboard is visible");
611
+ For manual screenshots (e.g., with mouse cursor visible):
612
+ ```javascript
613
+ await testdriver.screenshot(1, false, true);
575
614
  ```
576
615
 
577
616
  ## Tips for Agents
578
617
 
579
618
  1. **⚠️ WRITE CODE IMMEDIATELY** - After EVERY successful MCP action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the session ends.
580
619
  2. **⚠️ RUN TESTS YOURSELF** - Do NOT tell the user to run tests. YOU must run the tests using `npx vitest run <testFile> --reporter=dot`. Always use `--reporter=dot` for cleaner output. Analyze the output and iterate until the test passes. **Always share the test report link** (e.g., `https://app.testdriver.ai/projects/.../reports/...`) with the user after each run.
581
- 3. **⚠️ ADD SCREENSHOTS LIBERALLY** - Include `await testdriver.screenshot()` throughout your tests: after provision, before/after clicks, after typing, and before assertions. This creates a visual trail that makes debugging failures much easier.
582
- 4. **⚠️ USE SCREENSHOT VIEWING FOR DEBUGGING** - When tests fail, use `list_local_screenshots` and `view_local_screenshot` MCP commands to see exactly what the UI looked like. This is often faster than re-running the test.
620
+ 3. **Screenshots are automatic** - TestDriver captures screenshots before/after every command by default. Each screenshot filename includes the line number (e.g., `001-click-before-L42-submit-button.png`) making it easy to trace issues.
621
+ 4. **⚠️ USE SCREENSHOT VIEWING FOR DEBUGGING** - When tests fail, use `list_local_screenshots` and `view_local_screenshot` MCP commands to see exactly what the UI looked like. The filenames tell you which line of code triggered each screenshot.
583
622
  5. **⚠️ NEVER USE `.wait()`** - Do NOT use any `.wait()` method. Instead, use `find()` with a `timeout` option to poll for elements, or use `assert()` / `check()` to verify state. Explicit waits are flaky and slow.
584
623
  6. **Use MCP tools for development** - Build tests interactively with visual feedback
585
624
  7. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
@@ -8,6 +8,14 @@ const readline = require("readline");
8
8
  const os = require("os");
9
9
  const { execSync } = require("child_process");
10
10
 
11
+ // Load .env file for CLI usage (TD_API_ROOT, etc.)
12
+ require("dotenv").config();
13
+
14
+ // API configuration
15
+ const API_BASE_URL = process.env.TD_API_ROOT || "https://v6.testdriver.ai";
16
+ const POLL_INTERVAL = 5000; // 5 seconds
17
+ const POLL_TIMEOUT = 900000; // 15 minutes
18
+
11
19
  /**
12
20
  * Init command - scaffolds Vitest SDK example tests for TestDriver
13
21
  */
@@ -79,8 +87,33 @@ class InitCommand extends BaseCommand {
79
87
  }
80
88
 
81
89
  console.log(chalk.cyan(" Setting up your TestDriver API key...\n"));
90
+
91
+ // Ask user how they want to authenticate
92
+ const choice = await this.askChoice(
93
+ " How would you like to authenticate?\n",
94
+ [
95
+ { key: "1", label: "Login with browser", description: "(recommended)" },
96
+ { key: "2", label: "Enter API key manually", description: "" },
97
+ ],
98
+ );
99
+
100
+ if (choice === "1") {
101
+ // Browser login flow
102
+ try {
103
+ const apiKey = await this.browserLogin();
104
+ if (apiKey) {
105
+ console.log(chalk.green("\n ✓ Logged in successfully!\n"));
106
+ return apiKey;
107
+ }
108
+ } catch (error) {
109
+ console.log(chalk.yellow(`\n ⚠️ Browser login failed: ${error.message}\n`));
110
+ console.log(chalk.gray(" Falling back to manual API key entry...\n"));
111
+ }
112
+ }
113
+
114
+ // Manual API key entry
82
115
  console.log(
83
- chalk.gray(" Get your API key from: https://console.testdriver.ai/team"),
116
+ chalk.gray(" Get your API key from: https://console.testdriver.ai/team\n"),
84
117
  );
85
118
 
86
119
  // Ask if user wants to open the browser
@@ -89,7 +122,6 @@ class InitCommand extends BaseCommand {
89
122
  );
90
123
  if (shouldOpen) {
91
124
  try {
92
- // Dynamic import for ES module
93
125
  const open = (await import("open")).default;
94
126
  await open("https://console.testdriver.ai/team");
95
127
  console.log(chalk.gray(" Opening browser...\n"));
@@ -119,6 +151,119 @@ class InitCommand extends BaseCommand {
119
151
  }
120
152
  }
121
153
 
154
+ /**
155
+ * Browser-based login flow using device code
156
+ * @returns {Promise<string>} The API key
157
+ */
158
+ async browserLogin() {
159
+ // Step 1: Create device code
160
+ process.stdout.write(chalk.gray(" Requesting authorization code..."));
161
+
162
+ const createResponse = await fetch(`${API_BASE_URL}/auth/device/code`, {
163
+ method: "POST",
164
+ headers: { "Content-Type": "application/json" },
165
+ });
166
+
167
+ if (!createResponse.ok) {
168
+ throw new Error("Failed to create device code");
169
+ }
170
+
171
+ const { device_code, verification_uri, expires_in, interval } = await createResponse.json();
172
+ console.log(chalk.green(" done\n"));
173
+
174
+ // Step 2: Open browser
175
+ console.log(chalk.cyan(` Opening browser to authorize CLI...\n`));
176
+ console.log(chalk.gray(` If browser doesn't open, visit:\n ${verification_uri}\n`));
177
+
178
+ try {
179
+ const open = (await import("open")).default;
180
+ await open(verification_uri);
181
+ } catch (error) {
182
+ // Browser didn't open, user can use the URL manually
183
+ }
184
+
185
+ // Step 3: Poll for token
186
+ const pollInterval = (interval || 5) * 1000;
187
+ const timeout = (expires_in || 900) * 1000;
188
+ const startTime = Date.now();
189
+
190
+ process.stdout.write(chalk.gray(" Waiting for authorization..."));
191
+
192
+ // Start spinner
193
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
194
+ let spinnerIndex = 0;
195
+ const spinnerInterval = setInterval(() => {
196
+ process.stdout.write(`\r Waiting for authorization... ${spinnerFrames[spinnerIndex]}`);
197
+ spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
198
+ }, 100);
199
+
200
+ try {
201
+ while (Date.now() - startTime < timeout) {
202
+ await this.sleep(pollInterval);
203
+
204
+ const tokenResponse = await fetch(`${API_BASE_URL}/auth/device/token`, {
205
+ method: "POST",
206
+ headers: { "Content-Type": "application/json" },
207
+ body: JSON.stringify({ deviceCode: device_code }),
208
+ });
209
+
210
+ const data = await tokenResponse.json();
211
+
212
+ if (tokenResponse.ok && data.apiKey) {
213
+ clearInterval(spinnerInterval);
214
+ process.stdout.write("\r Waiting for authorization... " + chalk.green("✓") + "\n");
215
+ return data.apiKey;
216
+ }
217
+
218
+ if (data.error === "expired_token") {
219
+ clearInterval(spinnerInterval);
220
+ throw new Error("Authorization timed out. Please try again.");
221
+ }
222
+
223
+ // authorization_pending - continue polling
224
+ }
225
+
226
+ clearInterval(spinnerInterval);
227
+ throw new Error("Authorization timed out. Please try again.");
228
+ } catch (error) {
229
+ clearInterval(spinnerInterval);
230
+ process.stdout.write("\n");
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Ask user to choose from a list of options
237
+ */
238
+ async askChoice(question, options) {
239
+ return new Promise((resolve) => {
240
+ const rl = readline.createInterface({
241
+ input: process.stdin,
242
+ output: process.stdout,
243
+ });
244
+
245
+ console.log(question);
246
+ for (const opt of options) {
247
+ const desc = opt.description ? chalk.gray(` ${opt.description}`) : "";
248
+ console.log(` ${chalk.cyan(opt.key)}. ${opt.label}${desc}`);
249
+ }
250
+ console.log("");
251
+
252
+ rl.question(" Enter choice [1]: ", (answer) => {
253
+ rl.close();
254
+ const normalized = answer.trim() || "1";
255
+ resolve(normalized);
256
+ });
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Sleep for a given number of milliseconds
262
+ */
263
+ sleep(ms) {
264
+ return new Promise((resolve) => setTimeout(resolve, ms));
265
+ }
266
+
122
267
  /**
123
268
  * Prompt for hidden input (like password)
124
269
  */
@@ -1,5 +1,6 @@
1
1
  import { execSync } from "child_process";
2
2
  import crypto from "crypto";
3
+ import fs from "fs";
3
4
  import { createRequire } from "module";
4
5
  import path from "path";
5
6
  import { postOrUpdateTestResults } from "../lib/github-comment.mjs";
@@ -1201,6 +1202,49 @@ function getGitInfo() {
1201
1202
  // GitHub Comment Helper
1202
1203
  // ============================================================================
1203
1204
 
1205
+ /**
1206
+ * Extract PR number from GitHub Actions environment
1207
+ * Checks multiple sources: env vars, event file, and GITHUB_REF
1208
+ * @returns {string|null} PR number or null if not found
1209
+ */
1210
+ function extractPRNumber() {
1211
+ // Try direct environment variables first
1212
+ let prNumber =
1213
+ process.env.GITHUB_PR_NUMBER ||
1214
+ process.env.TD_GITHUB_PR ||
1215
+ process.env.PR_NUMBER;
1216
+
1217
+ if (prNumber) {
1218
+ return prNumber;
1219
+ }
1220
+
1221
+ // Try to extract from GitHub Actions event path
1222
+ if (process.env.GITHUB_EVENT_PATH) {
1223
+ try {
1224
+ const eventData = JSON.parse(
1225
+ fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"),
1226
+ );
1227
+ if (eventData.pull_request?.number) {
1228
+ return String(eventData.pull_request.number);
1229
+ }
1230
+ } catch (err) {
1231
+ logger.debug("Could not read GitHub event file:", err.message);
1232
+ }
1233
+ }
1234
+
1235
+ // Try to extract from GITHUB_REF (refs/pull/123/merge or refs/pull/123/head)
1236
+ if (process.env.GITHUB_REF) {
1237
+ const match = process.env.GITHUB_REF.match(
1238
+ /refs\/pull\/(\d+)\/(merge|head)/,
1239
+ );
1240
+ if (match) {
1241
+ return match[1];
1242
+ }
1243
+ }
1244
+
1245
+ return null;
1246
+ }
1247
+
1204
1248
  /**
1205
1249
  * Post GitHub comment with test results if enabled
1206
1250
  * Checks for GitHub token and PR number in environment variables
@@ -1220,7 +1264,7 @@ async function postGitHubCommentIfEnabled(testRunUrl, stats, completeData) {
1220
1264
 
1221
1265
  // Check if GitHub comment posting is enabled
1222
1266
  const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
1223
- const prNumber = process.env.GITHUB_PR_NUMBER;
1267
+ const prNumber = extractPRNumber();
1224
1268
  const commitSha = process.env.GITHUB_SHA || pluginState.gitInfo.commit;
1225
1269
 
1226
1270
  // Only post if we have a token and either a PR number or commit SHA
@@ -1388,20 +1388,57 @@ server.registerTool("exec", {
1388
1388
  throw error;
1389
1389
  }
1390
1390
  });
1391
+ function parseScreenshotFilename(filename) {
1392
+ // Match pattern: 001-click-before-L42-submit-button.png or 001-click-error-L42-submit-button.png
1393
+ const match = filename.match(/^(\d+)-([a-z]+)-(before|after|error)-L(\d+)-(.+)\.png$/i);
1394
+ if (match) {
1395
+ return {
1396
+ sequence: parseInt(match[1], 10),
1397
+ action: match[2].toLowerCase(),
1398
+ phase: match[3].toLowerCase(),
1399
+ lineNumber: parseInt(match[4], 10),
1400
+ description: match[5],
1401
+ };
1402
+ }
1403
+ return {};
1404
+ }
1391
1405
  // List Local Screenshots - lists screenshots saved to .testdriver directory
1392
1406
  server.registerTool("list_local_screenshots", {
1393
- description: `List screenshots saved in the .testdriver directory.
1407
+ description: `List and filter screenshots saved in the .testdriver directory.
1408
+
1409
+ Screenshots from auto-screenshot feature use the format: <seq>-<action>-<phase>-L<line>-<description>.png
1410
+ Example: 001-click-before-L42-submit-button.png
1394
1411
 
1395
- This tool helps you find screenshots that have been saved during test runs or via the screenshot tool.
1396
- Screenshots are organized in subdirectories like 'mcp-screenshots' and 'screenshots'.
1412
+ This tool supports powerful filtering to find specific screenshots:
1413
+ - By test file (directory)
1414
+ - By line number or range
1415
+ - By action type (click, find, type, assert, etc.)
1416
+ - By phase (before/after/error - error screenshots are captured when actions fail)
1417
+ - By regex pattern on filename
1418
+ - By sequence number range
1397
1419
 
1398
1420
  Returns a list of screenshot paths that can be viewed with the 'view_local_screenshot' tool.`,
1399
1421
  inputSchema: z.object({
1400
- directory: z.string().optional().describe("Subdirectory to list (e.g., 'mcp-screenshots', 'screenshots'). If not provided, lists all subdirectories."),
1422
+ directory: z.string().optional().describe("Test file or subdirectory to search (e.g., 'login.test', 'mcp-screenshots'). If not provided, searches all."),
1423
+ line: z.number().optional().describe("Filter by exact line number from test file (e.g., 42 matches L42)"),
1424
+ lineRange: z.object({
1425
+ start: z.number().describe("Start line number (inclusive)"),
1426
+ end: z.number().describe("End line number (inclusive)"),
1427
+ }).optional().describe("Filter by line number range (e.g., { start: 10, end: 20 })"),
1428
+ action: z.string().optional().describe("Filter by action type: click, find, type, assert, provision, scroll, hover, etc."),
1429
+ phase: z.enum(["before", "after", "error"]).optional().describe("Filter by phase: 'before' (pre-action), 'after' (post-action), or 'error' (when action fails)"),
1430
+ pattern: z.string().optional().describe("Regex pattern to match against filename (e.g., 'submit|login' or 'button.*click')"),
1431
+ sequence: z.number().optional().describe("Filter by exact sequence number"),
1432
+ sequenceRange: z.object({
1433
+ start: z.number().describe("Start sequence (inclusive)"),
1434
+ end: z.number().describe("End sequence (inclusive)"),
1435
+ }).optional().describe("Filter by sequence range (e.g., { start: 1, end: 10 })"),
1436
+ limit: z.number().optional().describe("Maximum number of results to return (default: 50)"),
1437
+ sortBy: z.enum(["modified", "sequence", "line"]).optional().describe("Sort by: 'modified' (newest first), 'sequence' (execution order), or 'line' (line number). Default: 'modified'"),
1401
1438
  }),
1402
1439
  }, async (params) => {
1403
1440
  const startTime = Date.now();
1404
- logger.info("list_local_screenshots: Starting", { directory: params.directory });
1441
+ logger.info("list_local_screenshots: Starting", { ...params });
1405
1442
  try {
1406
1443
  // Find .testdriver directory - check current working directory and common locations
1407
1444
  const possiblePaths = [
@@ -1420,6 +1457,16 @@ Returns a list of screenshot paths that can be viewed with the 'view_local_scree
1420
1457
  return createToolResult(false, "No .testdriver directory found. Screenshots are saved here during test runs.", { error: "Directory not found" });
1421
1458
  }
1422
1459
  const screenshots = [];
1460
+ // Compile regex pattern if provided
1461
+ let regexPattern = null;
1462
+ if (params.pattern) {
1463
+ try {
1464
+ regexPattern = new RegExp(params.pattern, "i");
1465
+ }
1466
+ catch {
1467
+ return createToolResult(false, `Invalid regex pattern: ${params.pattern}`, { error: "Invalid regex" });
1468
+ }
1469
+ }
1423
1470
  // Function to recursively find PNG files
1424
1471
  const findPngFiles = (dir) => {
1425
1472
  if (!fs.existsSync(dir))
@@ -1434,49 +1481,120 @@ Returns a list of screenshot paths that can be viewed with the 'view_local_scree
1434
1481
  }
1435
1482
  }
1436
1483
  else if (entry.isFile() && entry.name.toLowerCase().endsWith(".png")) {
1484
+ const parsed = parseScreenshotFilename(entry.name);
1485
+ // Apply filters
1486
+ if (params.line !== undefined && parsed.lineNumber !== params.line)
1487
+ continue;
1488
+ if (params.lineRange && (parsed.lineNumber === undefined ||
1489
+ parsed.lineNumber < params.lineRange.start ||
1490
+ parsed.lineNumber > params.lineRange.end))
1491
+ continue;
1492
+ if (params.action && parsed.action !== params.action.toLowerCase())
1493
+ continue;
1494
+ if (params.phase && parsed.phase !== params.phase)
1495
+ continue;
1496
+ if (params.sequence !== undefined && parsed.sequence !== params.sequence)
1497
+ continue;
1498
+ if (params.sequenceRange && (parsed.sequence === undefined ||
1499
+ parsed.sequence < params.sequenceRange.start ||
1500
+ parsed.sequence > params.sequenceRange.end))
1501
+ continue;
1502
+ if (regexPattern && !regexPattern.test(entry.name))
1503
+ continue;
1437
1504
  const stats = fs.statSync(fullPath);
1438
1505
  screenshots.push({
1439
1506
  path: fullPath,
1440
1507
  name: entry.name,
1441
1508
  modified: stats.mtime,
1442
1509
  size: stats.size,
1510
+ parsed,
1443
1511
  });
1444
1512
  }
1445
1513
  }
1446
1514
  };
1447
1515
  findPngFiles(testdriverDir);
1448
- // Sort by modification time (newest first)
1449
- screenshots.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1516
+ // Sort based on sortBy parameter
1517
+ const sortBy = params.sortBy || "modified";
1518
+ if (sortBy === "modified") {
1519
+ screenshots.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1520
+ }
1521
+ else if (sortBy === "sequence") {
1522
+ screenshots.sort((a, b) => (a.parsed.sequence ?? Infinity) - (b.parsed.sequence ?? Infinity));
1523
+ }
1524
+ else if (sortBy === "line") {
1525
+ screenshots.sort((a, b) => (a.parsed.lineNumber ?? Infinity) - (b.parsed.lineNumber ?? Infinity));
1526
+ }
1450
1527
  const duration = Date.now() - startTime;
1451
1528
  logger.info("list_local_screenshots: Completed", { count: screenshots.length, duration });
1452
1529
  if (screenshots.length === 0) {
1453
- return createToolResult(true, "No screenshots found in .testdriver directory.", {
1530
+ const filters = [];
1531
+ if (params.directory)
1532
+ filters.push(`directory=${params.directory}`);
1533
+ if (params.line)
1534
+ filters.push(`line=${params.line}`);
1535
+ if (params.lineRange)
1536
+ filters.push(`lineRange=${params.lineRange.start}-${params.lineRange.end}`);
1537
+ if (params.action)
1538
+ filters.push(`action=${params.action}`);
1539
+ if (params.phase)
1540
+ filters.push(`phase=${params.phase}`);
1541
+ if (params.pattern)
1542
+ filters.push(`pattern=${params.pattern}`);
1543
+ if (params.sequence)
1544
+ filters.push(`sequence=${params.sequence}`);
1545
+ if (params.sequenceRange)
1546
+ filters.push(`sequenceRange=${params.sequenceRange.start}-${params.sequenceRange.end}`);
1547
+ const filterMsg = filters.length > 0 ? ` with filters: ${filters.join(", ")}` : "";
1548
+ return createToolResult(true, `No screenshots found in .testdriver directory${filterMsg}.`, {
1454
1549
  action: "list_local_screenshots",
1455
1550
  count: 0,
1456
1551
  directory: testdriverDir,
1552
+ filters: params,
1457
1553
  duration
1458
1554
  });
1459
1555
  }
1460
- // Format the list for display
1461
- const screenshotList = screenshots.slice(0, 50).map((s, i) => {
1556
+ const limit = params.limit || 50;
1557
+ const limitedScreenshots = screenshots.slice(0, limit);
1558
+ // Format the list for display with parsed info
1559
+ const screenshotList = limitedScreenshots.map((s, i) => {
1462
1560
  const relativePath = path.relative(testdriverDir, s.path);
1463
1561
  const sizeKB = Math.round(s.size / 1024);
1464
1562
  const timeAgo = formatTimeAgo(s.modified);
1465
- return `${i + 1}. ${relativePath} (${sizeKB}KB, ${timeAgo})`;
1563
+ // Add parsed info if available
1564
+ const parts = [`${i + 1}. ${relativePath}`];
1565
+ const meta = [];
1566
+ if (s.parsed.lineNumber)
1567
+ meta.push(`L${s.parsed.lineNumber}`);
1568
+ if (s.parsed.action)
1569
+ meta.push(s.parsed.action);
1570
+ if (s.parsed.phase)
1571
+ meta.push(s.parsed.phase);
1572
+ meta.push(`${sizeKB}KB`);
1573
+ meta.push(timeAgo);
1574
+ parts.push(`(${meta.join(", ")})`);
1575
+ return parts.join(" ");
1466
1576
  }).join("\n");
1467
- const message = screenshots.length > 50
1468
- ? `Found ${screenshots.length} screenshots (showing 50 most recent):\n\n${screenshotList}`
1469
- : `Found ${screenshots.length} screenshot(s):\n\n${screenshotList}`;
1577
+ const message = screenshots.length > limit
1578
+ ? `Found ${screenshots.length} screenshots (showing ${limit} results, sorted by ${sortBy}):\n\n${screenshotList}`
1579
+ : `Found ${screenshots.length} screenshot(s) (sorted by ${sortBy}):\n\n${screenshotList}`;
1470
1580
  return createToolResult(true, message, {
1471
1581
  action: "list_local_screenshots",
1472
1582
  count: screenshots.length,
1583
+ returned: limitedScreenshots.length,
1473
1584
  directory: testdriverDir,
1474
- screenshots: screenshots.slice(0, 50).map(s => ({
1585
+ filters: params,
1586
+ sortBy,
1587
+ screenshots: limitedScreenshots.map(s => ({
1475
1588
  path: s.path,
1476
1589
  relativePath: path.relative(testdriverDir, s.path),
1477
1590
  name: s.name,
1478
1591
  modified: s.modified.toISOString(),
1479
1592
  sizeBytes: s.size,
1593
+ sequence: s.parsed.sequence,
1594
+ action: s.parsed.action,
1595
+ phase: s.parsed.phase,
1596
+ lineNumber: s.parsed.lineNumber,
1597
+ description: s.parsed.description,
1480
1598
  })),
1481
1599
  duration
1482
1600
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.2.80",
3
+ "version": "7.2.82",
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",
@@ -132,7 +132,7 @@
132
132
  "mocha": "^10.8.2",
133
133
  "node-addon-api": "^8.0.0",
134
134
  "prettier": "3.3.3",
135
- "testdriverai": "^7.2.60",
135
+ "testdriverai": "^7.2.79",
136
136
  "vitest": "^4.0.18"
137
137
  },
138
138
  "optionalDependencies": {
package/sdk.d.ts CHANGED
@@ -263,6 +263,13 @@ export interface TestDriverOptions {
263
263
  reconnect?: boolean;
264
264
  /** Enable/disable Dashcam video recording (default: true) */
265
265
  dashcam?: boolean;
266
+ /**
267
+ * Enable automatic screenshots before and after each command (default: true)
268
+ * Screenshots are saved to .testdriver/screenshots/<test>/ with descriptive filenames
269
+ * Format: <seq>-<action>-<phase>-L<line>-<description>.png
270
+ * Example: 001-click-before-L42-submit-button.png
271
+ */
272
+ autoScreenshots?: boolean;
266
273
  /** Redraw configuration for screen change detection */
267
274
  redraw?:
268
275
  | boolean
package/sdk.js CHANGED
@@ -69,6 +69,53 @@ function getCallerFileHash() {
69
69
  }
70
70
  }
71
71
 
72
+ /**
73
+ * Get detailed caller information including file path, line number, and column
74
+ * Used for automatic screenshot naming to identify which line of code triggered an action
75
+ * @param {number} [skipFrames=0] - Additional frames to skip in the stack trace
76
+ * @returns {{filePath: string|null, line: number|null, column: number|null, functionName: string|null}}
77
+ */
78
+ function getCallerInfo(skipFrames = 0) {
79
+ const originalPrepareStackTrace = Error.prepareStackTrace;
80
+ try {
81
+ const err = new Error();
82
+ Error.prepareStackTrace = (_, stack) => stack;
83
+ const stack = err.stack;
84
+ Error.prepareStackTrace = originalPrepareStackTrace;
85
+
86
+ // Look for the first file that's not sdk.js, hooks.mjs, or node internals
87
+ let skipped = 0;
88
+ for (const callSite of stack) {
89
+ const fileName = callSite.getFileName();
90
+ if (
91
+ fileName &&
92
+ !fileName.includes("sdk.js") &&
93
+ !fileName.includes("hooks.mjs") &&
94
+ !fileName.includes("hooks.js") &&
95
+ !fileName.includes("node_modules") &&
96
+ !fileName.includes("node:internal") &&
97
+ fileName !== "evalmachine.<anonymous>"
98
+ ) {
99
+ if (skipped < skipFrames) {
100
+ skipped++;
101
+ continue;
102
+ }
103
+ return {
104
+ filePath: fileName,
105
+ line: callSite.getLineNumber(),
106
+ column: callSite.getColumnNumber(),
107
+ functionName: callSite.getFunctionName(),
108
+ };
109
+ }
110
+ }
111
+ } catch (error) {
112
+ // Silently fail and return nulls
113
+ } finally {
114
+ Error.prepareStackTrace = originalPrepareStackTrace;
115
+ }
116
+ return { filePath: null, line: null, column: null, functionName: null };
117
+ }
118
+
72
119
  /**
73
120
  * Custom error class for element operation failures
74
121
  * Includes debugging information like screenshots and AI responses
@@ -1430,6 +1477,12 @@ class TestDriverSDK {
1430
1477
  this._lastPromiseSettled = true;
1431
1478
  this._lastCommandName = null;
1432
1479
 
1480
+ // Auto-screenshots configuration
1481
+ // When enabled, automatically captures screenshots before/after each command
1482
+ // Screenshots are saved to .testdriver/screenshots/<test>/ with descriptive names
1483
+ this.autoScreenshots = options.autoScreenshots !== false;
1484
+ this._screenshotSequence = 0; // Counter for sequential screenshot naming
1485
+
1433
1486
  // Set up command methods that lazy-await connection
1434
1487
  this._setupCommandMethods();
1435
1488
  }
@@ -2733,10 +2786,18 @@ CAPTCHA_SOLVER_EOF`,
2733
2786
 
2734
2787
  this._ensureConnected();
2735
2788
 
2789
+ // Get caller info for auto-screenshot naming
2790
+ const callerInfo = this.autoScreenshots ? getCallerInfo() : null;
2791
+
2736
2792
  // Track this promise for unawaited detection
2737
2793
  this._lastCommandName = "find";
2738
2794
  this._lastPromiseSettled = false;
2739
2795
 
2796
+ // Take "before" screenshot if enabled
2797
+ if (this.autoScreenshots) {
2798
+ await this._saveAutoScreenshot("find", "before", callerInfo, description);
2799
+ }
2800
+
2740
2801
  const element = new Element(
2741
2802
  description,
2742
2803
  this,
@@ -2744,6 +2805,12 @@ CAPTCHA_SOLVER_EOF`,
2744
2805
  this.commands,
2745
2806
  );
2746
2807
  const result = await element.find(null, options);
2808
+
2809
+ // Take "after" screenshot if enabled
2810
+ if (this.autoScreenshots) {
2811
+ await this._saveAutoScreenshot("find", "after", callerInfo, description);
2812
+ }
2813
+
2747
2814
  this._lastPromiseSettled = true;
2748
2815
  return result;
2749
2816
  })();
@@ -2792,10 +2859,18 @@ CAPTCHA_SOLVER_EOF`,
2792
2859
 
2793
2860
  this._ensureConnected();
2794
2861
 
2862
+ // Get caller info for auto-screenshot naming
2863
+ const callerInfo = this.autoScreenshots ? getCallerInfo() : null;
2864
+
2795
2865
  // Track this promise for unawaited detection
2796
2866
  this._lastCommandName = "findAll";
2797
2867
  this._lastPromiseSettled = false;
2798
2868
 
2869
+ // Take "before" screenshot if enabled
2870
+ if (this.autoScreenshots) {
2871
+ await this._saveAutoScreenshot("findAll", "before", callerInfo, description);
2872
+ }
2873
+
2799
2874
  // Capture absolute timestamp at the very start of the command
2800
2875
  // Frontend will calculate relative time using: timestamp - replay.clientStartDate
2801
2876
  const absoluteTimestamp = Date.now();
@@ -2951,6 +3026,11 @@ CAPTCHA_SOLVER_EOF`,
2951
3026
  this.emitter.emit(events.log.debug, ` Time: ${duration}ms`);
2952
3027
  }
2953
3028
 
3029
+ // Take "after" screenshot if enabled
3030
+ if (this.autoScreenshots) {
3031
+ await this._saveAutoScreenshot("findAll", "after", callerInfo, description);
3032
+ }
3033
+
2954
3034
  this._lastPromiseSettled = true;
2955
3035
  return elements;
2956
3036
  } else {
@@ -2989,6 +3069,11 @@ CAPTCHA_SOLVER_EOF`,
2989
3069
  });
2990
3070
  }
2991
3071
 
3072
+ // Take "after" screenshot if enabled (no elements found)
3073
+ if (this.autoScreenshots) {
3074
+ await this._saveAutoScreenshot("findAll", "after", callerInfo, description);
3075
+ }
3076
+
2992
3077
  // No elements found - return empty array
2993
3078
  this._lastPromiseSettled = true;
2994
3079
  return [];
@@ -3025,6 +3110,11 @@ CAPTCHA_SOLVER_EOF`,
3025
3110
  });
3026
3111
  }
3027
3112
 
3113
+ // Take "error" screenshot if enabled
3114
+ if (this.autoScreenshots) {
3115
+ await this._saveAutoScreenshot("findAll", "error", callerInfo, description);
3116
+ }
3117
+
3028
3118
  this._lastPromiseSettled = true;
3029
3119
  return [];
3030
3120
  }
@@ -3072,6 +3162,7 @@ CAPTCHA_SOLVER_EOF`,
3072
3162
  /**
3073
3163
  * Dynamically set up command methods based on available commands
3074
3164
  * This creates camelCase methods that wrap the underlying command functions
3165
+ * When autoScreenshots is enabled, captures before/after screenshots for each command
3075
3166
  * @private
3076
3167
  */
3077
3168
  _setupCommandMethods() {
@@ -3096,6 +3187,53 @@ CAPTCHA_SOLVER_EOF`,
3096
3187
  exec: "exec",
3097
3188
  };
3098
3189
 
3190
+ // Helper to extract a description from command args for screenshot naming
3191
+ const getDescriptionFromArgs = (methodName, args) => {
3192
+ if (!args || args.length === 0) return "";
3193
+ const firstArg = args[0];
3194
+
3195
+ switch (methodName) {
3196
+ case "type":
3197
+ // For type, use the text being typed (truncated)
3198
+ return typeof firstArg === "string" ? firstArg.substring(0, 20) : "";
3199
+ case "pressKeys":
3200
+ // For pressKeys, show the keys
3201
+ return Array.isArray(firstArg) ? firstArg.join("+") : String(firstArg);
3202
+ case "click":
3203
+ case "hover":
3204
+ // For click/hover, try to get coordinates or prompt
3205
+ if (typeof firstArg === "object" && firstArg !== null) {
3206
+ return firstArg.prompt || `${firstArg.x},${firstArg.y}`;
3207
+ }
3208
+ return typeof firstArg === "number" ? `${firstArg},${args[1]}` : "";
3209
+ case "scroll":
3210
+ // For scroll, show direction
3211
+ return typeof firstArg === "string" ? firstArg : "down";
3212
+ case "waitForText":
3213
+ case "scrollUntilText":
3214
+ // For text-based commands, use the text
3215
+ if (typeof firstArg === "object" && firstArg !== null) {
3216
+ return firstArg.text || "";
3217
+ }
3218
+ return typeof firstArg === "string" ? firstArg : "";
3219
+ case "focusApplication":
3220
+ // For focus, use the app name
3221
+ return typeof firstArg === "string" ? firstArg : "";
3222
+ case "assert":
3223
+ case "extract":
3224
+ // For assert/extract, use the assertion/description
3225
+ return typeof firstArg === "string" ? firstArg.substring(0, 30) : "";
3226
+ case "exec":
3227
+ // For exec, show the language
3228
+ if (typeof firstArg === "object" && firstArg !== null) {
3229
+ return firstArg.language || "code";
3230
+ }
3231
+ return typeof firstArg === "string" ? firstArg : "code";
3232
+ default:
3233
+ return typeof firstArg === "string" ? firstArg.substring(0, 20) : "";
3234
+ }
3235
+ };
3236
+
3099
3237
  // Create SDK methods that lazy-await connection then forward to this.commands
3100
3238
  for (const [commandName, methodName] of Object.entries(commandMapping)) {
3101
3239
  this[methodName] = async function (...args) {
@@ -3115,19 +3253,39 @@ CAPTCHA_SOLVER_EOF`,
3115
3253
 
3116
3254
  this._ensureConnected();
3117
3255
 
3118
- // Capture the call site for better error reporting
3256
+ // Capture the call site for better error reporting AND for auto-screenshots
3119
3257
  const callSite = {};
3120
3258
  Error.captureStackTrace(callSite, this[methodName]);
3121
3259
 
3260
+ // Get caller info for auto-screenshot naming
3261
+ const callerInfo = this.autoScreenshots ? getCallerInfo() : null;
3262
+ const description = this.autoScreenshots ? getDescriptionFromArgs(methodName, args) : "";
3263
+
3122
3264
  // Track this promise for unawaited detection
3123
3265
  this._lastCommandName = methodName;
3124
3266
  this._lastPromiseSettled = false;
3125
3267
 
3126
3268
  try {
3269
+ // Take "before" screenshot if enabled
3270
+ if (this.autoScreenshots) {
3271
+ await this._saveAutoScreenshot(methodName, "before", callerInfo, description);
3272
+ }
3273
+
3127
3274
  const result = await this.commands[commandName](...args);
3275
+
3276
+ // Take "after" screenshot if enabled
3277
+ if (this.autoScreenshots) {
3278
+ await this._saveAutoScreenshot(methodName, "after", callerInfo, description);
3279
+ }
3280
+
3128
3281
  this._lastPromiseSettled = true;
3129
3282
  return result;
3130
3283
  } catch (error) {
3284
+ // Take "error" screenshot if enabled (instead of "after")
3285
+ if (this.autoScreenshots) {
3286
+ await this._saveAutoScreenshot(methodName, "error", callerInfo, description);
3287
+ }
3288
+
3131
3289
  this._lastPromiseSettled = true;
3132
3290
  // Ensure we have a proper Error object with a message
3133
3291
  let properError = error;
@@ -3212,6 +3370,80 @@ CAPTCHA_SOLVER_EOF`,
3212
3370
  return filePath;
3213
3371
  }
3214
3372
 
3373
+ /**
3374
+ * Save an automatic screenshot with descriptive naming
3375
+ * Used internally when autoScreenshots is enabled
3376
+ * @private
3377
+ * @param {string} actionName - Name of the action (click, type, hover, etc.)
3378
+ * @param {string} phase - 'before' or 'after'
3379
+ * @param {Object} callerInfo - Caller information from getCallerInfo()
3380
+ * @param {string} [description] - Optional description of the action target
3381
+ * @returns {Promise<string|null>} The file path where the screenshot was saved, or null if failed
3382
+ */
3383
+ async _saveAutoScreenshot(actionName, phase, callerInfo, description = "") {
3384
+ if (!this.autoScreenshots || !this.connected) {
3385
+ return null;
3386
+ }
3387
+
3388
+ try {
3389
+ // Increment sequence for unique ordering
3390
+ this._screenshotSequence++;
3391
+ const seq = String(this._screenshotSequence).padStart(3, "0");
3392
+
3393
+ // Extract line number info
3394
+ const lineInfo = callerInfo.line ? `L${callerInfo.line}` : "L???";
3395
+
3396
+ // Sanitize description for filename (remove special chars, limit length)
3397
+ const sanitizedDesc = description
3398
+ .replace(/[^a-zA-Z0-9\s-]/g, "")
3399
+ .replace(/\s+/g, "-")
3400
+ .substring(0, 30)
3401
+ .toLowerCase();
3402
+
3403
+ // Build filename: 001-click-before-L42-submit-button.png
3404
+ const descPart = sanitizedDesc ? `-${sanitizedDesc}` : "";
3405
+ const filename = `${seq}-${actionName}-${phase}-${lineInfo}${descPart}.png`;
3406
+
3407
+ const base64Data = await this.system.captureScreenBase64(1, false, false);
3408
+
3409
+ // Save to .testdriver/screenshots/<test-file-name> directory
3410
+ let screenshotsDir = path.join(process.cwd(), ".testdriver", "screenshots");
3411
+ if (this.testFile) {
3412
+ const testFileName = path.basename(
3413
+ this.testFile,
3414
+ path.extname(this.testFile),
3415
+ );
3416
+ screenshotsDir = path.join(screenshotsDir, testFileName);
3417
+ }
3418
+ if (!fs.existsSync(screenshotsDir)) {
3419
+ fs.mkdirSync(screenshotsDir, { recursive: true });
3420
+ }
3421
+
3422
+ const filePath = path.join(screenshotsDir, filename);
3423
+
3424
+ // Remove data:image/png;base64, prefix if present
3425
+ const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, "");
3426
+ const buffer = Buffer.from(cleanBase64, "base64");
3427
+
3428
+ fs.writeFileSync(filePath, buffer);
3429
+
3430
+ // Debug log in verbose mode
3431
+ const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
3432
+ if (debugMode) {
3433
+ this.emitter.emit("log:debug", `📸 Auto-screenshot: ${filename}`);
3434
+ }
3435
+
3436
+ return filePath;
3437
+ } catch (error) {
3438
+ // Don't fail the command if screenshot fails
3439
+ const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
3440
+ if (debugMode) {
3441
+ this.emitter.emit("log:debug", `Failed to save auto-screenshot: ${error.message}`);
3442
+ }
3443
+ return null;
3444
+ }
3445
+ }
3446
+
3215
3447
  /**
3216
3448
  * Ensure the SDK is connected before running commands
3217
3449
  * @private