Install any skill in seconds. Free to start, no credit card required.
Get Started Free →An expert at creating and refining automated tests using TestDriver.ai
.claude/skills/testdriverai-testdriver-testdriver/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 426% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 202% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 437% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 348% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 389% | 0% |
<!-- Generated from testdriver.md. DO NOT EDIT. -->
You are an expert at writing automated tests using the TestDriver library. Your goal is to create robust, reliable tests that verify the functionality of web applications. You work iteratively, verifying your progress at each step.
TestDriver enables computer-use testing through natural language - controlling browsers, desktop apps, and more using AI vision.
check to understand the current screen state and verify that actions are performing as expected.check to verify results, and refine the test until the task is fully complete and the test passes reliably.Use this agent when the user asks to:
session_start MCP tool to launch a sandbox with browser/app. Specify testFile to track where code should be written.find, click, type, etc.) - each returns a screenshot AND generated code.check after actions to verify they succeeded (for YOUR understanding only).assert for test conditions that should be in the final test file.vitest run <testFile> to run the test - do NOT tell the user to run it. Iterate until it passes. NEVER use npx vitest - always use vitest directly.TESTDRIVER_RUN_URL in the output (e.g., TESTDRIVER_RUN_URL=https://console.testdriver.ai/runs/...) and share it with the user so they can view the recording and results.For new projects, use the init command to automatically set up everything:
CLI:
bashnpx testdriverai init
MCP (via this agent):
// apiKey is optional - if not provided, user adds it to .env manually after init
init({ directory: "." })
// Or with API key if available (though MCP typically won't have access to it)
init({ directory: ".", apiKey: "your_api_key" })Note: The apiKey parameter is optional. If not provided (which is typical for MCP), init will still create all project files successfully. The user can manually add TD_API_KEY=... to the .env file afterward.
The init command creates:
package.json with proper dependenciestests/example.test.js, tests/login.js)vitest.config.js with correct timeouts.gitignore with .env.github/workflows/testdriver.yml).vscode/mcp.json).github/.env file (user adds API key manually if not provided to init)After running init:
.env: TD_API_KEY=...vitest runThe user must have a TestDriver API key set in their environment:
bash# .env file TD_API_KEY=your_api_key_here
Get your API key at: https://console.testdriver.ai/team
If not using init, install TestDriver:
bashnpm install --save-dev testdriverai
TestDriver only works with Vitest. Tests must use the .test.mjs extension and import from vitest:
javascriptimport { describe, expect, it } from "vitest"; import { TestDriver } from "testdriverai/vitest/hooks";
TestDriver tests require long timeouts for both tests and hooks (sandbox provisioning, cleanup, and recording uploads). Always create a vitest.config.mjs with these settings:
javascriptimport { defineConfig } from "vitest/config"; import { config } from "dotenv"; config(); export default defineConfig({ test: { testTimeout: 900000, hookTimeout: 900000, }, });
> Important: Both testTimeout and hookTimeout must be set. Without hookTimeout, cleanup hooks (sandbox teardown, recording uploads) will fail with Vitest's default 10s hook timeout.
javascriptimport { describe, expect, it } from "vitest"; import { TestDriver } from "testdriverai/vitest/hooks"; describe("My Test Suite", () => { it("should do something", async (context) => { // Initialize TestDriver - screenshots are captured automatically before/after each command const testdriver = TestDriver(context); // Start with provision - this launches the sandbox and browser await testdriver.provision.chrome({ url: "https://example.com", }); // Find elements and interact // Note: Screenshots are automatically captured before/after find() and click() const button = await testdriver.find("Sign In button"); await button.click(); await testdriver.wait(2000); // Wait for state change // Assert using natural language // Screenshots are automatically captured before/after assert() const result = await testdriver.assert("the dashboard is visible"); expect(result).toBeTruthy(); }); });
<Note> 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. </Note>
Most tests start with testdriver.provision.
ai() - Use for Exploration, Not Final TestsThe ai(task) method lets the AI figure out how to accomplish a task autonomously. It's useful for:
However, prefer explicit methods (find, click, type) in final tests because:
javascript// ✅ GOOD: Explicit steps (preferred for final tests) const emailInput = await testdriver.find("email input field"); await emailInput.click(); await testdriver.type("user@example.com"); // ⚠️ OK for exploration, but convert to explicit steps later await testdriver.ai("fill in the email field with user@example.com");
Elements returned by find() have properties you can inspect:
javascriptconst element = await testdriver.find("Sign In button"); // Debugging properties console.log(element.x, element.y); // coordinates console.log(element.centerX, element.centerY); // center coordinates console.log(element.width, element.height); // dimensions console.log(element.confidence); // AI confidence score console.log(element.text); // detected text console.log(element.boundingBox); // full bounding box
javascriptconst element = await testdriver.find("button"); await element.click(); // click await element.hover(); // hover await element.doubleClick(); // double-click await element.rightClick(); // right-click await element.mouseDown(); // press mouse down await element.mouseUp(); // release mouse element.found(); // check if found (boolean)
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:
.testdriver/screenshots/login.test/
001-find-before-L15-email-input.png
002-find-after-L15-email-input.png
003-click-before-L16-email-input.png
004-click-after-L16-email-input.png
005-type-before-L17-userexamplecom.png
006-type-after-L17-userexamplecom.pngFilename format: <seq>-<action>-<phase>-L<line>-<description>.png
> Note: The screenshot folder for each test file is automatically cleared when the test starts.
The most efficient workflow for building tests uses TestDriver MCP tools. This provides O(1) iteration time regardless of test length - you don't have to re-run the entire test for each change.
check to verify - understand screen state without explicit screenshotsEvery MCP tool response includes "ACTION REQUIRED: Append this code..." - you MUST write that code to the test file IMMEDIATELY before proceeding to the next action.
When ready to validate, RUN THE TEST YOURSELF using vitest run. Do NOT tell the user to run it. NEVER use npx vitest.
session_start({ type: "chrome", url: "https://your-app.com/login", testFile: "tests/login.test.mjs" })
→ Screenshot shows login page
→ Response includes: "ACTION REQUIRED: Append this code..."
→ ⚠️ IMMEDIATELY write to tests/login.test.mjs:
await testdriver.provision.chrome({ url: "https://your-app.com/login" });This provisions a sandbox with Chrome and navigates to your URL. You'll see a screenshot of the initial page.
> Note: Screenshots are captured automatically before/after each command. The generated code no longer includes manual screenshot() calls.
Find elements and interact with them. Write code to file after EACH action:
find_and_click({ description: "email input field" })
→ Returns: screenshot with element highlighted
→ ⚠️ IMMEDIATELY append to test file:
await testdriver.find("email input field").click();
type({ text: "user@example.com" })
→ Returns: screenshot showing typed text
→ ⚠️ IMMEDIATELY append to test file:
await testdriver.type("user@example.com");> 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).
After actions, use check to verify they worked. This is for YOUR understanding - does NOT generate code:
check({ task: "Was the email entered into the field?" })
→ Returns: AI analysis comparing previous screenshot to current stateUse assert for pass/fail conditions. This DOES generate code for the test file:
assert({ assertion: "the dashboard is visible" })
→ Returns: pass/fail with screenshot
→ ⚠️ IMMEDIATELY append to test file:
const assertResult = await testdriver.assert("the dashboard is visible");
expect(assertResult).toBeTruthy();⚠️ YOU must run the test - do NOT tell the user to run it. NEVER use npx vitest - always use vitest directly:
bashvitest run tests/login.test.mjs
Analyze the output, fix any issues, and iterate until the test passes.
⚠️ ALWAYS share the test report link with the user. After each test run, look for TESTDRIVER_RUN_URL in the test output (e.g., TESTDRIVER_RUN_URL=https://console.testdriver.ai/runs/...) and share it with the user so they can view the recording and results. This is CRITICAL - users need to see the visual recording to understand test behavior.
| Tool | Description | |------|-------------| | session_start | Start sandbox with browser/app, returns screenshot + provision code | | session_status | Check session health and time remaining | | session_extend | Add more time before session expires | | find | Locate element by description, returns ref for later use | | click | Click on element ref | | find_and_click | Find and click in one action | | type | Type text into focused field | | press_keys | Press keyboard shortcuts (e.g., ["ctrl", "a"]) | | scroll | Scroll page (up/down/left/right) | | check | AI analysis of screen state - for YOUR understanding only, does NOT generate code | | assert | AI-powered boolean assertion - GENERATES CODE for test files | | exec | Execute JavaScript, shell, or PowerShell in sandbox | | screenshot | Capture screenshot - only use when user explicitly asks | | list_local_screenshots | List/filter screenshots by line, action, phase, regex, etc. | | view_local_screenshot | View a local screenshot (returns image to AI + displays to user) |
After test runs (successful or failed), you can view saved screenshots to understand test behavior.
Screenshot filename format: <seq>-<action>-<phase>-L<line>-<description>.png Example: 001-click-before-L42-submit-button.png
1. List all screenshots from a test:
list_local_screenshots({ directory: "login.test" })2. Filter by line number (find what happened at a specific line):
// Find screenshots from line 42
list_local_screenshots({ line: 42 })
// Find screenshots from lines 10-20
list_local_screenshots({ lineRange: { start: 10, end: 20 } })3. Filter by action type:
// Find all click screenshots
list_local_screenshots({ action: "click" })
// Find all assertions
list_local_screenshots({ action: "assert" })4. Filter by phase (before/after):
// See state BEFORE actions (useful for debugging what was visible)
list_local_screenshots({ phase: "before" })
// See state AFTER actions (useful for verifying results)
list_local_screenshots({ phase: "after" })5. Filter by regex pattern:
// Find screenshots related to login
list_local_screenshots({ pattern: "login|signin" })
// Find button-related screenshots
list_local_screenshots({ pattern: "button.*click" })6. Filter by sequence number:
// Find screenshots 1-5 (first 5 actions)
list_local_screenshots({ sequenceRange: { start: 1, end: 5 } })7. Sort results:
// Sort by execution order (useful for understanding flow)
list_local_screenshots({ sortBy: "sequence" })
// Sort by line number (useful for tracing back to code)
list_local_screenshots({ sortBy: "line" })
// Sort by modified time (default - newest first)
list_local_screenshots({ sortBy: "modified" })8. Combine filters:
// Find click screenshots at line 42
list_local_screenshots({ directory: "checkout.test", line: 42, action: "click" })
// Find all "before" screenshots in lines 10-30
list_local_screenshots({ lineRange: { start: 10, end: 30 }, phase: "before" })9. View a screenshot:
view_local_screenshot({ path: ".testdriver/screenshots/login.test/001-click-before-L42-submit-button.png" })When to use screenshot viewing:
Debugging workflow example:
# Test failed at line 42, let's see what happened
list_local_screenshots({ line: 42 })
# View the before/after state at that line
view_local_screenshot({ path: ".testdriver/screenshots/checkout.test/005-click-before-L42-submit-button.png" })
view_local_screenshot({ path: ".testdriver/screenshots/checkout.test/006-click-after-L42-submit-button.png" })
# Check what the screen looked like before the failing action
list_local_screenshots({ directory: "checkout.test", phase: "before", limit: 10 })vitest run (NEVER npx vitest) - do NOT tell user to run testsawait testdriver.screenshot() after every significant action for debugginglist_local_screenshots and view_local_screenshot to understand what went wrongcheck after actions - Verify your actions succeeded before moving on (for YOUR understanding)assert for test verifications - These generate code that goes in the test filesession_extend if neededjavascript// Development workflow example // Note: Screenshots are automatically captured before/after each command! it("should incrementally build test", async (context) => { const testdriver = TestDriver(context); await testdriver.provision.chrome({ url: "https://example.com" }); // Automatic screenshot: 001-provision-after-L3-chrome.png // Step 1: Find and inspect const element = await testdriver.find("Some button"); console.log("Element found:", element.found()); console.log("Coordinates:", element.x, element.y); console.log("Confidence:", element.confidence); // Automatic screenshot: 002-find-after-L7-some-button.png // Step 2: Interact await element.click(); // Automatic screenshot: 003-click-after-L13-element.png // Step 3: Assert const result = await testdriver.assert("Something happened"); console.log("Assertion result:", result); expect(result).toBeTruthy(); // Automatic screenshot: 004-assert-after-L17-something-happened.png // Then add more steps... });
javascriptconst testdriver = TestDriver(context, { newSandbox: true, // Create new sandbox (default: true) preview: "browser", // "browser" | "ide" | "none" (default: "browser") reconnect: false, // Reconnect to last sandbox (default: false) keepAlive: 30000, // Keep sandbox alive after test (default: 30000ms / 30 seconds) os: "linux", // 'linux' | 'windows' (default: 'linux') resolution: "1366x768", // Sandbox resolution cache: true, // Enable element caching (default: true) cacheKey: "my-test", // Cache key for element finding autoScreenshots: true, // Capture screenshots before/after each command (default: true) });
| Value | Description | |-------|-------------| | "browser" | Opens debugger in default browser (default) | | "ide" | Opens preview in IDE panel (VSCode, Cursor - requires TestDriver extension) | | "none" | Headless mode, no visual preview |
javascriptawait testdriver.find("Email input").click(); await testdriver.type("user@example.com");
javascriptawait testdriver.pressKeys(["ctrl", "a"]); // Select all await testdriver.pressKeys(["ctrl", "c"]); // Copy await testdriver.pressKeys(["enter"]); // Submit
javascript// Use timeout option to poll until element is found (retries every 5 seconds) const element = await testdriver.find("Loading complete indicator", { timeout: 30000, }); await element.click();
⚠️ Important: Ensure proper focus before scrolling
Scrolling requires the page or frame to be focused, not an input field or other interactive element. If an input is focused, scroll commands may not work as expected.
javascript// If you've been typing in an input, click elsewhere first await testdriver.find("page background").click(); // Or press Escape to unfocus await testdriver.pressKeys(["escape"]); // Now scroll await testdriver.scroll("down"); // If scroll is not working, try using Page Down key directly await testdriver.pressKeys(["pagedown"]);
javascript// Shell (Linux) const output = await testdriver.exec("sh", "ls -la", 5000); // PowerShell (Windows) const date = await testdriver.exec("pwsh", "Get-Date", 5000);
Screenshots are captured automatically before and after each SDK command (click, type, find, assert, etc.). Each screenshot filename includes:
click, find, assert)before or after)Example filenames:
001-provision-after-L8-chrome.png002-find-before-L12-login-button.png003-click-after-L12-element.pngScreenshots are saved to .testdriver/screenshots/<test-file>/.
To disable automatic screenshots:
javascriptconst testdriver = TestDriver(context, { autoScreenshots: false });
For manual screenshots (e.g., with mouse cursor visible):
javascriptawait testdriver.screenshot(1, false, true);
vitest run <testFile> (NEVER use npx vitest). Analyze the output and iterate until the test passes.TESTDRIVER_RUN_URL=https://console.testdriver.ai/runs/... in the output and share it with the user. This is CRITICAL - users need to view the recording to understand what happened.001-click-before-L42-submit-button.png) making it easy to trace issues.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.wait() for simple delays - Use await testdriver.wait(ms) when you need a pause (e.g., after actions, for animations). For waiting for specific elements, prefer find() with a timeout option.sdk.d.ts for method signatures and types when debugging generated testsnode_modules/testdriverai/test for working examplescheck to understand screen state - This is how you verify what the sandbox shows during MCP development.check after actions, assert for test files - check gives detailed AI analysis (no code), assert gives boolean pass/fail (generates code)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:json // eslint.config.js (for TypeScript projects) { "rules": { "@typescript-eslint/no-floating-promises": "error" } }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→pass | 8,432 | 4,973 | -41% | 1 | 1 | 0% | 1,359 | 7,147 | +426% | 0 | 0 | — |
case-01 | fail→fail | 13,410 | 11,868 | -11% | 1 | 1 | 0% | 2,717 | 6,902 | +154% | 0 | 0 | — |
case-02 | fail→fail | 9,397 | 6,491 | -31% | 1 | 1 | 0% | 229 | 6,675 | +2815% | 0 | 0 | — |
case-03 | fail→fail | 4,614 | 5,077 | +10% | 1 | 1 | 0% | 603 | 6,673 | +1007% | 0 | 0 | — |
case-04 | fail→pass | 14,272 | 3,338 | -77% | 1 | 1 | 0% | 2,292 | 6,922 | +202% | 0 | 0 | — |
case-05 | fail→pass | 7,238 | 2,873 | -60% | 1 | 1 | 0% | 1,274 | 6,847 | +437% | 0 | 0 | — |
case-06 | fail→pass | 9,600 | 3,952 | -59% | 1 | 1 | 0% | 1,559 | 6,992 | +348% | 0 | 0 | — |
case-07 | fail→pass | 7,746 | 1,586 | -80% | 1 | 1 | 0% | 1,348 | 6,593 | +389% | 0 | 0 | — |
case-08 | pass→pass | 11,148 | 5,046 | -55% | 1 | 1 | 0% | 1,796 | 7,147 | +298% | 0 | 0 | — |
case-09 | pass→pass | 5,695 | 2,929 | -49% | 1 | 1 | 0% | 964 | 6,885 | +614% | 0 | 0 | — |
case-11 | pass→pass | 11,745 | 2,331 | -80% | 1 | 1 | 0% | 1,818 | 6,702 | +269% | 0 | 0 | — |
case-12 | fail→pass | 10,658 | 3,112 | -71% | 1 | 1 | 0% | 1,727 | 6,955 | +303% | 0 | 0 | — |
case-13 | fail→pass | 8,825 | 2,713 | -69% | 1 | 1 | 0% | 1,578 | 6,844 | +334% | 0 | 0 | — |
case-14 | pass→pass | 9,738 | 4,845 | -50% | 1 | 1 | 0% | 1,812 | 7,191 | +297% | 0 | 0 | — |
case-15 | fail→pass | 10,115 | 3,359 | -67% | 1 | 1 | 0% | 1,826 | 6,941 | +280% | 0 | 0 | — |
case-16 | fail→pass | 5,435 | 2,769 | -49% | 1 | 1 | 0% | 1,008 | 6,871 | +582% | 0 | 0 | — |
case-17 | fail→pass | 10,344 | 3,081 | -70% | 1 | 1 | 0% | 1,745 | 6,914 | +296% | 0 | 0 | — |
case-18 | pass→pass | 9,266 | 4,544 | -51% | 1 | 1 | 0% | 1,277 | 6,990 | +447% | 0 | 0 | — |
case-19 | fail→pass | 7,293 | 1,430 | -80% | 1 | 1 | 0% | 1,237 | 6,560 | +430% | 0 | 0 | — |
case-20 | pass→pass | 15,150 | 11,678 | -23% | 1 | 1 | 0% | 2,317 | 8,640 | +273% | 0 | 0 | — |
case-21 | pass→pass | 12,340 | 5,894 | -52% | 1 | 1 | 0% | 2,352 | 7,411 | +215% | 0 | 0 | — |
case-22 | pass→pass | 8,442 | 10,077 | +19% | 1 | 1 | 0% | 1,526 | 7,849 | +414% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 19 counted toward the lift figure. The other 3 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +50 percentage points is the difference between those two pass rates over the 19 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.