# Changelog
Source: https://docs.testdriver.ai/changelog
Product updates and announcements
π Canary release
This release promotes changes from `v7.9.4-test` through `v7.9.51-test` to canary, rolling up all updates since `v7.9.3`.
β¨ New features
* (Internal) **Autoscaling**
* (Internal) **End-to-end distributed tracing**
π§ Improvements
* **Faster image matching**
* **Storage and infra hardening**
* **Sentry environment consistency**
* **Chrome extension interaction reliability**
* **Docs and onboarding refresh** β Updated v7 docs, SDK examples, and VS Code onboarding/init flows.
π Bug fixes
* **Windows AMI build stability** β Fixed `chrome.exe` startup/build failures.
* **Realtime cleanup reliability** β Fixed connection cleanup leaks in session shutdown paths.
* **Environment and deployment fixes**
Releases test.11 through test.22 include version bumps and internal CI improvements across all packages. No new user-facing changes β see [v7.8.0-test.10](#v780-test10) for the latest updates in this release stream.
π§ Improvements
* **Parallel test concurrency** β Concurrency limits are now enforced with atomic slot tracking, eliminating a race condition where multiple tests launching at the same time could all bypass the limit. If you hit your concurrency cap, the SDK waits and retries automatically.
* **Dashboard access for all plans** β Test recordings and metrics on the [dashboard](https://console.testdriver.ai) are now visible to all users, including those on the Free plan.
* **Sandbox message throttling** β High-frequency interactions no longer risk hitting connection limits. The SDK automatically paces outgoing messages during fast command sequences.
* **Windows Dashcam stability** β Web log tracking on Windows sandboxes now handles errors gracefully instead of failing the test.
π§ Improvements
* **Stable release promotion** β Fixed an issue where promoting a test or canary release to stable could be incorrectly skipped. Stable releases now proceed reliably regardless of the prior pre-release channel.
* **Example docs sync** β Example documentation now updates more reliably, continuing even when individual example tests fail so that passing examples still get refreshed.
π§ Improvements
* **VS Code environment switching** β The TestDriver VS Code extension now includes a status bar indicator and a new **TestDriver: Switch Environment** command. You can switch between stable, canary, and test environments directly from VS Code, and your MCP server configuration updates automatically.
* **Plan rename: Starter β Pro** β The "Starter" plan has been renamed to **Pro**. No changes to pricing or features β just a clearer name. The "Self-Hosted" tier is now labeled **Enterprise**.
* **SDK network resilience** β API requests now automatically retry on transient server errors (500, 502, 503, 504) with exponential backoff, reducing flaky test failures caused by brief infrastructure hiccups.
* **Pricing page refresh** β The [hosted plans page](/v7/hosted) now shows plan cards with included minutes, concurrency limits, and pricing at a glance.
π Docs updates
* **Quickstart redesign** β The [quickstart](/v7/quickstart) now has tabbed setup paths for CLI, GitHub Copilot, and manual installation.
* **Deployment section** β "Cloud" is now [Hosted](/v7/hosted) and the previous Enterprise page has been consolidated into [Self-Hosted](/v7/self-hosted), which covers both standard and air-gapped deployments.
This release includes all changes from v7.8.0-test.6 with version bumps across all packages. No additional user-facing changes.
π§ Improvements
* **Sandbox spawning reliability** β Sandbox creation now uses a 3-minute timeout to handle slow infrastructure, then immediately switches to a 60-second orphan timeout for fast cleanup. This reduces sandbox creation failures during high-traffic periods.
* **Command deadline timeouts** β Sandbox commands now enforce a hard deadline that terminates execution if the connection drops mid-command, preventing tests from hanging indefinitely.
* **Real-time channel cleanup** β Fixed a compatibility issue with the real-time messaging layer that could cause channel cleanup to fail during shutdown.
* **Dashboard timestamp tooltips** β Extended UTC date tooltips to the test history sidebar. Hovering over any relative timestamp (e.g., "5 minutes ago") across the [dashboard](https://console.testdriver.ai) now shows the full UTC date and time.
β¨ New features
* **[GitHub Copilot integration](/v7/copilot/running-tests)** β Use TestDriver directly from GitHub Copilot in VS Code. The new MCP server lets Copilot launch sandboxes, interact with elements, and run assertions through natural language. Includes guides for [creating tests](/v7/generating-tests), [running tests](/v7/copilot/running-tests), [GitHub Actions integration](/v7/copilot/auto-healing#use-testdriver-in-github), and [auto-healing](/v7/copilot/auto-healing).
π§ Improvements
* **Sandbox reliability** β Linux sandboxes now use a longer creation timeout to handle slow infrastructure, then immediately switch to a short orphan timeout for fast cleanup of disconnected sessions.
* **Command execution timeouts** β Sandbox commands now have deadline timeouts that prevent hangs if the connection drops mid-execution.
* **Real-time connection stability** β Updated the real-time messaging layer for better compatibility and more reliable channel cleanup during shutdown.
* **Dashboard date display** β Hovering over relative timestamps (e.g., "5 minutes ago") now shows the full UTC date and time.
This release includes all changes from v7.8.0-canary.4 with version bumps.
β¨ New features
* **[Cache API](/v7/cache)** β Speed up repeated test runs with screenshot-based caching. The system compares screenshots to cached results and reuses element positions when the screen hasn't changed, reducing AI calls.
* **[Custom error classes](/v7/errors)** β New `ElementNotFoundError` class with rich debugging info including screenshot paths, pixel diffs, and detailed messages.
* **[Events system](/v7/events)** β Listen to SDK lifecycle events with wildcard support via `testdriver.emitter`. Uses colon-delimited namespaces (e.g., `command:start`, `log:*`).
* **[Provision API](/v7/provision)** β Launch browsers, desktop apps, VS Code, and Chrome extensions in your sandbox before tests run. Access via `testdriver.provision.*`.
* **[Redraw detection](/v7/redraw)** β Wait for screens to stabilize after interactions using two-phase detection with pixel comparison and z-score analysis.
* **[Screenshots API](/v7/screenshots)** β Capture screenshots manually with `testdriver.screenshot()` or automatically before/after every command.
π New examples
* [Exec output](/v7/examples/exec-output) β Capture and use output from PowerShell commands
* [Exec PowerShell](/v7/examples/exec-pwsh) β Generate dynamic data with PowerShell
* [Focus window](/v7/examples/focus-window) β Switch focus between application windows
π§ Improvements
* Improved console URL mapping for canary and test environments
* Enhanced Ably channel cleanup for better resource management
* Updated release workflow with improved changelog generation
π Stable release
This release promotes v7.5.25 to stable with deployment channel improvements.
β¨ Features
* **Deployment channels** β SDK now supports stable and canary release channels. Stable releases use the `latest` npm tag, while canary releases use the `canary` tag. Install canary with `npm install testdriverai@canary`.
π§ Improvements
* Improved cache hit debugging and logging
* Enhanced element location reliability
* Better error handling during test execution
π Bug fixes
* Fixed restart behavior during auto-updates
* Fixed console URL routing for various environments
This release includes changes from v7.5.17 through v7.5.25.
π Bug fixes
* Fixed restart update behavior
π§ Improvements
* Disabled auto-update restart to improve CI stability
π§ Improvements
* Added markdown-to-HTML rendering for marketing content
π§ Improvements
* Enhanced debugging output for cache hits
* Improved element location reliability
π§ Improvements
* Improved codespace scaling and development environment setup
* Updated VS Code extension version
# assert()
Source: https://docs.testdriver.ai/v7/assert
Make AI-powered assertions about screen state
## Overview
Make AI-powered assertions about the current screen state using natural language. The AI analyzes the screen and verifies that your assertion is true.
## Syntax
```javascript theme={null}
await testdriver.assert(assertion)
await testdriver.assert(assertion, options)
```
## Parameters
Natural language description of what should be true
Optional configuration
AI sampling configuration for this assert call (overrides global `ai` config from constructor).
Controls randomness. `0` = deterministic, higher = more creative. Default: model default.
Sampling parameters
Top-P (nucleus sampling). Range: 0-1.
Top-K sampling. `1` = most deterministic.
## Returns
`Promise` - `true` if assertion passes, throws error if assertion fails
## Examples
### Basic Assertions
```javascript theme={null}
// Verify page elements
await testdriver.assert('the login page is displayed');
await testdriver.assert('submit button is visible');
await testdriver.assert('error message is shown');
// Verify text content
await testdriver.assert('the page title is "Welcome"');
await testdriver.assert('username field contains "john.doe"');
await testdriver.assert('success message says "Account created"');
// Verify states
await testdriver.assert('the form is empty');
await testdriver.assert('the checkbox is checked');
await testdriver.assert('the dropdown shows "United States"');
// Verify visual appearance
await testdriver.assert('the button is blue');
await testdriver.assert('the loading spinner is displayed');
await testdriver.assert('the modal dialog is open');
```
## Best Practices
**Be specific in assertions**
More specific assertions are more reliable:
```javascript theme={null}
// β Too vague
await testdriver.assert('button is visible');
// β Specific
await testdriver.assert('blue submit button is visible below the form');
```
**Assert state changes**
Verify state before and after actions:
```javascript theme={null}
// Before
await testdriver.assert('cart is empty');
// Action
const addBtn = await testdriver.find('add to cart');
await addBtn.click();
// After
await testdriver.assert('cart contains 1 item');
```
**Use with test framework assertions**
Combine AI assertions with traditional test assertions:
```javascript theme={null}
// AI assertion
const result = await testdriver.assert('success message is displayed');
// Framework assertion
expect(result).toBeTruthy();
// Extract for detailed comparison
const message = await testdriver.extract('the success message text');
expect(message).toContain('successfully');
```
## Polling Assertions
For conditions that may take time to become true:
```javascript theme={null}
async function waitForAssertion(testdriver, assertion, timeout = 30000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
await testdriver.assert(assertion);
return true; // Assertion passed
} catch (error) {
// Assertion failed, wait and retry
await new Promise(r => setTimeout(r, 1000));
}
}
throw new Error(`Assertion timeout: "${assertion}"`);
}
// Usage
await waitForAssertion(testdriver, 'page has finished loading', 30000);
await waitForAssertion(testdriver, 'results are displayed', 10000);
```
## Use Cases
```javascript theme={null}
// Try to submit empty form
const submitBtn = await testdriver.find('submit button');
await submitBtn.click();
// Verify validation errors
await testdriver.assert('email field shows "required" error');
await testdriver.assert('password field shows "required" error');
// Verify form not submitted
await testdriver.assert('still on the form page');
```
```javascript theme={null}
const loginBtn = await testdriver.find('login button');
await loginBtn.click();
// Verify navigation
await testdriver.assert('user dashboard is displayed');
await testdriver.assert('welcome message shows user name');
await testdriver.assert('logout button is visible');
```
```javascript theme={null}
const loadBtn = await testdriver.find('load more button');
await loadBtn.click();
// Poll for content using helper
await waitForAssertion(testdriver, 'more than 10 items are shown', 10000);
await testdriver.assert('load more button is still visible');
```
```javascript theme={null}
// Verify hover effect
const button = await testdriver.find('primary button');
await button.hover();
await testdriver.assert('button background is darker');
// Verify button is enabled
await testdriver.assert('submit button is enabled');
```
```javascript theme={null}
// Step 1
await testdriver.assert('step 1 is active');
const nextBtn = await testdriver.find('next button');
await nextBtn.click();
// Step 2
await testdriver.assert('step 2 is active');
await testdriver.assert('step 1 is completed');
await nextBtn.click();
// Step 3
await testdriver.assert('step 3 is active');
await testdriver.assert('step 2 is completed');
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it, expect } from 'vitest';
import TestDriver from 'testdriverai';
describe('Assertions', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should validate login flow', async () => {
await testdriver.focusApplication('Google Chrome');
// Verify initial state
await testdriver.assert('the login page is displayed');
await testdriver.assert('username field is empty');
await testdriver.assert('password field is empty');
// Fill form
const usernameField = await testdriver.find('username input');
await usernameField.click();
await testdriver.type('testuser');
await testdriver.pressKeys(['tab']);
await testdriver.type('password123');
// Verify fields filled
await testdriver.assert('username field contains "testuser"');
await testdriver.assert('password field is not empty');
// Submit
await testdriver.pressKeys(['enter']);
// Poll for success
await waitForAssertion(testdriver, 'user dashboard is displayed', 10000);
// Verify logged in state
await testdriver.assert('welcome message is shown');
await testdriver.assert('logout button is visible');
// Verify login page is gone
await testdriver.assert('login form is displayed', false, true);
});
it('should show validation errors', async () => {
// Try empty submission
const submitBtn = await testdriver.find('submit button');
await submitBtn.click();
// Verify multiple errors
const errors = [
'email field shows error',
'name field shows error',
'password field shows error'
];
for (const error of errors) {
const result = await testdriver.assert(error);
expect(result).toBeTruthy();
}
// Verify not submitted
await testdriver.assert('confirmation page is shown', false, true);
});
});
```
## Related Methods
* [`extract()`](/v7/extract) - Extract information for detailed assertions
* [`find()`](/v7/find) - Locate elements to verify
# AWS Setup Guide
Source: https://docs.testdriver.ai/v7/aws-setup
Deploy TestDriver on your AWS infrastructure using CloudFormation
This guide walks you through setting up self-hosted TestDriver instances on AWS. By the end, you'll have fully automated test infrastructure that spawns and terminates instances on-demand.
```mermaid theme={null}
graph LR
A[Vitest Test] --> B[setup-aws hook]
B --> C[Spawns EC2]
C --> D[Runs Test]
D --> E[Terminates EC2]
```
## How It Works
TestDriver automatically manages AWS EC2 instances for your tests:
1. **Deploy CloudFormation** β One-time infrastructure setup
2. **Configure Vitest** β Add one line to your config
3. **Run Tests** β Instances spawn automatically, run tests, and terminate
That's it! No manual instance management needed.
# Quickstart
One-click AWS setup
```javascript vitest.config.mjs theme={null}
setupFiles: ['testdriverai/vitest/setup', 'testdriverai/vitest/setup-aws']
```
```bash theme={null}
TD_OS=windows AWS_REGION=us-east-2 \
AWS_LAUNCH_TEMPLATE_ID=lt-xxx AMI_ID=ami-xxx \
vitest run
```
## Overview
The setup process is simple:
1. **Deploy CloudFormation** β Creates VPC, security groups, IAM roles, and launch templates
2. **Configure Vitest** β Add `setup-aws` to automatically manage instance lifecycle
3. **Run Tests** β Set `TD_OS=windows` with AWS credentials and instances spawn/terminate automatically
## Prerequisites
Before you begin, ensure you have:
* AWS account with CloudFormation permissions
* [AWS CLI](https://aws.amazon.com/cli/) installed and configured (`aws configure`)
* Access to the TestDriver AMI β [Contact us](http://testdriver.ai/demo) with your AWS region
* A GitHub repository for your tests
The TestDriver Golden Image AMI ID is `ami-0504bf50fad62f312`. Contact us to get access in your preferred AWS region.
## Step 1: Deploy CloudFormation Stack
Our CloudFormation template creates all the AWS infrastructure you need:
* Dedicated VPC with public subnet
* Security group with required port access
* IAM roles and instance profiles
* EC2 launch template for instance creation
Click the button below to launch the CloudFormation stack in your AWS Console:
Deploy TestDriver infrastructure with one click
Configure the stack parameters:
* **Stack name**: `testdriver-infrastructure` (or your preferred name)
* **ProjectTag**: `testdriver`
* **AllowedIngressCidr**: Your IP range (e.g., `203.0.113.0/24`)
* **InstanceType**: `c5.xlarge` (recommended)
* **CreateKeyPair**: `true`
**Security**: Replace `AllowedIngressCidr` with your specific IP ranges to restrict VPC access. Avoid using `0.0.0.0/0` in production.
### Get Your Launch Template ID
After the stack creation completes, navigate to the **Outputs** tab to find your `LaunchTemplateId`:
**Save this ID** β you'll need it for spawning instances and CI configuration.
Download the template from the [TestDriver CLI repository](https://github.com/testdriverai/testdriverai/blob/main/setup/aws/cloudformation.yaml), then deploy:
```bash theme={null}
aws cloudformation deploy \
--template-file setup/aws/cloudformation.yaml \
--stack-name testdriver-infrastructure \
--parameter-overrides \
ProjectTag=testdriver \
AllowedIngressCidr=0.0.0.0/0 \
InstanceType=c5.xlarge \
CreateKeyPair=true \
--capabilities CAPABILITY_IAM
```
**Security**: Replace `AllowedIngressCidr=0.0.0.0/0` with your specific IP ranges to restrict VPC access.
### Get Your Launch Template ID
After deployment completes, retrieve the launch template ID:
```bash theme={null}
aws cloudformation describe-stacks \
--stack-name testdriver-infrastructure \
--query 'Stacks[0].Outputs[?OutputKey==`LaunchTemplateId`].OutputValue' \
--output text
```
**Save this ID** β you'll need it for spawning instances and CI configuration.
## Step 2: Configure Vitest
Add the AWS setup hook to your `vitest.config.mjs`:
```javascript vitest.config.mjs theme={null}
import { defineConfig } from 'vitest/config';
import { config } from 'dotenv';
import TestDriver from 'testdriverai/vitest';
config(); // Load .env file
export default defineConfig({
test: {
testTimeout: 900000,
hookTimeout: 900000,
maxConcurrency: 3,
reporters: [
'default',
TestDriver(),
['junit', { outputFile: 'test-report.junit.xml' }]
],
setupFiles: ['testdriverai/vitest/setup', 'testdriverai/vitest/setup-aws'],
},
});
```
**That's it!** The `setup-aws` hook automatically spawns and terminates instances when `TD_OS=windows` is set. No manual instance management needed.
## Step 3: Write Your Tests
Tests should use `context.ip || process.env.TD_IP` for the IP configuration:
```javascript theme={null}
import { describe, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("My Test", () => {
it("should run on self-hosted instance", async (context) => {
const testdriver = TestDriver(context, {
ip: context.ip || process.env.TD_IP,
});
await testdriver.provision.chrome({ url: "https://example.com" });
// ... your test steps
});
});
```
**How it works**: When `TD_OS=windows` with AWS credentials, `context.ip` is automatically set by the setup hook. When running without AWS setup (cloud-hosted), both are undefined and TestDriver uses the cloud. When `TD_IP` is provided manually, it takes precedence.
## Step 4: Run Tests
### Locally
```bash theme={null}
TD_OS=windows \
AWS_REGION=us-east-2 \
AWS_LAUNCH_TEMPLATE_ID=lt-xxx \
AMI_ID=ami-0504bf50fad62f312 \
vitest run
```
Each test gets its own fresh EC2 instance that's automatically terminated after completion.
### In GitHub Actions
Automate testing with self-hosted instances in your CI/CD pipeline. TestDriver automatically spawns a fresh instance for each test, runs the test, and terminates the instance.
```yaml .github/workflows/test.yml theme={null}
name: TestDriver Self-Hosted Windows Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Windows tests with self-hosted instances
run: npx vitest run examples/*.test.mjs
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
TD_OS: windows
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-2
AWS_LAUNCH_TEMPLATE_ID: ${{ secrets.AWS_LAUNCH_TEMPLATE_ID }}
AMI_ID: ${{ secrets.AMI_ID }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-report.junit.xml
```
**Automatic Instance Management**: Setting `TD_OS=windows` with AWS credentials enables automatic instance spawning. Each test gets its own fresh instance that's terminated after the test completes.
### Required GitHub Secrets
| Secret | Description | Example |
| ------------------------ | -------------------------- | ----------------------------------------------------------- |
| `AWS_ACCESS_KEY_ID` | AWS access key | `AKIAIOSFODNN7EXAMPLE` |
| `AWS_SECRET_ACCESS_KEY` | AWS secret key | `wJalrXUtnFEMI/K7MDENG...` |
| `AWS_REGION` | AWS region | `us-east-2` |
| `AWS_LAUNCH_TEMPLATE_ID` | From CloudFormation output | `lt-07c53ce8349b958d1` |
| `AMI_ID` | TestDriver AMI ID | `ami-0504bf50fad62f312` |
| `TD_API_KEY` | Your TestDriver API key | From [console.testdriver.ai](https://console.testdriver.ai) |
Add these as **GitHub Repository Secrets** under Settings β Secrets and variables β Actions
### Example Workflows
For complete production examples, see:
* **[Scheduled Tests](https://github.com/testdriverai/testdriverai/blob/main/.github/workflows/acceptance-windows-scheduled.yaml)** β Daily automated test runs
* **[Reusable Workflow](https://github.com/testdriverai/testdriverai/blob/main/.github/workflows/windows-self-hosted.yaml)** β Shared configuration for PR and scheduled tests
## Advanced Usage
### Using an Existing Instance
If you already have a running instance, you can skip automatic spawning by providing `TD_IP`:
```bash theme={null}
TD_OS=windows TD_IP=1.2.3.4 vitest run
```
The `setup-aws` hook will detect `TD_IP` is already set and skip spawning a new instance.
### Manual Instance Management
For advanced use cases, you can manually spawn instances using the [`spawn-runner.sh`](https://github.com/testdriverai/testdriverai/blob/main/setup/aws/spawn-runner.sh) script:
```bash theme={null}
AWS_REGION=us-east-2 \
AMI_ID=ami-0504bf50fad62f312 \
AWS_LAUNCH_TEMPLATE_ID=lt-xxx \
bash setup/aws/spawn-runner.sh
```
Output:
```
PUBLIC_IP=1.2.3.4
INSTANCE_ID=i-1234567890abcdef0
AWS_REGION=us-east-2
```
Then manually terminate when done:
```bash theme={null}
aws ec2 terminate-instances \
--instance-ids i-1234567890abcdef0 \
--region us-east-2
```
For complete production examples, see:
* **[Scheduled Tests](https://github.com/testdriverai/testdriverai/blob/main/.github/workflows/acceptance-windows-scheduled.yaml)** β Daily automated test runs
* **[Reusable Workflow](https://github.com/testdriverai/testdriverai/blob/main/.github/workflows/windows-self-hosted.yaml)** β Shared configuration for PR and scheduled tests
### Connecting to an Instance
You can connect to running instances via:
* **RDP** β Use the public IP on port 3389
* **VNC** β Access via web browser at `http://:5900`
* **AWS Console** β Use EC2 Instance Connect or Session Manager
Stopped instances retain their EBS volumes and can be restarted later. Terminated instances are permanently deleted. Always terminate instances when done to avoid storage costs.
## AMI Customization
The TestDriver Golden Image comes pre-configured with:
* Windows Server with desktop environment
* VNC + web server for remote access
* Python, Node.js, Git
* TestDriver agent and dependencies
### Creating a Custom AMI
You can customize the AMI to include additional software or configurations:
Use the default credentials:
* **Username**: `testdriver`
* **Password**: `wwv9uJ0sqlulbN3`
**Critical**: Run the password rotation script immediately:
```powershell theme={null}
C:\testdriver\RotateLocalPasswords.ps1
```
Save the new password securely.
Install any additional dependencies, configure settings, or modify the environment as needed.
Use the AWS console or CLI to create an AMI from your modified instance. Update your workflow to use the new AMI ID.
**Security**: Never use the default password in production. Always rotate passwords before creating custom AMIs.
## Security Best Practices
### Network Security
* **Restrict CIDR blocks** β Only allow access from known IP ranges
* **Use VPC endpoints** β For private AWS service communication
* **Enable VPC Flow Logs** β For network monitoring
### AWS Authentication
Use OIDC instead of long-term credentials for GitHub Actions:
```yaml theme={null}
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-2
```
See [GitHub's OIDC documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) for setup instructions.
### Instance Security
* **Terminate immediately** after tests complete
* **Monitor costs** with AWS billing alerts
* **Use least-privilege IAM roles**
* **Enable CloudTrail** for audit logging
## Troubleshooting
### Instance Not Responding
* **Check security groups** β Verify required ports are open (RDP 3389, VNC 5900, TestDriver ports)
* **Verify status checks** β Ensure instance has passed AWS status checks
* **Check AMI compatibility** β Some instance types don't support certain AMIs
### Connection Timeouts
* Verify network connectivity from CI runner to instance
* Check VPC routing and internet gateway configuration
* Confirm instance is in the correct subnet
### AWS CLI Errors
* Validate credentials and permissions
* Check service quotas and limits
* Verify region consistency across all resources
# Learn
Source: https://docs.testdriver.ai/v7/caching
How TestDriver learns your app and caches what it discovers for instant, deterministic replays
Once the agent has [explored your app](/v7/generating-tests), TestDriver remembers what it found. Every element the AI vision agent discovers is cached with a vision fingerprintβa perceptual hash of the screen state where it was located. On the next run, TestDriver matches against that cache instead of calling the AI again. Passing tests replay instantly, deterministically, and cheaply.
This learning is what makes TestDriver fast. Intelligent caching delivers up to **1.7x faster** test execution by skipping redundant AI vision analysisβthe agent only thinks when it sees something new.
```javascript theme={null}
// First run: builds cache
await testdriver.find('submit button');
// Second run: exact match
await testdriver.find('submit button');
```
## Automatic Caching
Learning is enabled automatically with zero configuration. The cache keyβthe fingerprint TestDriver uses to recognize what it already knowsβis computed from:
* **File hash**: SHA-256 hash of the test file contents
* **Selector prompt**: The exact text description passed to `find()`
* **Screenshot context**: Perceptual hash of the current screen state
* **Platform**: Operating system and browser version
When you modify your test file, the hash changes automatically, invalidating stale cache entries and ensuring fresh AI analysis with your updated test logic.
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from 'testdriverai/presets';
test('auto-cached test', async (context) => {
const { testdriver } = await chrome(context, {
url: 'https://example.com'
});
// First call: AI analyzes screen, saves to cache
await testdriver.find('More information link'); // 2.1s
// Second call: cache hit, instant response
await testdriver.find('More information link'); // 12ms β‘
});
```
## Managing the Cache
You can clear the cache within the TestDriver console. There, you'll also find previews of cached elements, the input prompts, as well as analytics on cache hit rates.
Manage and clear your test cache from the TestDriver console.
## Debugging Cache Hits and Misses
You can track what TestDriver has learned by inspecting cache performance in your tests:
```javascript theme={null}
test('monitor cache performance', async (context) => {
const { testdriver } = await chrome(context, { url });
const element = await testdriver.find('submit button');
if (element.cacheHit) {
console.log('β Cache hit - instant response');
console.log('Strategy:', element.cacheStrategy); // 'exact', 'pixeldiff', or 'template'
console.log('Similarity:', `${(element.similarity * 100).toFixed(1)}%`);
console.log('Cache age:', element.cacheCreatedAt);
} else {
console.log('β±οΈ Cache miss - AI analysis performed');
console.log('New cache entry created');
}
});
```
## Configuring the Cache
You can configure how TestDriver learns globally when initializing TestDriver:
```javascript theme={null}
import { TestDriver } from 'testdriverai';
const testdriver = new TestDriver({
apiKey: process.env.TD_API_KEY,
cacheKey: 'my-test-suite', // cache-key for this instance
cacheDefaults: {
threshold: 0.05, // 95% similarity
}
});
```
It's also possible to override cache settings per `find()` call:
```javascript theme={null}
// Default: 95% similarity required
await testdriver.find('submit button');
// Explicit strict threshold
await testdriver.find('submit button', {
cacheThreshold: 0.01 // 99% similarity
});
```
## Caching with Variables
Custom cache keys prevent cache pollution when using variables in prompts, dramatically improving cache hit ratesβso TestDriver reuses what it learned even when your data changes.
```javascript theme={null}
// β Without cache key - creates new cache for each variable value
const email = 'user@example.com';
await testdriver.find(`input for ${email}`); // Cache miss every time
// β With cache key - reuses cache regardless of variable
const email = 'user@example.com';
await testdriver.find(`input for ${email}`, {
cacheKey: 'email-input'
});
// Also useful for dynamic IDs, names, or other changing data
const orderId = generateOrderId();
await testdriver.find(`order ${orderId} status`, {
cacheKey: 'order-status' // Same cache for all orders
});
```
## Next
Now that TestDriver has learned your app, run your tests in CI and locallyβreplaying the cache for fast, deterministic results.
# captcha()
Source: https://docs.testdriver.ai/v7/captcha
Solve captchas using 2captcha service
# Captcha Solving
TestDriver can automatically solve captchas during your tests using the 2captcha service. This feature works on both **Linux** and **Windows** sandboxes.
## Quick Start
```javascript theme={null}
const result = await testdriver.captcha({
apiKey: process.env.TWOCAPTCHA_API_KEY,
});
console.log(result.success); // true
console.log(result.token); // The solved captcha token
```
That's it! TestDriver will automatically:
* Detect the captcha type on the page
* Extract the sitekey
* Solve the captcha via 2captcha
* Inject the token into the page
* Trigger any callbacks
## Supported Captcha Types
| Type | Auto-Detected | Notes |
| -------------------- | ------------- | ----------------------- |
| reCAPTCHA v2 | β | Including invisible |
| reCAPTCHA v3 | β | Action is auto-detected |
| hCaptcha | β | |
| Cloudflare Turnstile | β | |
## Getting a 2captcha API Key
1. Sign up at [2captcha.com](https://2captcha.com)
2. Add funds to your account
3. Copy your API key from the dashboard
## Configuration Options
```javascript theme={null}
const result = await testdriver.captcha({
// Required
apiKey: '2CAPTCHA_API_KEY',
// Optional - usually auto-detected
sitekey: '6Le...', // Override auto-detected sitekey
type: 'recaptcha_v3', // Override auto-detected type
action: 'submit', // reCAPTCHA v3 action
// Timing
timeout: 120000, // Max wait time (default: 120s)
pollInterval: 5000, // Poll interval (default: 5s)
});
```
## Full Example
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriver";
describe("Checkout flow", () => {
it("should complete checkout with captcha", async (context) => {
const testdriver = TestDriver(context);
// Navigate to checkout page
await testdriver.provision.chrome({
url: 'https://example.com/checkout',
});
// Fill out form
await testdriver.type({ text: 'John Doe', selector: '#name' });
await testdriver.type({ text: 'john@example.com', selector: '#email' });
// Solve the captcha
const result = await testdriver.captcha({
apiKey: process.env.TWOCAPTCHA_API_KEY,
});
expect(result.success).toBe(true);
// Submit the form
await testdriver.click({ selector: '#submit' });
// Verify success
await testdriver.find({ text: 'Order confirmed' });
}, 180000);
});
```
## Environment Variables
You can set your API key as an environment variable:
```bash theme={null}
export TWOCAPTCHA_API_KEY=your_api_key_here
```
Then use it in your tests:
```javascript theme={null}
const result = await testdriver.captcha({
apiKey: process.env.TWOCAPTCHA_API_KEY,
});
```
### GitHub Actions
Add the key to your repository secrets and expose it in your workflow:
```yaml theme={null}
- name: Run Tests
run: vitest run
env:
TWOCAPTCHA_API_KEY: ${{ secrets.TWOCAPTCHA_API_KEY }}
```
## How It Works
1. **Detection**: Scans the page for captcha elements (`data-sitekey`, script tags, etc.)
2. **Submit**: Sends the captcha challenge to 2captcha's solving service
3. **Poll**: Waits for human solvers to complete the captcha
4. **Inject**: Injects the solved token into the page's hidden fields
5. **Callback**: Triggers any JavaScript callbacks the page expects
## Troubleshooting
### "Could not auto-detect captcha"
The captcha element wasn't found on the page. Try:
* Waiting for the page to fully load before calling `captcha()`
* Providing the `sitekey` and `type` manually
### Timeout errors
Captcha solving typically takes 10-30 seconds. If you're getting timeouts:
* Increase the `timeout` option
* Check your 2captcha balance
* Verify the captcha type is correct
### Token not working
Some sites validate tokens immediately. Make sure:
* The token is injected before form submission
* The captcha type matches what the site expects
* For reCAPTCHA v3, the `action` parameter matches the site's expected action
## Requirements
* Chrome must be launched with remote debugging enabled (automatic on all sandboxes)
* A valid 2captcha API key with sufficient balance
# Integrating into CI/CD
Source: https://docs.testdriver.ai/v7/ci-cd
Run TestDriver tests in CI/CD with parallel execution and cross-platform support
TestDriver integrates seamlessly with popular CI providers, enabling automated end-to-end testing on every push and pull request.
## Authentication
On **GitHub Actions, prefer OIDC** via the published `testdriverai/action` β
there's no `TD_API_KEY` secret to store, copy, or rotate. The action proves the
workflow is running inside your org and TestDriver exchanges that proof for your
team's key at run time. See the GitHub Actions tab below.
For other CI providers (or self-hosted runners without OIDC), fall back to a
stored API key from [console.testdriver.ai/team](https://console.testdriver.ai/team),
added as a `TD_API_KEY` secret in your CI provider's settings.
Never commit your API key directly in code. Always use OIDC or your CI provider's secrets management.
## CI Provider Examples
### Authenticate with OIDC via `testdriverai/action` (recommended)
Use the published [`testdriverai/action`](https://github.com/testdriverai/action) β it mints the OIDC token, exchanges it for your team's API key, and exports `TD_API_KEY` for the steps that follow. **No `TD_API_KEY` secret to store or rotate.**
One-time setup: authorize the [TestDriver GitHub App](https://console.testdriver.ai) for your org so the org β team binding exists. If your org authorized the App before OIDC support shipped, re-authorize once. If the App isn't authorized, the action fails with a console link (or falls back to the `api-key` secret if you provide one).
```yaml .github/workflows/testdriver.yml theme={null}
name: TestDriver Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
permissions:
id-token: write # REQUIRED to mint an OIDC token
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Authenticate to TestDriver
uses: testdriverai/action@stable # pin @stable / @canary / @test to your SDK channel
with:
api-key: ${{ secrets.TD_API_KEY }} # optional fallback if OIDC isn't set up
- name: Run TestDriver tests
run: npx vitest run
```
### Stored-key fallback
Only if you can't use OIDC (e.g. self-hosted runners without an OIDC provider). Add the key as a secret and pass it via `env`:
1. Navigate to your GitHub repository
2. Go to **Settings** β **Secrets and variables** β **Actions**
3. Click **New repository secret**
4. Name: `TD_API_KEY`, Value: your API key
5. Click **Add secret**
### Basic Workflow
Create `.github/workflows/testdriver.yml`:
```yaml .github/workflows/testdriver.yml theme={null}
name: TestDriver Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run TestDriver tests
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
run: vitest --run
```
### Parallel Execution
Use matrix strategy to run tests in parallel:
```yaml .github/workflows/testdriver-parallel.yml theme={null}
name: TestDriver Tests (Parallel)
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run tests (shard ${{ matrix.shard }}/4)
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
run: vitest --run --shard=${{ matrix.shard }}/4
```
### Multi-Platform Testing
```yaml .github/workflows/testdriver-multiplatform.yml theme={null}
name: TestDriver Tests (Multi-Platform)
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
td-os: [linux, windows]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run tests on ${{ matrix.td-os }}
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
TD_OS: ${{ matrix.td-os }}
run: vitest --run
```
### Adding Secrets
1. Go to your GitLab project
2. Navigate to **Settings** β **CI/CD** β **Variables**
3. Click **Add variable**
4. Key: `TD_API_KEY`, Value: your API key
5. Check **Mask variable** and click **Add variable**
### Basic Pipeline
Create `.gitlab-ci.yml`:
```yaml .gitlab-ci.yml theme={null}
stages:
- test
testdriver:
stage: test
image: node:20
cache:
paths:
- node_modules/
script:
- npm ci
- vitest --run
variables:
TD_API_KEY: $TD_API_KEY
```
### Parallel Execution
```yaml .gitlab-ci.yml theme={null}
stages:
- test
.testdriver-base:
stage: test
image: node:20
cache:
paths:
- node_modules/
before_script:
- npm ci
variables:
TD_API_KEY: $TD_API_KEY
testdriver-shard-1:
extends: .testdriver-base
script:
- vitest --run --shard=1/4
testdriver-shard-2:
extends: .testdriver-base
script:
- vitest --run --shard=2/4
testdriver-shard-3:
extends: .testdriver-base
script:
- vitest --run --shard=3/4
testdriver-shard-4:
extends: .testdriver-base
script:
- vitest --run --shard=4/4
```
### Multi-Platform Testing
```yaml .gitlab-ci.yml theme={null}
stages:
- test
.testdriver-base:
stage: test
image: node:20
cache:
paths:
- node_modules/
before_script:
- npm ci
variables:
TD_API_KEY: $TD_API_KEY
testdriver-linux:
extends: .testdriver-base
variables:
TD_OS: linux
script:
- vitest --run
testdriver-windows:
extends: .testdriver-base
variables:
TD_OS: windows
script:
- vitest --run
```
### Adding Secrets
1. Go to your CircleCI project
2. Click **Project Settings** β **Environment Variables**
3. Click **Add Environment Variable**
4. Name: `TD_API_KEY`, Value: your API key
### Basic Config
Create `.circleci/config.yml`:
```yaml .circleci/config.yml theme={null}
version: 2.1
jobs:
test:
docker:
- image: cimg/node:20.0
steps:
- checkout
- restore_cache:
keys:
- npm-deps-{{ checksum "package-lock.json" }}
- run: npm ci
- save_cache:
key: npm-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run:
name: Run TestDriver tests
command: vitest --run
environment:
TD_API_KEY: ${TD_API_KEY}
workflows:
test:
jobs:
- test
```
### Parallel Execution
```yaml .circleci/config.yml theme={null}
version: 2.1
jobs:
test:
docker:
- image: cimg/node:20.0
parallelism: 4
steps:
- checkout
- restore_cache:
keys:
- npm-deps-{{ checksum "package-lock.json" }}
- run: npm ci
- save_cache:
key: npm-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run:
name: Run TestDriver tests
command: |
vitest --run --shard=$((CIRCLE_NODE_INDEX + 1))/$CIRCLE_NODE_TOTAL
environment:
TD_API_KEY: ${TD_API_KEY}
workflows:
test:
jobs:
- test
```
### Multi-Platform Testing
```yaml .circleci/config.yml theme={null}
version: 2.1
jobs:
test:
docker:
- image: cimg/node:20.0
parameters:
td-os:
type: string
steps:
- checkout
- run: npm ci
- run:
name: Run TestDriver tests on << parameters.td-os >>
command: vitest --run
environment:
TD_API_KEY: ${TD_API_KEY}
TD_OS: << parameters.td-os >>
workflows:
test:
jobs:
- test:
td-os: linux
- test:
td-os: windows
```
### Adding Secrets
1. Go to your Azure DevOps project
2. Navigate to **Pipelines** β **Library** β **Variable groups**
3. Create a new variable group or edit existing
4. Add variable: `TD_API_KEY` with your API key
5. Click the lock icon to make it secret
### Basic Pipeline
Create `azure-pipelines.yml`:
```yaml azure-pipelines.yml theme={null}
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
displayName: 'Setup Node.js'
- script: npm ci
displayName: 'Install dependencies'
- script: vitest --run
displayName: 'Run TestDriver tests'
env:
TD_API_KEY: $(TD_API_KEY)
```
### Parallel Execution
```yaml azure-pipelines.yml theme={null}
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
strategy:
matrix:
shard1:
SHARD: '1/4'
shard2:
SHARD: '2/4'
shard3:
SHARD: '3/4'
shard4:
SHARD: '4/4'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: 'Install dependencies'
- script: vitest --run --shard=$(SHARD)
displayName: 'Run TestDriver tests'
env:
TD_API_KEY: $(TD_API_KEY)
```
### Multi-Platform Testing
```yaml azure-pipelines.yml theme={null}
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
strategy:
matrix:
linux:
TD_OS: 'linux'
windows:
TD_OS: 'windows'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: 'Install dependencies'
- script: vitest --run
displayName: 'Run TestDriver tests on $(TD_OS)'
env:
TD_API_KEY: $(TD_API_KEY)
TD_OS: $(TD_OS)
```
### Adding Secrets
1. Go to **Manage Jenkins** β **Credentials**
2. Select the appropriate domain
3. Click **Add Credentials**
4. Kind: **Secret text**
5. ID: `td-api-key`, Secret: your API key
### Basic Pipeline
Create `Jenkinsfile`:
```groovy Jenkinsfile theme={null}
pipeline {
agent {
docker {
image 'node:20'
}
}
environment {
TD_API_KEY = credentials('td-api-key')
}
stages {
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Test') {
steps {
sh 'vitest --run'
}
}
}
}
```
### Parallel Execution
```groovy Jenkinsfile theme={null}
pipeline {
agent none
environment {
TD_API_KEY = credentials('td-api-key')
}
stages {
stage('Test') {
parallel {
stage('Shard 1') {
agent { docker { image 'node:20' } }
steps {
sh 'npm ci'
sh 'vitest --run --shard=1/4'
}
}
stage('Shard 2') {
agent { docker { image 'node:20' } }
steps {
sh 'npm ci'
sh 'vitest --run --shard=2/4'
}
}
stage('Shard 3') {
agent { docker { image 'node:20' } }
steps {
sh 'npm ci'
sh 'vitest --run --shard=3/4'
}
}
stage('Shard 4') {
agent { docker { image 'node:20' } }
steps {
sh 'npm ci'
sh 'vitest --run --shard=4/4'
}
}
}
}
}
}
```
### Multi-Platform Testing
```groovy Jenkinsfile theme={null}
pipeline {
agent none
environment {
TD_API_KEY = credentials('td-api-key')
}
stages {
stage('Test') {
parallel {
stage('Linux') {
agent { docker { image 'node:20' } }
environment {
TD_OS = 'linux'
}
steps {
sh 'npm ci'
sh 'vitest --run'
}
}
stage('Windows') {
agent { docker { image 'node:20' } }
environment {
TD_OS = 'windows'
}
steps {
sh 'npm ci'
sh 'vitest --run'
}
}
}
}
}
}
```
## Reading Platform in Tests
When using multi-platform testing, read the `TD_OS` environment variable in your test:
```javascript tests/cross-platform.test.mjs theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Cross-platform tests", () => {
it("should work on both Linux and Windows", async (context) => {
const os = process.env.TD_OS || 'linux';
const testdriver = TestDriver(context, {
os: os // 'linux' or 'windows'
});
await testdriver.provision.chrome({
url: 'https://example.com',
});
const result = await testdriver.assert("the page loaded successfully");
expect(result).toBeTruthy();
});
});
```
## Concurrency limits
Your plan allows a fixed number of sandboxes running at once. When a test asks for
a sandbox and you're already at that limit, the request is **queued** rather than
failed immediately: the SDK waits for a slot to free up, retrying every 10 seconds,
then proceeds automatically once one opens. This is what lets a parallel CI matrix
(many jobs starting at once) work on a plan with fewer slots than jobs β the extra
jobs simply wait their turn instead of erroring.
By default the SDK waits up to **60 seconds** for a slot before giving up with a
concurrency-limit error. Control that ceiling with `TD_CONCURRENCY_MAX_WAIT`:
| Value | Behavior |
| ----------------------------- | -------------------------------------------------------- |
| *unset* | Wait up to **60 seconds** (the default). |
| `TD_CONCURRENCY_MAX_WAIT=300` | Wait up to **300 seconds** (5 minutes) before giving up. |
| `TD_CONCURRENCY_MAX_WAIT=0` | **Don't queue** β fail on the first denial. |
The value is **in seconds** (fractional values are allowed and rounded to the
nearest millisecond). Any invalid or negative value falls back to the 60-second
default. The wait applies per sandbox request, across both the initial allocation
and the realtime slot-approval handshake.
```yaml theme={null}
# Example: a large parallel matrix that may queue for a while.
# Give each job up to 5 minutes to acquire a slot before failing.
- name: Run TestDriver tests
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
TD_CONCURRENCY_MAX_WAIT: "300"
run: npx vitest run
```
Raise `TD_CONCURRENCY_MAX_WAIT` when you run more parallel jobs than your plan has
slots and would rather they queue than fail. Set it to `0` when you'd prefer a job
to **fail fast** on a busy account (e.g. a quick smoke test that shouldn't sit
waiting). When jobs routinely give up waiting, that's the signal to
[add more slots](https://console.testdriver.ai/checkout/pro).
## Viewing Results
All test runs are automatically recorded and visible in your TestDriver dashboard at [console.testdriver.ai](https://console.testdriver.ai):
* All test runs with pass/fail status
* Video replays of each test
* Error messages and screenshots on failure
* Git commit and branch information
* Duration trends over time
# click()
Source: https://docs.testdriver.ai/v7/click
Click at specific coordinates or on elements
## Element Click
When called on an Element object, clicks on the located element.
### Syntax
```javascript theme={null}
await element.click(action)
```
### Parameters
Type of click action: `'click'`, `'double-click'`, `'right-click'`, `'hover'`, `'mouseDown'`, `'mouseUp'`
### Returns
`Promise`
### Examples
```javascript theme={null}
// Regular click
const button = await testdriver.find('submit button');
await button.click();
// Double-click
const file = await testdriver.find('README.txt file');
await file.click('double-click');
// Right-click
const item = await testdriver.find('menu item');
await item.click('right-click');
// Hover (same as element.hover())
const tooltip = await testdriver.find('info icon');
await button.click('hover');
```
## Coordinate Click
Click at specific screen coordinates.
### Syntax
```javascript theme={null}
await testdriver.click(x, y, action)
```
### Parameters
X coordinate
Y coordinate
Type of click: `'click'`, `'double-click'`, `'right-click'`, `'mouseDown'`, `'mouseUp'`
### Returns
`Promise`
### Examples
```javascript theme={null}
// Click at coordinates
await testdriver.click(500, 300);
// Double-click at coordinates
await testdriver.click(500, 300, 'double-click');
// Right-click at coordinates
await testdriver.click(500, 300, 'right-click');
```
## Click Actions
### Regular Click
Single left-click action.
```javascript theme={null}
const button = await testdriver.find('Login button');
await button.click();
```
### Double Click
Double-click action, commonly used to open files or select text.
```javascript theme={null}
const file = await testdriver.find('document.pdf');
await file.click('double-click');
// Or use the dedicated method
await file.doubleClick();
```
### Right Click
Right-click to open context menus.
```javascript theme={null}
const folder = await testdriver.find('Documents folder');
await folder.click('right-click');
// Or use the dedicated method
await folder.rightClick();
```
### Mouse Down / Mouse Up
For drag operations or custom click behavior.
```javascript theme={null}
const draggable = await testdriver.find('draggable item');
await draggable.click('mouseDown');
// Move to target
const dropZone = await testdriver.find('drop zone');
await dropZone.hover();
await dropZone.click('mouseUp');
// Or use dedicated methods
await draggable.mouseDown();
await dropZone.mouseUp();
```
## Best Practices
**Prefer element clicks over coordinate clicks**
Element-based clicking is more reliable and resolution-independent:
```javascript theme={null}
// β Preferred
const button = await testdriver.find('submit button');
await button.click();
// β Avoid (fragile)
await testdriver.click(500, 300);
```
**Verify element was found**
```javascript theme={null}
const element = await testdriver.find('button');
if (!element.found()) {
throw new Error('Element not found');
}
await element.click();
```
**Element must be found before clicking**
The `find()` method automatically locates elements, but clicking an element that wasn't found will throw an error:
```javascript theme={null}
const element = await testdriver.find('button');
// This will throw if element wasn't found
await element.click();
```
## Use Cases
```javascript theme={null}
const submitBtn = await testdriver.find('submit button');
await submitBtn.click();
const cancelBtn = await testdriver.find('cancel button');
await cancelBtn.click();
```
```javascript theme={null}
const file = await testdriver.find('report.pdf file icon');
await file.doubleClick();
```
```javascript theme={null}
const item = await testdriver.find('file item');
await item.rightClick();
// Select menu option
const deleteOption = await testdriver.find('Delete option');
await deleteOption.click();
```
```javascript theme={null}
const source = await testdriver.find('source item');
await source.mouseDown();
const target = await testdriver.find('target zone');
await target.hover();
await target.mouseUp();
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it } from 'vitest';
import TestDriver from 'testdriverai';
describe('Click Interactions', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should perform various click actions', async () => {
await testdriver.focusApplication('Google Chrome');
// Regular click
const loginBtn = await testdriver.find('login button');
await loginBtn.click();
// Right-click for context menu
const profileIcon = await testdriver.find('profile icon');
await profileIcon.rightClick();
const settingsOption = await testdriver.find('Settings menu option');
await settingsOption.click();
// Double-click to edit
const nameField = await testdriver.find('name display field');
await nameField.doubleClick();
// Verify state
await testdriver.assert('name field is now editable');
});
it('should perform drag and drop', async () => {
const item = await testdriver.find('draggable item');
await item.mouseDown();
// Drag to new location
const dropTarget = await testdriver.find('drop area');
await dropTarget.hover();
await dropTarget.mouseUp();
// Verify
await testdriver.assert('item is in the drop area');
});
});
```
## Related Methods
* [`find()`](/v7/find) - Locate elements to click
* [`hover()`](/v7/hover) - Hover without clicking
* [`doubleClick()`](/v7/double-click) - Dedicated double-click method
* [`rightClick()`](/v7/right-click) - Dedicated right-click method
# TestDriver Client
Source: https://docs.testdriver.ai/v7/client
Initialize and configure the TestDriver SDK client
## Overview
The `TestDriver` client is the main entry point for the SDK. It handles authentication, sandbox connection, and provides access to all testing methods.
## Constructor
```javascript theme={null}
const testdriver = new TestDriver(apiKey, options)
```
### Parameters
Your TestDriver API key from the [dashboard](https://console.testdriver.ai/team)
Configuration options for the client
Operating system for the sandbox: `'windows'` or `'linux'`
Screen resolution for the sandbox (e.g., `'1920x1080'`, `'1366x768'`). Custom resolutions are only available on Enterprise plans.
API endpoint URL (typically only changed for self-hosted deployments)
Enable or disable usage analytics
Enable or disable console logging
Automatically capture screenshots before and after each command. Screenshots are saved to `.testdriver/screenshots//` with descriptive filenames that include the line number and action name. Format: `---L-.png`
Force creation of a new sandbox instead of reusing an existing one
Reattach to the last used sandbox instead of creating a new one. When `true`, the SDK reads the sandbox id from `.testdriver/last-sandbox` (written automatically on every successful connect) and rejoins that VM. Provision methods (`chrome`, `vscode`, `installer`, etc.) are skipped because the application is already running. The previous sandbox must still be alive β see [`keepAlive`](#keepalive) and the [Run guide](/v7/copilot/running-tests#keeping-machines-alive-between-runs).
Reattach to a specific sandbox id instead of the one recorded in `.testdriver/last-sandbox`. Use this for CI matrices or to pin a chain of tests to a known VM. Implies `reconnect: true` behavior (provision calls are skipped).
Preview mode for live test visualization:
* `"browser"` β Opens debugger in default browser (default)
* `"ide"` β Opens preview in IDE panel (VSCode, Cursor - requires TestDriver extension)
* `"none"` β Headless mode, no visual preview
**Deprecated**: Use `preview: "none"` instead. Run in headless mode without opening the debugger.
Keep the sandbox alive when a test fails so you can reconnect and debug interactively. The sandbox ID is printed to the console.
Direct IP address to connect to a running sandbox instance (for self-hosted deployments)
Custom AMI ID for the sandbox instance (AWS deployments, e.g., `'ami-1234'`)
EC2 instance type for the sandbox (AWS deployments, e.g., `'i3.metal'`)
Enable or disable element caching, or provide advanced threshold configuration.
Enable or disable caching
Fine-tune cache matching
Thresholds for `find()` operations
Pixel diff threshold for screen comparison (0-1). `0.05` = 5% diff allowed.
OpenCV template match threshold for element matching (0-1). `0.8` = 80% correlation.
Pixel diff threshold for `assert()` operations (0-1). `0.05` = 5% diff allowed.
Cache key for element finding operations. If provided, enables caching tied to this key.
Global default for AI verification of located elements. When `true`, every `find()` runs a second AI check to confirm the returned coordinates actually match the requested element (catching hallucinated or incorrect positions), at the cost of extra latency. Can be overridden per call via `find(description, { verify: true | false })`.
Enable or disable Dashcam video recording
Enable or disable screen-change (redraw) detection, or provide advanced configuration.
Enable or disable redraw detection
Threshold configuration
Pixel diff threshold (0-1). Set to `false` to disable screen redraw detection.
Enable or disable network activity monitoring
Additional environment variables to pass to the sandbox
Global AI sampling configuration. Controls how the AI model generates responses for `find()` verification and `assert()` calls. Can be overridden per call.
Controls randomness in AI responses. `0` = deterministic (best for verification), higher values = more creative. Default: `0` for find verification, model default for assert.
Nucleus and top-k sampling parameters
Top-P (nucleus sampling). Limits token choices to the smallest set whose cumulative probability exceeds P. Lower values = more focused responses. Range: 0-1.
Top-K sampling. Limits token choices to the top K most likely tokens. `1` = always pick the most likely token. `0` = disabled (consider all tokens).
### Example
```javascript theme={null}
import TestDriver from 'testdriverai';
// API key is automatically loaded from TD_API_KEY in .env
const testdriver = new TestDriver({
os: 'windows',
resolution: '1920x1080',
logging: true,
analytics: true
});
// With AI config for stricter verification
const testdriver = new TestDriver({
ai: { temperature: 0, top: { p: 0.9, k: 40 } }
});
// Or pass API key explicitly
const testdriver = new TestDriver('your-api-key', {
os: 'windows'
});
```
## Authentication
### auth()
Authenticate with the TestDriver API.
```javascript theme={null}
await testdriver.auth()
```
**Returns:** `Promise` - Authentication token
**Example:**
```javascript theme={null}
await testdriver.auth();
```
You must call `auth()` before `connect()`. Most examples call both sequentially.
## Connection Management
### connect()
Connect to a sandbox environment. This creates or reconnects to a virtual machine where your tests will run.
```javascript theme={null}
await testdriver.connect(options)
```
#### Parameters
Connection options
Force creation of a new sandbox instead of reusing an existing one
Existing sandbox ID to reconnect to
Direct IP address to connect to (for self-hosted sandboxes)
AMI to use for the sandbox (AWS deployments)
Instance type for the sandbox (AWS deployments)
Preview mode for live test visualization:
* `"browser"` - Opens debugger in default browser (default)
* `"ide"` - Opens preview in IDE panel (VSCode, Cursor - requires TestDriver extension)
* `"none"` - Headless mode, no visual preview
**Deprecated**: Use `preview: "none"` instead. Run in headless mode without opening the debugger.
Keep sandbox alive for the specified number of milliseconds after disconnect. Set to `0` to terminate immediately on disconnect. Useful for debugging or reconnecting to the same sandbox.
**Returns:** `Promise<Object>` - Sandbox instance details including `instanceId`, `ip`, `vncPort`, etc.
#### Examples
**Basic connection:**
```javascript theme={null}
await testdriver.connect();
```
**Reconnect to existing sandbox:**
```javascript theme={null}
const instance = await testdriver.connect({
sandboxId: 'existing-sandbox-id-123'
});
```
**Self-hosted sandbox:**
```javascript theme={null}
await testdriver.connect({
ip: '192.168.1.100'
});
```
### disconnect()
Disconnect from the sandbox and clean up resources.
```javascript theme={null}
await testdriver.disconnect()
```
**Returns:** `Promise`
**Example:**
```javascript theme={null}
afterAll(async () => {
await testdriver.disconnect();
});
```
## Instance Information
### getInstance()
Get the current sandbox instance details.
```javascript theme={null}
const instance = testdriver.getInstance()
```
**Returns:** `Object | null` - Sandbox instance information
**Example:**
```javascript theme={null}
const instance = testdriver.getInstance();
console.log('Instance ID:', instance.instanceId);
console.log('IP Address:', instance.ip);
```
### getSessionId()
Get the current session ID for tracking and debugging.
```javascript theme={null}
const sessionId = testdriver.getSessionId()
```
**Returns:** `string | null` - Session ID
**Example:**
```javascript theme={null}
const sessionId = testdriver.getSessionId();
console.log('Session:', sessionId);
```
## Logging & Events
### setLogging()
Enable or disable console logging at runtime.
```javascript theme={null}
testdriver.setLogging(enabled)
```
**Parameters:**
* `enabled` (boolean) - Whether to enable logging
**Example:**
```javascript theme={null}
// Disable logging for cleanup operations
testdriver.setLogging(false);
await testdriver.disconnect();
testdriver.setLogging(true);
```
### getEmitter()
Get the event emitter for custom event handling.
```javascript theme={null}
const emitter = testdriver.getEmitter()
```
**Returns:** `EventEmitter2` - Event emitter instance
**Example:**
```javascript theme={null}
const emitter = testdriver.getEmitter();
emitter.on('command:start', (data) => {
console.log('Command started:', data);
});
emitter.on('command:success', (data) => {
console.log('Command succeeded:', data);
});
emitter.on('command:error', (error) => {
console.error('Command failed:', error);
});
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it } from 'vitest';
import TestDriver from 'testdriverai';
describe('My Test Suite', () => {
let testdriver;
beforeAll(async () => {
// Initialize client - API key loaded automatically from .env
testdriver = new TestDriver({
os: 'windows',
resolution: '1366x768',
logging: true
});
// Set up event listeners
const emitter = testdriver.getEmitter();
emitter.on('log:info', (msg) => console.log('[INFO]', msg));
// Authenticate and connect
await testdriver.auth();
const instance = await testdriver.connect();
console.log('Connected to sandbox:', instance.instanceId);
});
afterAll(async () => {
await testdriver.disconnect();
});
it('runs a test', async () => {
// Your test code here
});
});
```
## Best Practices
Use `beforeAll`/`afterAll` to create one sandbox per test suite rather than per test. This significantly reduces execution time.
Wrap `connect()` in a try-catch block to handle network issues or quota limits:
```javascript theme={null}
try {
await testdriver.connect();
} catch (error) {
console.error('Failed to connect:', error.message);
throw error;
}
```
Use `afterAll` or try-finally blocks to ensure `disconnect()` is called even if tests fail. This prevents orphaned sandboxes.
Never hardcode API keys. The SDK automatically loads `TD_API_KEY` from your `.env` file:
```bash .env theme={null}
TD_API_KEY=your_api_key_here
```
```javascript theme={null}
// API key is loaded automatically - no need to pass it!
const testdriver = new TestDriver();
```
# Prevent
Source: https://docs.testdriver.ai/v7/copilot/auto-healing
Catch regressions automatically with CI, auto-healing, and GitHub integration
Prevent regressions by letting TestDriver run and repair itself on every change. Once you've [explored](/v7/generating-tests), [taught it your app](/v7/caching), [run it](/v7/copilot/running-tests), [validated outcomes](/v7/making-assertions), [adapted to your UI](/v7/performing-actions), and learned to [debug with screenshots](/v7/debugging-with-screenshots), the last step is closing the loop: wire TestDriver into your pull requests so regressions never reach production.
Your AI agent can run tests on every PR, investigate failures, and propose fixes β and you can drive all of it from GitHub itself, whether you're at your desk or on your phone.
## Use TestDriver in GitHub
TestDriver works directly in GitHub's web interface and mobile app. The same MCP server that powers VS Code integration also works in GitHub, letting you create and manage tests from anywhere.
### How It Works
When you add a TestDriver agent file to your repository at `.github/agents/testdriver.agent.md`, GitHub Copilot can use TestDriver's MCP tools directly in:
* GitHub.com (web browser)
* GitHub Mobile app (iOS/Android)
* Pull request conversations
* Issue comments
### Using TestDriver in GitHub Web
Open your repository on GitHub.com. Make sure you have the TestDriver agent file at `.github/agents/testdriver.agent.md`.
Click the **Copilot icon** in the GitHub interface to open a chat. You can find this in:
* The repository's Code tab
* Pull request pages
* Issue pages
Start your message with `@testdriver`:
```
@testdriver Create a test that verifies the homepage loads correctly at https://myapp.com
```
The agent will spawn a sandbox environment and begin executing, just like in VS Code.
As the test runs, screenshots appear directly in the chat. You can see what the AI sees and provide guidance if needed.
### Creating Tests from PR Comments
You can create tests directly from pull request reviews. Comment on a PR and mention Copilot:
```
@copilot create a TestDriver test that verifies this new feature works.
Test the checkout flow with a guest user.
```
Copilot will:
1. Use the TestDriver MCP server
2. Create a test based on your description
3. Commit the test file to the PR branch
This is useful for:
* Adding test coverage during code review
* Verifying bug fixes before merging
* Creating regression tests for new features
### Creating Tests from Issues
You can also create tests from issue comments:
```
@copilot Use TestDriver to create a test that reproduces this bug.
Navigate to /settings, change the theme, and verify it persists after refresh.
```
The test will be created in a new branch and linked to the issue.
### Mobile App Support
The GitHub Mobile app supports Copilot chat, which means you can use TestDriver from your phone:
1. Open the GitHub app
2. Navigate to your repository
3. Tap the Copilot icon
4. Type `@testdriver` followed by your request
Screenshots and test progress appear in the chat, letting you create and debug tests on the go.
### Example: PR Review Workflow
Here's a complete workflow for adding tests during code review:
A developer opens a pull request with a new feature.
The reviewer comments:
```
@copilot Create a TestDriver test for this user registration flow.
Test both successful registration and validation errors.
```
Copilot spawns TestDriver, creates the tests, and commits them to the PR branch.
The new tests run automatically in CI, validating the feature works as expected.
### Agent File Reference
The agent file at `.github/agents/testdriver.agent.md` contains the configuration for GitHub Copilot to use TestDriver. Here's the structure:
```yaml theme={null}
---
name: testdriver
description: An expert at creating and refining automated tests using TestDriver.ai
mcp-servers:
testdriver:
command: npx
args:
- -p
- testdriverai
- testdriverai-mcp
env:
TD_API_KEY: ${TD_API_KEY}
---
# TestDriver Expert
You are an expert at writing automated tests using TestDriver...
```
The `TD_API_KEY` is pulled from your repository secrets when running in GitHub Actions or from your environment when using the web interface.
Make sure `TD_API_KEY` is set in your repository secrets for CI workflows. Go to **Settings β Secrets and variables β Actions** to add it.
## Auto-Healing Tests
Auto-healing tests automatically fix themselves when your application changes. By integrating an AI coding agent with TestDriver in your CI pipeline, you can have AI investigate test failures and propose fixes. The example below uses GitHub Copilot in GitHub Actions, but the same approach works with any AI agent that can run TestDriver's MCP server.
### How It Works
When a test fails in CI:
1. **GitHub Actions detects the failure**
2. **The AI agent spawns TestDriver** with access to the MCP server
3. **The agent investigates** by running the failing test and analyzing what changed
4. **The agent creates a fix** by updating the test code
5. **A pull request is opened** with the proposed changes for review
### Setting Up Auto-Healing
Add your TestDriver API key to your repository secrets:
1. Go to **Settings β Secrets and variables β Actions**
2. Click **New repository secret**
3. Add `TD_API_KEY` with your API key value
Add a GitHub Actions workflow that triggers on test failures:
```yaml .github/workflows/auto-heal.yml theme={null}
name: Auto-Heal Tests
on:
workflow_run:
workflows: ["Tests"] # Your main test workflow
types: [completed]
branches: [main]
jobs:
auto-heal:
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Run failing tests and capture output
id: tests
continue-on-error: true
run: |
vitest run 2>&1 | tee test-output.txt
echo "output<> $GITHUB_OUTPUT
cat test-output.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
- name: Invoke Copilot to fix tests
uses: github/copilot-action@v1
with:
prompt: |
@testdriver The following tests failed:
${{ steps.tests.outputs.output }}
Please investigate each failure by:
1. Running the failing test
2. Analyzing what changed in the UI or behavior
3. Updating the test code to fix the issue
Create a commit with your changes.
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
title: "Auto-heal: Fix failing tests"
body: |
This PR was automatically generated by the auto-heal workflow.
## Changes
The following tests were updated to fix failures detected in CI.
## Review
Please review the changes carefully before merging.
branch: auto-heal/${{ github.run_id }}
commit-message: "fix: auto-heal failing tests"
```
Make sure your main test workflow has a name that matches the `workflows` trigger:
```yaml .github/workflows/tests.yml theme={null}
name: Tests # Must match the workflow_run trigger
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: vitest run
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
```
### Example: Button Text Change
Here's what auto-healing looks like when a button's text changes:
A developer changes a button's text from "Submit" to "Send":
```html theme={null}
```
The test fails because it's looking for the old text:
```javascript theme={null}
// This now fails
const submitButton = await testdriver.find("Submit button");
```
The auto-heal workflow runs, and the AI agent investigates:
```
The test is looking for a "Submit button" but I see a "Send button"
on the page. The button functionality is the same, just the text changed.
I'll update the test to use the new text.
```
A pull request is opened with the fix:
```javascript theme={null}
// Updated by auto-heal
const submitButton = await testdriver.find("Send button");
```
### Configuration Options
#### Selective Auto-Healing
You can limit auto-healing to specific test files or patterns:
```yaml theme={null}
- name: Run failing tests
run: |
# Only heal tests in the e2e directory
vitest run tests/e2e/ 2>&1 | tee test-output.txt
```
#### Manual Approval
For safety, you can require manual approval before auto-heal runs:
```yaml theme={null}
jobs:
auto-heal:
environment: auto-heal # Requires approval
```
Configure the environment in **Settings β Environments** with required reviewers.
#### Limiting Changes
Add instructions to constrain what the AI can change:
```yaml theme={null}
- name: Invoke Copilot to fix tests
uses: github/copilot-action@v1
with:
prompt: |
@testdriver Fix the failing tests.
Rules:
- Only update element selectors and text matching
- Do not change test logic or assertions
- Do not add or remove tests
- Keep changes minimal
```
### Best Practices
Auto-heal is a tool, not a replacement for human judgment. Review all changes before merging to ensure the test still validates what you intended.
Tests with clear, semantic descriptions are easier for the AI to heal:
```javascript theme={null}
// β Good - describes purpose
await testdriver.find("primary call-to-action button in the hero section");
// β Bad - too vague
await testdriver.find("button");
```
Configure GitHub notifications or Slack integration to be alerted when auto-heal PRs are created.
Monitor how often tests need healing. High heal rates may indicate:
* Tests are too brittle
* Application is changing rapidly
* Element descriptions need improvement
### Limitations
Auto-healing works best for:
* Element text changes
* Layout and styling updates
* Minor UI restructuring
It may struggle with:
* Major workflow changes
* New features requiring new assertions
* Complex multi-step interactions
For significant changes, create new tests by going back to [Explore](/v7/generating-tests) and generating them with the TestDriver agent.
Open the TestDriver console to monitor test runs, healing PRs, and CI results across your projects.
# Run
Source: https://docs.testdriver.ai/v7/copilot/running-tests
Set up your environment and run TestDriver tests locally and in CI
Run the tests you've explored and learned, anywhere. TestDriver tests are plain [Vitest](https://vitest.dev) files, so they run the same way on your machine and in CI β across web, desktop, and extensions on real VMs. This page walks through getting set up, configuring **where** and **how** your tests run (the device and app under test, and the sandbox machine), and actually running them locally and in CI.
If you haven't written tests yet, start with [Explore](/v7/generating-tests) to generate your first tests and [Learn](/v7/caching) to make them fast and reliable.
## Setup
TestDriver integrates with AI coding assistants through the VS Code extension and MCP server. The same MCP server works with GitHub Copilot, Cursor, Claude Desktop, and any other MCP-capable assistant. This section walks you through the complete setup.
### Prerequisites
Before you begin, you'll need:
* **An MCP-capable AI assistant** β Such as GitHub Copilot, Cursor, or Claude Desktop. For Copilot, a [free tier](https://github.com/features/copilot/plans) is available.
* **TestDriver Account** β Create a free account at [console.testdriver.ai](https://console.testdriver.ai/team) to get your API key. 60 free device minutes, no credit card required.
* **VS Code** β The TestDriver extension provides the best experience with live preview and integrated test running.
### Setup Steps
The extension provides:
* One-click sign-in and project initialization
* Live preview panel for watching tests execute
* MCP server configuration
Sign in to connect your account and API key.
1. Open the command palette (`Cmd+Shift+P` or `Ctrl+Shift+P`)
2. Run **TestDriver: Login**
3. Your browser will open to the TestDriver sign-in page
4. Sign in (or create an account)
5. You'll be redirected back to VS Code, now signed in
The extension automatically saves your API key to VS Code's secure storage and your workspace `.env` file.
Set up TestDriver in your project with a single command.
1. Open the command palette (`Cmd+Shift+P` or `Ctrl+Shift+P`)
2. Run **TestDriver: Init Project**
This command:
* Creates a `package.json` with TestDriver and Vitest dependencies
* Generates a `vitest.config.mjs` with proper timeout settings
* Creates example test files in `tests/`
* Sets up `.env` with your API key
* Creates the TestDriver agent file at `.github/agents/testdriver.agent.md`
* Configures the MCP server
If you already have a `package.json`, the command will add the necessary dependencies to it.
The MCP server enables your AI assistant to control TestDriver sandboxes.
After initialization, the MCP configuration is created at `.vscode/mcp.json`:
```json .vscode/mcp.json theme={null}
{
"servers": {
"testdriver": {
"command": "npx",
"args": ["-p", "testdriverai", "testdriverai-mcp"],
"env": {
"TD_PREVIEW": "ide",
"TD_API_KEY": "your-api-key"
}
}
}
}
```
**To start the MCP server:**
1. Open the command palette (`Cmd+Shift+P` or `Ctrl+Shift+P`)
2. Run **MCP: List Servers**
3. Click on the **testdriver** server
4. Select **Start Server**
You can also click the MCP icon in the status bar to manage servers.
See the [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more details on managing MCP servers.
Make sure your API key is set. The extension uses the key from your sign-in, but you can also set it via the `TD_API_KEY` environment variable.
For the best experience running tests, install the Vitest extension:
Run tests with GUI mode from the Test Explorer
After installation, you'll see a beaker icon in the sidebar for accessing the Test Explorer.
### Verify Your Setup
To verify everything is configured correctly:
1. Open the command palette and run **TestDriver: Check Status**
2. You should see:
* β Signed in
* β MCP server configured
* β Project initialized
### The Agent File
During initialization, TestDriver creates an agent file at `.github/agents/testdriver.agent.md`. This file tells your AI assistant how to use TestDriver's MCP tools.
The agent has access to tools like:
* `session_start` β Launch a sandbox with Chrome or other apps
* `find` / `click` / `type` β Interact with elements on screen
* `assert` β Verify conditions using AI vision
* `screenshot` β Capture the current screen state
## Configuring the Device
Provision methods are the starting point for most tests. They launch applications in your sandbox and prepare the environment for testing β a browser, a desktop app, a Chrome extension, or VS Code.
### Chrome Browser
The most common starting point for web testing. Launches Chrome browser and navigates to a URL.
```javascript theme={null}
await testdriver.provision.chrome({
url: 'https://example.com',
});
```
#### Options
| Option | Type | Default | Description |
| ----------- | ------- | ----------------------------------------- | --------------------------- |
| `url` | string | `'http://testdriver-sandbox.vercel.app/'` | URL to navigate to |
| `maximized` | boolean | `true` | Start browser maximized |
| `guest` | boolean | `false` | Use guest mode (no profile) |
#### Example: Basic Web Test
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Login Flow", () => {
it("should log in successfully", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({
url: 'https://myapp.com/login',
});
await testdriver.find("Email input").click();
await testdriver.type("user@example.com");
await testdriver.find("Password input").click();
await testdriver.type("password123");
await testdriver.find("Sign In button").click();
const result = await testdriver.assert("the dashboard is visible");
expect(result).toBeTruthy();
});
});
```
`provision.chrome()` automatically starts Dashcam recording and waits for Chrome to be ready before returning.
### Chrome Extensions
Launch Chrome with a custom extension loaded. Supports both local extensions and Chrome Web Store extensions.
#### Load from Local Path
Clone or create an extension locally, then load it:
```javascript theme={null}
// First, get the extension onto the sandbox
await testdriver.exec(
'sh',
'git clone https://github.com/user/my-extension.git /tmp/my-extension',
60000
);
// Launch Chrome with the extension
await testdriver.provision.chromeExtension({
extensionPath: '/tmp/my-extension',
url: 'https://example.com'
});
```
#### Load from Chrome Web Store
Load any published extension by its Chrome Web Store ID:
```javascript theme={null}
await testdriver.provision.chromeExtension({
extensionId: 'cjpalhdlnbpafiamejdnhcphjbkeiagm', // uBlock Origin
url: 'https://example.com'
});
```
Find the extension ID in the Chrome Web Store URL. For example, `https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm` β ID is `cjpalhdlnbpafiamejdnhcphjbkeiagm`
#### Options
| Option | Type | Default | Description |
| --------------- | ------- | ------- | ------------------------------------------ |
| `extensionPath` | string | - | Local path to unpacked extension directory |
| `extensionId` | string | - | Chrome Web Store extension ID |
| `url` | string | - | URL to navigate to after launch |
| `maximized` | boolean | `true` | Start browser maximized |
You must provide either `extensionPath` or `extensionId`, but not both.
#### Example: Testing a Chrome Extension
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Chrome Extension Test", () => {
it("should load and interact with extension", async (context) => {
const testdriver = TestDriver(context);
// Clone extension from GitHub
await testdriver.exec(
'sh',
'git clone https://github.com/user/my-extension.git /tmp/my-extension',
60000,
true
);
// Launch Chrome with extension loaded
await testdriver.provision.chromeExtension({
extensionPath: '/tmp/my-extension',
url: 'https://testdriver.ai'
});
// Click extensions puzzle icon
const extensionsButton = await testdriver.find("puzzle-shaped icon in Chrome toolbar");
await extensionsButton.click();
// Interact with your extension
const myExtension = await testdriver.find("My Extension in the dropdown");
await myExtension.click();
const result = await testdriver.assert("extension popup is visible");
expect(result).toBeTruthy();
});
});
```
### Desktop Apps
Download and install desktop applications. Supports `.deb`, `.rpm`, `.msi`, `.exe`, `.AppImage`, `.dmg`, `.pkg`, and shell scripts.
```javascript theme={null}
const filePath = await testdriver.provision.installer({
url: 'https://example.com/app.deb',
appName: 'MyApp', // Focus this app after install
launch: true, // Auto-launch after install
});
```
#### Options
| Option | Type | Default | Description |
| ---------- | ------- | ------------- | --------------------------------------- |
| `url` | string | **required** | URL to download the installer from |
| `filename` | string | auto-detected | Filename to save as |
| `appName` | string | - | Application name to focus after install |
| `launch` | boolean | `true` | Launch the app after installation |
#### Supported File Types
| Extension | OS | Install Method |
| ----------- | ------- | -------------------------------- |
| `.deb` | Linux | `dpkg -i` + `apt-get install -f` |
| `.rpm` | Linux | `rpm -i` |
| `.AppImage` | Linux | `chmod +x` |
| `.sh` | Linux | `chmod +x` + execute |
| `.msi` | Windows | `msiexec /i /quiet` |
| `.exe` | Windows | Silent install (`/S`) |
| `.dmg` | macOS | Mount + copy to Applications |
| `.pkg` | macOS | `installer -pkg` |
#### Example: Install and Test a Desktop App
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Desktop App Test", () => {
it("should install and launch app", async (context) => {
const testdriver = TestDriver(context);
// Download and install
const installerPath = await testdriver.provision.installer({
url: 'https://github.com/sharkdp/bat/releases/download/v0.24.0/bat_0.24.0_amd64.deb',
});
// Verify installation
const output = await testdriver.exec('sh', 'bat --version', 5000);
expect(output).toContain('bat');
});
});
```
#### Example: Windows Installer
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Windows App Test", () => {
it("should install on Windows", async (context) => {
const testdriver = TestDriver(context, {
os: 'windows'
});
// Download MSI installer
const installerPath = await testdriver.provision.installer({
url: 'https://example.com/app.msi',
launch: false, // Don't auto-launch
});
// Custom installation if needed
await testdriver.exec(
'pwsh',
`Start-Process msiexec.exe -ArgumentList "/i", "${installerPath}", "/qn" -Wait`,
120000
);
// Verify installation
const result = await testdriver.assert("application is installed");
expect(result).toBeTruthy();
});
});
```
#### Manual Installation
Set `launch: false` to download without auto-installing:
```javascript theme={null}
const filePath = await testdriver.provision.installer({
url: 'https://example.com/custom-script.sh',
launch: false,
});
// Run custom install commands
await testdriver.exec('sh', `chmod +x "${filePath}"`, 5000);
await testdriver.exec('sh', `"${filePath}" --custom-flag`, 60000);
```
### VS Code
Launch Visual Studio Code with optional workspace and extensions.
```javascript theme={null}
await testdriver.provision.vscode({
workspace: '/home/testdriver/my-project',
extensions: ['ms-python.python', 'esbenp.prettier-vscode'],
});
```
#### Options
| Option | Type | Default | Description |
| ------------ | --------- | ------- | ----------------------------- |
| `workspace` | string | - | Workspace folder to open |
| `extensions` | string\[] | `[]` | Extensions to install (by ID) |
#### Example: VS Code Extension Test
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("VS Code Test", () => {
it("should open workspace with extensions", async (context) => {
const testdriver = TestDriver(context);
// Create a test project
await testdriver.exec('sh', 'mkdir -p /tmp/test-project && echo "print(1)" > /tmp/test-project/test.py', 10000);
// Launch VS Code
await testdriver.provision.vscode({
workspace: '/tmp/test-project',
extensions: ['ms-python.python'],
});
// Verify VS Code is ready
const result = await testdriver.assert("VS Code is open with the project");
expect(result).toBeTruthy();
// Open the Python file
await testdriver.find("test.py in the explorer").click();
});
});
```
### Choosing the Right Provision Method
| Use Case | Method |
| ------------------------------------------ | --------------------------- |
| Testing a website | `provision.chrome` |
| Testing a Chrome extension | `provision.chromeExtension` |
| Testing a desktop app (needs installation) | `provision.installer` |
| Testing VS Code or VS Code extensions | `provision.vscode` |
All provision methods automatically start Dashcam recording and wait for the application to be ready before returning. You don't need to call `dashcam.start()` manually.
## Configuring the Machine
TestDriver provisions a fresh cloud VM for every test by default. This section covers how to configure Linux and Windows machines, reduce startup time by keeping machines alive between runs, use provision scripts for repeatable setup, and install custom software on the fly.
### Linux Machines
Linux is the default operating system. No extra configuration is required.
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("My Test", () => {
it("runs on Linux", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({ url: "https://example.com" });
const result = await testdriver.assert("the page loaded successfully");
expect(result).toBeTruthy();
});
});
```
#### Common Linux Options
| Option | Type | Default | Description |
| --------------- | ------- | ------------ | ----------------------------------------------------------- |
| `os` | string | `"linux"` | Operating system |
| `resolution` | string | `"1366x768"` | Screen resolution (Enterprise only) |
| `e2bTemplateId` | string | β | Custom E2B template ID (see [Self-Hosted](/v7/self-hosted)) |
| `keepAlive` | number | `60000` | Ms to keep VM alive after disconnect |
| `reconnect` | boolean | `false` | Reconnect to last used sandbox |
```javascript theme={null}
const testdriver = TestDriver(context, {
os: "linux",
resolution: "1920x1080",
keepAlive: 5 * 60 * 1000, // keep alive 5 minutes
});
```
### Windows Machines
Set `os: "windows"` to provision a Windows VM instead. Everything else works the same way.
```javascript theme={null}
const testdriver = TestDriver(context, {
os: "windows",
});
await testdriver.provision.chrome({ url: "https://example.com" });
```
Windows sandboxes use EC2 instances and take longer to boot than Linux (E2B) sandboxes β typically 1β3 minutes for a cold start. See [Keeping Machines Alive](#keeping-machines-alive-between-runs) below to avoid this cost on repeated runs.
#### Common Windows Options
| Option | Type | Default | Description |
| ----------------- | ------- | ------------ | ------------------------------------ |
| `os` | string | β | Set to `"windows"` |
| `resolution` | string | `"1366x768"` | Screen resolution (Enterprise only) |
| `sandboxAmi` | string | β | Custom AMI ID (self-hosted) |
| `sandboxInstance` | string | β | EC2 instance type (self-hosted) |
| `keepAlive` | number | `60000` | Ms to keep VM alive after disconnect |
| `reconnect` | boolean | `false` | Reconnect to last used sandbox |
```javascript theme={null}
const testdriver = TestDriver(context, {
os: "windows",
resolution: "1920x1080",
keepAlive: 10 * 60 * 1000, // keep alive 10 minutes
});
```
### Keeping Machines Alive Between Runs
Windows (and Linux) cold starts can be expensive if you're iterating quickly. Use `keepAlive` + `reconnect` to reuse the same VM across multiple test runs.
#### How it works
Every time the SDK successfully connects to a sandbox, it records the sandbox id in `.testdriver/last-sandbox` inside your project directory. The next test that opts in with `reconnect: true` reads that file and reattaches automatically β no manual id tracking required.
Provision calls (`testdriver.provision.chrome(...)`, `vscode(...)`, etc.) are **skipped** when reconnecting, because the application is already running inside the sandbox from the previous run.
`.testdriver/last-sandbox` is already covered by the default TestDriver `.gitignore`. Don't commit it.
#### Step 1 β Start the machine with a long `keepAlive`
```javascript theme={null}
// first.test.mjs
const testdriver = TestDriver(context, {
os: "windows",
keepAlive: 30 * 60 * 1000, // keep alive 30 minutes after this test ends
});
await testdriver.provision.chrome({ url: "https://example.com" });
// ... your test steps
```
When this test finishes, the sandbox stays running for 30 minutes instead of being terminated immediately.
#### Step 2 β Reattach automatically with `reconnect: true`
```javascript theme={null}
// second.test.mjs
const testdriver = TestDriver(context, {
os: "windows",
reconnect: true, // β reads .testdriver/last-sandbox
keepAlive: 30 * 60 * 1000,
});
// No provision call β Chrome is already open from the previous run.
await testdriver.find("Sign In button").click();
```
#### Step 2 (alternative) β Reattach to an explicit id
If you need to pin to a specific sandbox (CI matrix, multiple chains in parallel, etc.) pass the id directly:
```javascript theme={null}
await testdriver.connect({ sandboxId: "sandbox-abc123" });
```
When reattaching to a sandbox:
* You reuse a specific running machine directly
* You continue from the app state created in the earlier run
* You must run within the previous test's `keepAlive` window
Use `testdriver.getLastSandboxId()` to read the recorded sandbox id (and optional metadata) for scripting purposes.
#### Chaining describe blocks within one test file
A common pattern is to break a long flow into focused `describe` blocks that share one sandbox β the first block provisions and signs in, later blocks reconnect and continue:
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
const KEEP_ALIVE_MS = 5 * 60 * 1000;
describe("step 1 β log in", () => {
it("signs in and lands on the dashboard", async (context) => {
const testdriver = TestDriver(context, { keepAlive: KEEP_ALIVE_MS });
await testdriver.provision.chrome({ url: "https://example.com/login" });
await testdriver.find("username input").click();
await testdriver.type("standard_user");
await testdriver.pressKeys(["tab"]);
await testdriver.type("secret_sauce", { secret: true });
await testdriver.pressKeys(["enter"]);
expect(await testdriver.assert("the dashboard is visible")).toBeTruthy();
});
});
describe("step 2 β add to cart", () => {
it("reuses the logged-in sandbox", async (context) => {
const testdriver = TestDriver(context, {
reconnect: true, // β skip provisioning, reattach
keepAlive: KEEP_ALIVE_MS,
});
await testdriver.find("Add to cart").click();
await testdriver.find("cart icon").click();
expect(await testdriver.assert("the cart has an item")).toBeTruthy();
});
});
describe("step 3 β check out", () => {
it("continues from the cart state", async (context) => {
const testdriver = TestDriver(context, { reconnect: true, keepAlive: 30_000 });
await testdriver.find("Checkout").click();
expect(await testdriver.assert("the checkout form is visible")).toBeTruthy();
});
});
```
A runnable copy of this pattern lives at [`examples/reconnect-sequential.test.mjs`](https://github.com/testdriverai/mono/blob/main/sdk/examples/reconnect-sequential.test.mjs).
Vitest runs **test files** in parallel by default. Within a single file, `describe`/`it` blocks run in source order, so reconnect chaining works as written. To chain across multiple files, run them sequentially (e.g. `vitest run --sequence.concurrent=false` or place them in a single project pool with workers set to 1).
#### How `keepAlive` works
`keepAlive` is a duration in milliseconds. After the SDK disconnects, the server keeps the VM running for that long before terminating it. The default is `60000` (1 minute). Note: `keepAlive: 0` currently falls back to the default disconnect grace period rather than terminating immediately, so use a positive duration when you want to control the grace window explicitly.
```javascript theme={null}
const testdriver = TestDriver(context, {
keepAlive: 0, // currently uses the default 1 minute grace period
// keepAlive: 60000, // default β 1 minute
// keepAlive: 600000, // 10 minutes
// keepAlive: 3600000, // 1 hour
});
```
Machines kept alive beyond your test session continue to consume credits. Always set a `keepAlive` value appropriate for your workflow.
### Using Provision Scripts
Provision scripts let you run arbitrary setup steps before your test starts β downloading fixtures, seeding a database, configuring environment variables, and more. Use `testdriver.exec()` to run shell or PowerShell commands directly in the sandbox.
Full reference for running shell and PowerShell commands in the sandbox.
#### Linux setup script
```javascript theme={null}
await testdriver.provision.chrome({ url: "https://myapp.com" });
// Run a setup script from your repo
await testdriver.exec("sh", `
curl -s https://myapp.com/api/reset-test-db -X POST
echo "Test DB reset"
`, 30000);
```
#### Windows setup script (PowerShell)
```javascript theme={null}
await testdriver.provision.chrome({ url: "https://myapp.com" });
await testdriver.exec("pwsh", `
$env:API_URL = "https://staging.myapp.com"
Write-Host "Environment configured"
`, 15000);
```
#### Clone a repo and run a script
```javascript theme={null}
await testdriver.exec("sh", `
git clone https://github.com/myorg/test-fixtures.git /tmp/fixtures
bash /tmp/fixtures/seed.sh
`, 120000);
```
### Installing Custom Software
You can install software at the start of a test using `exec()`. This works for any package available via `apt`, `brew`, `choco`, `winget`, npm, pip, or direct download.
#### Linux β apt packages
```javascript theme={null}
await testdriver.exec("sh", `
sudo apt-get update -qq
sudo apt-get install -y ffmpeg imagemagick
`, 120000);
```
#### Linux β Node.js tools
```javascript theme={null}
await testdriver.exec("sh", "npm install -g @playwright/test", 60000);
```
#### Windows β winget
```javascript theme={null}
await testdriver.exec("pwsh", `
winget install --id=7zip.7zip -e --silent
`, 120000);
```
#### Windows β Chocolatey
```javascript theme={null}
await testdriver.exec("pwsh", `
choco install googlechrome --yes --no-progress
`, 180000);
```
#### Download and run an installer
```javascript theme={null}
// Linux
await testdriver.exec("sh", `
curl -L https://example.com/installer.sh -o /tmp/installer.sh
chmod +x /tmp/installer.sh
/tmp/installer.sh --silent
`, 300000);
// Windows
await testdriver.exec("pwsh", `
Invoke-WebRequest -Uri "https://example.com/installer.exe" -OutFile "$env:TEMP\\installer.exe"
Start-Process "$env:TEMP\\installer.exe" -ArgumentList "/S" -Wait
`, 300000);
```
Installing software at test start adds to your test duration. For software you use in every test, consider preloading it into a custom VM image via the Enterprise self-hosted plan.
#### Want Software Pre-Installed on Every Machine?
Installing packages at runtime works well for occasional or lightweight dependencies. But if you're installing the same 5-minute setup on every test run, you're wasting time and credits.
With the **Self-Hosted Enterprise plan** you get access to our golden VM base image and Packer scripts, so you can bake your applications, dependencies, and configuration directly into a custom AMI. Tests spin up with everything already installed β zero setup time.
Preload software, configure custom hardware, and run unlimited tests with a flat license fee. Our team assists with deployment and setup.
## Running Tests
After creating tests with the TestDriver agent, you can re-run them without starting a new MCP session. Tests are saved as standard Vitest files that run independently β the same way on your machine and in CI.
### Running from Terminal
TestDriver works with [Vitest's](https://vitest.dev) test runner. Use Vitest to run your tests from the command line to see full output:
```bash theme={null}
# Run all tests
vitest run
# Run a specific test file
vitest run tests/login.test.mjs
# Run in watch mode (re-runs on file changes)
vitest
```
Install Vitest globally for best results: `npm install vitest -g`
Vitest automatically discovers files matching patterns like `*.test.js`, `*.test.mjs`, or `*.spec.js`.
#### Common CLI options
```bash theme={null}
# Run multiple specific files
vitest run login.test.mjs checkout.test.mjs
# Run every test in a folder
vitest run tests/e2e/
# Filter tests by name (supports regex)
vitest run --grep "login"
# Generate a coverage report
vitest run --coverage
```
Coverage requires the `@vitest/coverage-v8` package. Install it with `npm install -D @vitest/coverage-v8`.
#### Vitest UI
For interactive debugging, launch the web-based UI (starts in watch mode):
```bash theme={null}
vitest --ui
```
Open [http://localhost:51204](http://localhost:51204) to browse your test tree, see pass/fail states and timing, view inline console output, and re-run individual tests.
Combine with `--open` to open the UI in your browser automatically: `vitest --ui --open`
### Running from VS Code (GUI Mode)
For a visual testing experience, use the **Vitest extension**:
Click the **beaker icon** in the VS Code sidebar to open the Test Explorer. This shows all your test files and test cases.
Click the **play button** next to any test file or individual test to run it. You can also:
* Run all tests with the "Run All" button
* Debug tests with the "Debug" button
Test results appear inline:
* β Green checkmark for passing tests
* β Red X for failing tests
* Click on a failing test to see error details
VS Code's Test Explorer only shows output for **failing tests**. To see output from passing tests (including screenshots and console logs), run tests from the terminal instead.
### Test Configuration
#### Timeouts
TestDriver tests require longer timeouts than typical unit tests. Your `vitest.config.mjs` should have:
```javascript vitest.config.mjs theme={null}
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
testTimeout: 900000, // 15 minutes
hookTimeout: 900000, // 15 minutes for setup/teardown
},
});
```
#### Environment Variables
Tests use the `TD_API_KEY` environment variable. Set it in your `.env` file:
```env .env theme={null}
TD_API_KEY=your-api-key-here
```
Or pass it when running tests:
```bash theme={null}
TD_API_KEY=your-key vitest run
```
### Parallel Execution
TestDriver runs each test in its own cloud sandbox, enabling true parallel execution. Run your entire test suite in minutes instead of hours.
Your TestDriver plan includes a set number of **license slots** that determine how many tests can run simultaneously. Each running test occupies one slot β when the test completes and the sandbox is destroyed, the slot is immediately freed for the next test.
Set `maxConcurrency` in your Vitest config to match your license slot limit:
```javascript vitest.config.mjs theme={null}
import { defineConfig } from 'vitest/config';
import TestDriver from 'testdriverai/vitest';
export default defineConfig({
test: {
testTimeout: 900000,
hookTimeout: 900000,
maxConcurrency: 5, // Match your license slot limit
reporters: ['default', TestDriver()],
setupFiles: ['testdriverai/vitest/setup'],
},
});
```
You can also cap concurrency from the CLI:
```bash theme={null}
vitest run --maxConcurrency=5
```
Setting `maxConcurrency` higher than your license slots will cause tests to fail when slots are exhausted. Always match this value to your plan's limit.
| Test Suite | Sequential (1 slot) | Parallel (5 slots) | Parallel (10 slots) |
| --------------------- | ------------------- | ------------------ | ------------------- |
| 10 tests @ 2min each | 20 min | 4 min | 2 min |
| 50 tests @ 2min each | 100 min | 20 min | 10 min |
| 100 tests @ 2min each | 200 min | 40 min | 20 min |
View your available slots at [console.testdriver.ai](https://console.testdriver.ai). Upgrade anytime to increase parallelization.
### Viewing Test Reports
After each test run, TestDriver provides a link to the full test report:
```
TESTDRIVER_RUN_URL=https://console.testdriver.ai/runs/abc123
```
The report includes:
* Video recording of the test
* Screenshots at each step
* Network logs and performance metrics
* Console output and errors
Access all your test runs and recordings in the TestDriver console
### Iterating on Tests
When tests fail or need updates, you have two options:
#### Option 1: Ask the AI Assistant (Recommended)
For discovering updated element descriptions or debugging failures, chat with the TestDriver agent through your AI assistant:
```
The login test is failing because the form layout changed.
Update tests/login.test.mjs to work with the new design.
```
The agent will:
1. Start a new session
2. Navigate to the page
3. Analyze the current state
4. Update the test code
This is the best approach when:
* Element text or layout has changed
* You need to see what's currently on screen
* The failure reason isn't obvious from the error message
#### Option 2: Edit the Code Directly
For simple changes, edit the test files directly:
```javascript theme={null}
// Change the element description
const button = await testdriver.find("Submit Order button");
// Add a wait
await testdriver.wait(2000);
// Update assertions
const result = await testdriver.assert("order confirmation shows order ID");
```
This is faster for:
* Updating text strings
* Adjusting timeouts
* Fixing typos
When a test fails because the UI shifted, let TestDriver fix it for you automatically. See [Prevent](/v7/copilot/auto-healing) for auto-healing, and [Debug](/v7/debugging-with-screenshots) for inspecting screenshots when a run goes wrong.
## Next
Now that your tests run anywhere, learn how to make strong assertions that catch real bugs.
# Dashcam
Source: https://docs.testdriver.ai/v7/dashcam
Record test execution with video and logs
## Overview
Dashcam provides automatic video recording and log aggregation for your tests. It captures screen recordings, application logs, and test execution details that can be reviewed later.
## Basic Usage
### With Presets
Most presets automatically include Dashcam:
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from 'testdriverai/presets';
test('my test', async (context) => {
const { testdriver, dashcam } = await chrome(context, {
url: 'https://example.com'
});
// Test executes with recording automatically
await testdriver.find('login button').then(el => el.click());
// Dashcam URL available after test
console.log('Replay:', dashcam.url);
});
```
### Manual Setup
For more control, create a Dashcam instance directly:
```javascript theme={null}
import TestDriver from 'testdriverai';
import Dashcam from 'testdriverai/lib/core/Dashcam.js';
const client = await TestDriver.create({ os: 'linux' });
const dashcam = new Dashcam(client, {
apiKey: process.env.DASHCAM_API_KEY
});
await dashcam.auth();
await dashcam.start();
// Run your tests
const url = await dashcam.stop();
console.log('Replay URL:', url);
```
## Constructor
Create a new Dashcam instance:
```javascript theme={null}
new Dashcam(client, options)
```
### Parameters
TestDriver client instance
Configuration options
Dashcam API key for authentication. Set via `TD_API_KEY` environment variable.
Automatically start recording after authentication
Log configurations to add automatically
## Methods
### auth()
Authenticate with Dashcam service:
```javascript theme={null}
await dashcam.auth(apiKey)
```
Override the API key set in constructor
**Returns:** `Promise`
**Example:**
```javascript theme={null}
await dashcam.auth('your-api-key');
```
### start()
Start recording:
```javascript theme={null}
await dashcam.start()
```
**Returns:** `Promise`
**Example:**
```javascript theme={null}
await dashcam.start();
console.log('Recording started');
```
### stop()
Stop recording and retrieve replay URL:
```javascript theme={null}
await dashcam.stop()
```
**Returns:** `Promise` - Replay URL if available
**Example:**
```javascript theme={null}
const url = await dashcam.stop();
if (url) {
console.log('Watch replay:', url);
} else {
console.log('No replay URL available');
}
```
### addFileLog()
Track a log file in the recording:
```javascript theme={null}
await dashcam.addFileLog(path, name)
```
Path to the log file
Display name for the log in Dashcam
**Returns:** `Promise`
**Example:**
```javascript theme={null}
// Linux/Mac
await dashcam.addFileLog('/tmp/app.log', 'Application Log');
// Windows
await dashcam.addFileLog('C:\\logs\\app.log', 'Application Log');
```
### addApplicationLog()
Track application-specific logs:
```javascript theme={null}
await dashcam.addApplicationLog(application, name)
```
Application name to track
Display name for the log
**Returns:** `Promise`
**Example:**
```javascript theme={null}
await dashcam.addApplicationLog('Google Chrome', 'Browser Logs');
```
### addWebLog()
Track web request logs by URL pattern:
```javascript theme={null}
await dashcam.addWebLog(pattern, name)
```
URL pattern to match (e.g., `"*example.com*"`)
Display name for the log
**Returns:** `Promise`
**Example:**
```javascript theme={null}
await dashcam.addWebLog('*example.com*', 'Web Logs');
```
### addLog()
Generic method to add any type of log:
```javascript theme={null}
await dashcam.addLog(config)
```
Log configuration
Display name for the log
Log type: `'file'`, `'application'`, or `'web'`
File path (required for type='file')
Application name (required for type='application')
URL pattern to match (required for type='web', e.g., `"*example.com*"`)
**Returns:** `Promise`
**Example:**
```javascript theme={null}
await dashcam.addLog({
name: 'Test Output',
type: 'file',
path: '/tmp/test.log'
});
await dashcam.addLog({
name: 'Chrome Logs',
type: 'application',
application: 'Google Chrome'
});
await dashcam.addLog({
name: 'Web Logs',
type: 'web',
pattern: '*example.com*'
});
```
### isRecording()
Check if currently recording:
```javascript theme={null}
await dashcam.isRecording()
```
**Returns:** `Promise` - True if recording is active
**Example:**
```javascript theme={null}
if (await dashcam.isRecording()) {
console.log('Recording in progress');
}
```
## Properties
### recording
Current recording state:
```javascript theme={null}
dashcam.recording // boolean
```
### apiKey
Configured API key:
```javascript theme={null}
dashcam.apiKey // string
```
### client
Associated TestDriver client:
```javascript theme={null}
dashcam.client // TestDriver instance
```
## Complete Examples
### Basic Recording
```javascript theme={null}
import { test } from 'vitest';
import TestDriver from 'testdriverai';
import Dashcam from 'testdriverai/lib/core/Dashcam.js';
test('record test execution', async () => {
const client = await TestDriver.create({ os: 'linux' });
const dashcam = new Dashcam(client);
await dashcam.auth();
await dashcam.start();
// Run your test
await client.find('button').then(el => el.click());
const url = await dashcam.stop();
console.log('Replay:', url);
await client.cleanup();
});
```
### With Log Tracking
```javascript theme={null}
test('record with logs', async () => {
const client = await TestDriver.create({ os: 'linux' });
const dashcam = new Dashcam(client);
await dashcam.auth();
// Add log files before starting
await dashcam.addFileLog('/tmp/testdriver.log', 'TestDriver Log');
await dashcam.addFileLog('/tmp/app.log', 'Application Log');
await dashcam.start();
// Test execution
await client.find('login button').then(el => el.click());
const url = await dashcam.stop();
console.log('Replay with logs:', url);
await client.cleanup();
});
```
### Auto-start Configuration
```javascript theme={null}
test('auto-start recording', async () => {
const client = await TestDriver.create({ os: 'linux' });
const dashcam = new Dashcam(client, {
autoStart: true,
logs: [
{
name: 'App Log',
type: 'file',
path: '/tmp/app.log'
}
]
});
await dashcam.auth(); // Automatically starts recording
// Test execution
await client.find('submit button').then(el => el.click());
const url = await dashcam.stop();
console.log('Replay:', url);
await client.cleanup();
});
```
### Using with Presets
```javascript theme={null}
import { chrome } from 'testdriverai/presets';
test('preset with dashcam', async (context) => {
const { testdriver, dashcam } = await chrome(context, {
url: 'https://example.com',
dashcam: true // Enabled by default
});
// Test runs with automatic recording
await testdriver.find('button').then(el => el.click());
// URL automatically available
console.log('Replay:', dashcam.url);
});
```
### Disabling Dashcam in Presets
```javascript theme={null}
test('without dashcam', async (context) => {
const { testdriver } = await chrome(context, {
url: 'https://example.com',
dashcam: false // Disable recording
});
// Test runs without recording (faster)
await testdriver.find('button').then(el => el.click());
});
```
## Platform Differences
### Windows
On Windows, Dashcam uses PowerShell commands and installs via npm:
```javascript theme={null}
// Windows-specific paths
await dashcam.addFileLog(
'C:\\Users\\testdriver\\Documents\\testdriver.log',
'TestDriver Log'
);
```
### Linux/Mac
On Linux/Mac, Dashcam uses shell commands:
```javascript theme={null}
// Unix-specific paths
await dashcam.addFileLog('/tmp/testdriver.log', 'TestDriver Log');
```
# Debug
Source: https://docs.testdriver.ai/v7/debugging-with-screenshots
Diagnose failing tests with screenshots, replays, and logs
## Overview
When a test fails, debug it by seeing exactly what happened β don't guess. TestDriver captures screenshots, video replays, and logs as your test runs, so you can replay the moment of failure instead of squinting at a stack trace. TestDriver MCP provides powerful commands to view and analyze the screenshots saved during test execution, enabling rapid debugging, test development, and comparison workflows without manually opening image files.
**Automatic Screenshots (Default: Enabled)**: TestDriver automatically captures screenshots before and after every command. Screenshots are named with the line number and action, making it easy to trace exactly which line of code produced each screenshot. For example: `001-click-before-L42-submit-button.png`
## MCP Commands
### list\_local\_screenshots
List and filter screenshots saved in the `.testdriver/screenshots/` directory:
```
list_local_screenshots()
```
**Filter Parameters:**
Filter screenshots by test file or subdirectory (e.g., "login.test", "mcp-screenshots"). If omitted, lists all screenshots.
Filter by exact line number from test file (e.g., 42 matches L42 in filename).
Filter by line number range. Example: `{ start: 10, end: 20 }` matches screenshots from lines 10-20.
Filter by action type: `click`, `find`, `type`, `assert`, `provision`, `scroll`, `hover`, etc.
Filter by phase: `"before"` (state before action) or `"after"` (state after action).
Regex pattern to match against filename. Example: `"login|signin"` or `"button.*click"`.
Filter by exact sequence number.
Filter by sequence range. Example: `{ start: 1, end: 10 }` matches first 10 screenshots.
Maximum number of results to return (default: 50).
Sort results by: `"modified"` (newest first, default), `"sequence"` (execution order), or `"line"` (line number).
**Returns:**
Array of screenshot metadata including:
* `path` - Full absolute path to the screenshot file
* `relativePath` - Path relative to `.testdriver/screenshots/`
* `name` - Screenshot filename
* `sizeBytes` - File size in bytes
* `modified` - Last modification timestamp
* `sequence` - Sequential number (from auto-screenshots)
* `action` - Action type (click, find, etc.)
* `phase` - Before/after phase
* `lineNumber` - Line number from test file
* `description` - Element or action description
**Example Responses:**
```json theme={null}
// Basic listing
[
{
"path": "/Users/user/project/.testdriver/screenshots/login.test/001-click-before-L42-submit-button.png",
"relativePath": "login.test/001-click-before-L42-submit-button.png",
"name": "001-click-before-L42-submit-button.png",
"sizeBytes": 145632,
"modified": "2026-01-23T10:00:00.000Z",
"sequence": 1,
"action": "click",
"phase": "before",
"lineNumber": 42,
"description": "submit-button"
}
]
```
### view\_local\_screenshot
View a specific screenshot from the list:
```
view_local_screenshot({ path: "/full/path/to/screenshot.png" })
```
**Parameters:**
Full absolute path to the screenshot file (as returned by `list_local_screenshots`)
**Returns:**
* Image content (displayed to both AI and user via MCP App)
* Screenshot metadata
* Success/error status
## Common Workflows
### Test Debugging After Failures
When a test fails, you don't have to wonder what went wrong β use powerful filtering to quickly find the screenshots that show exactly what happened:
**1. Find screenshots at the failing line:**
```
// If test failed at line 42
list_local_screenshots({ line: 42 })
// View before and after states at that line
view_local_screenshot({ path: ".testdriver/screenshots/login.test/005-click-before-L42-submit-button.png" })
view_local_screenshot({ path: ".testdriver/screenshots/login.test/006-click-after-L42-submit-button.png" })
```
**2. See what happened leading up to the failure:**
```
// Get screenshots from lines 35-45 to see context
list_local_screenshots({ directory: "login.test", lineRange: { start: 35, end: 45 } })
```
**3. Find all assertion screenshots:**
```
// See what the screen looked like during assertions
list_local_screenshots({ action: "assert" })
```
**4. View the final state before failure:**
```
// Get the last 5 screenshots in execution order
list_local_screenshots({ directory: "login.test", sortBy: "sequence", limit: 5 })
```
### Finding Specific Actions
When debugging element interactions:
```
// Find all click actions
list_local_screenshots({ action: "click" })
// Find what the screen looked like BEFORE each click
list_local_screenshots({ action: "click", phase: "before" })
// Find screenshots related to a specific element using regex
list_local_screenshots({ pattern: "submit|button" })
// Find all type actions (for form filling issues)
list_local_screenshots({ action: "type" })
```
### Understanding Test Flow
View screenshots in execution order to trace test behavior:
```
// Get screenshots in execution order
list_local_screenshots({ directory: "checkout.test", sortBy: "sequence" })
// Get just the first 10 actions
list_local_screenshots({ sequenceRange: { start: 1, end: 10 }, sortBy: "sequence" })
// Get just the last 10 actions
list_local_screenshots({ directory: "checkout.test", sortBy: "sequence", limit: 10 })
```
### Interactive Test Development
While building tests using MCP tools, view screenshots to verify your test logic:
1. **After a test run**, filter screenshots to see specific actions:
```
// See all assertions
list_local_screenshots({ action: "assert" })
// See what happened at a specific line you're debugging
list_local_screenshots({ line: 25 })
```
2. **Review key points** in the test execution:
```
view_local_screenshot({ path: ".testdriver/screenshots/my-test.test/after-login.png" })
```
3. **Verify element locations and states** before adding assertions
4. **Iterate** - adjust your test code based on what you see in the screenshots
### Comparison and Analysis
Compare screenshots to identify issues:
**Using phase filtering for before/after comparison:**
```
// See state before all clicks
list_local_screenshots({ action: "click", phase: "before" })
// See state after all clicks
list_local_screenshots({ action: "click", phase: "after" })
```
**Using line-based debugging:**
```
// Something went wrong around line 50
list_local_screenshots({ lineRange: { start: 45, end: 55 } })
```
**Using regex patterns:**
```
// Find screenshots related to login functionality
list_local_screenshots({ pattern: "login|signin|email|password" })
```
## Best Practices
When saving screenshots in tests, use descriptive names to make them easier to identify:
```javascript theme={null}
await testdriver.screenshot("initial-page-load");
await testdriver.screenshot("after-login-click");
await testdriver.screenshot("dashboard-loaded");
```
Then when listing screenshots, you can quickly identify key moments without viewing every image.
Always call `list_local_screenshots` first to see what's available. The list is sorted by modification time (newest first), making it easy to find recent test runs.
When debugging a specific test, use the `directory` parameter to filter screenshots:
```
list_local_screenshots({ directory: "problematic-test.test" })
```
This avoids clutter from other tests.
When a test fails (especially with assertions), look at screenshots immediately before the failure. They show exactly what the AI or test "saw" at that moment, helping you understand why an assertion failed or why an element wasn't found.
TestDriver test reports include screenshots in the timeline. Use MCP screenshot viewing for interactive debugging during development, and test reports for post-run analysis and team sharing.
Remember that each test run clears its screenshot folder. If you need to preserve screenshots for comparison:
```bash theme={null}
# Copy screenshots before next run
cp -r .testdriver/screenshots/my-test.test .testdriver/screenshots-backup/
```
## Screenshot File Organization
Understanding the directory structure helps with efficient screenshot viewing:
```
.testdriver/
screenshots/
login.test/ # Test file name (without .mjs extension)
001-find-before-L15-email-input.png # Auto: before find() at line 15
002-find-after-L15-email-input.png # Auto: after find() at line 15
003-click-before-L16-email-input.png # Auto: before click() at line 16
004-click-after-L16-email-input.png # Auto: after click() at line 16
login-complete.png # Manual: screenshot("login-complete")
checkout.test/
001-find-before-L12-add-to-cart.png
002-find-after-L12-add-to-cart.png
...
```
### Automatic Screenshot Naming Format
`---L-.png`
| Component | Description | Example |
| ------------- | -------------------------- | -------------------------- |
| `seq` | Sequential number | `001`, `002` |
| `action` | Command name | `click`, `type`, `find` |
| `phase` | Before, after, or error | `before`, `after`, `error` |
| `L` | Line number from test file | `L42` |
| `description` | Element/action description | `submit-button` |
### Key Points
* Each test file gets its own subdirectory
* Automatic screenshots include line numbers for easy tracing
* Manual `screenshot()` calls use custom names you provide
* Folders are cleared at the start of each test run
* All screenshots are PNG format
* Disable automatic screenshots with `autoScreenshots: false` if needed
## Interaction List Sidebar (Source of Truth)
When viewing a test run in the TestDriver console, the **interaction list sidebar** displays a screenshot for each interaction call (find, click, type, assert, etc.). These screenshots show exactly what was on the screen at the time each interaction was executed.
**The sidebar screenshots are the source of truth.** If a test is behaving unexpectedly, check the screenshot attached to the specific interaction in the sidebar β it shows precisely what the AI saw when making its decision. This is more reliable than inferring screen state from test logs or local screenshots alone.
Use the interaction list to:
* **Verify what the AI saw** β confirm the correct page/state was visible when `find()` or `assert()` ran
* **Debug misclicks** β see whether the target element was actually on screen
* **Identify timing issues** β spot cases where the UI hadn't finished loading before an interaction fired
* **Compare runs** β review interaction screenshots across multiple runs to catch flaky behavior
## Integration with Test Development
### During MCP Interactive Development
When using TestDriver MCP tools (`session_start`, `find_and_click`, etc.), screenshots are automatically captured and displayed. Additionally, you can view previously saved screenshots:
```
# After test development session
list_local_screenshots({ directory: "my-new-test.test" })
view_local_screenshot({ path: ".testdriver/screenshots/my-new-test.test/login-page.png" })
```
This helps verify your test logic before running the full test file.
### After Test Runs
When tests fail or behave unexpectedly, replay what happened step by step:
1. **Run the test** with `vitest run tests/my-test.test.mjs`
2. **List screenshots** using `list_local_screenshots`
3. **View relevant screenshots** to diagnose the issue
4. **Update test code** based on what you see
5. **Re-run and verify** the fix
## Troubleshooting
If `list_local_screenshots` returns an empty array:
* Ensure your test includes `await testdriver.screenshot()` calls
* Verify the test actually ran (check test output)
* Check that `.testdriver/screenshots/` directory exists
* Confirm you're in the correct project directory
If `view_local_screenshot` returns an error:
* Verify the path is exactly as returned by `list_local_screenshots`
* Check file permissions - ensure the screenshot file is readable
* Confirm the file hasn't been deleted or moved
If you have hundreds of screenshots making it hard to find what you need, use filtering:
* Filter by test file: `list_local_screenshots({ directory: "my-test.test" })`
* Filter by line number: `list_local_screenshots({ line: 42 })` or `list_local_screenshots({ lineRange: { start: 40, end: 50 } })`
* Filter by action: `list_local_screenshots({ action: "click" })`
* Filter by phase: `list_local_screenshots({ phase: "before" })`
* Use regex: `list_local_screenshots({ pattern: "submit|login" })`
* Limit results: `list_local_screenshots({ limit: 10 })`
* Sort by line: `list_local_screenshots({ sortBy: "line" })`
* Clean up old folders: `rm -rf .testdriver/screenshots/*`
Remember that screenshot folders are cleared at the start of each test run. If you see old screenshots:
* The test may not have run recently
* Or the test failed before reaching the clearing logic
* Manually clear: `rm -rf .testdriver/screenshots//`
## Where this fits in the Guide
Debugging is what you reach for when a [Run](/v7/copilot/running-tests) goes sideways or a [Validate](/v7/making-assertions) assertion fails β the screenshots show you precisely what the AI saw before it acted. Once you've diagnosed the failure, the next step is to stop it from recurring.
## Related
* [screenshot()](/v7/screenshot) - Capture screenshots during test execution
* [Dashcam](/v7/dashcam) - Record full test sessions with video and logs
* [assert()](/v7/assert) - Make AI-powered assertions that benefit from screenshot context
## Next
You've seen what went wrong β now keep it from happening again. Let auto-healing repair flaky tests automatically before they fail your suite.
# doubleClick
Source: https://docs.testdriver.ai/v7/double-click
Perform a double-click action on an element or at specific coordinates
## Overview
The `doubleClick()` method performs a double-click action on an element. You can either call it on an [`Element`](/v7/core-concepts/elements) instance or use it directly with a selector.
## Syntax
```javascript theme={null}
// Double-click on an element
await element.doubleClick();
// Double-click using a selector
await ai.doubleClick('selector');
```
## Parameters
When called on an `Element`, no parameters are required.
When called directly on the AI client:
| Parameter | Type | Description |
| ---------- | -------- | --------------------------------------------------- |
| `selector` | `string` | The selector describing the element to double-click |
## Returns
Returns a `Promise` that resolves when the double-click action completes.
## Examples
### Double-Click on Found Element
```javascript theme={null}
const fileItem = await ai.find('README.md file');
await fileItem.doubleClick();
```
### Direct Double-Click with Selector
```javascript theme={null}
await ai.doubleClick('README.md in the file list');
```
### Opening Files in VS Code
```javascript theme={null}
import { test } from 'vitest';
import { vscode } from '@testdriver/sdk';
test('opens a file by double-clicking', async () => {
const { ai } = await vscode();
// Double-click to open a file in the explorer
await ai.doubleClick('package.json in the file explorer');
// Verify the file opened
const editor = await ai.find('text editor showing package.json');
expect(editor).toBeTruthy();
});
```
### Opening Folders in File Manager
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from '@testdriver/sdk';
test('navigates folders in Google Drive', async () => {
const { ai } = await chrome('https://drive.google.com');
// Double-click to open a folder
await ai.doubleClick('Documents folder');
// Wait for folder to open
await ai.find('breadcrumb showing Documents');
});
```
### Selecting Text with Double-Click
```javascript theme={null}
// Double-click to select a word
await ai.doubleClick('word "TestDriver" in the paragraph');
// Verify selection
const selectedText = await ai.exec('window.getSelection().toString()');
expect(selectedText).toBe('TestDriver');
```
## Related Methods
* [`click()`](/v7/click) - Single click on an element
* [`rightClick()`](/v7/right-click) - Right-click to open context menu
* [`mouseDown()`](/v7/mouse-down) - Press mouse button without releasing
* [`mouseUp()`](/v7/mouse-up) - Release mouse button
* [`hover()`](/v7/hover) - Move mouse over element without clicking
# Element Finding
Source: https://docs.testdriver.ai/v7/elements
Locate and interact with UI elements using AI
## Overview
TestDriver's element finding system uses AI to locate elements on screen using natural language descriptions. The `find()` method returns an `Element` object that you can interact with.
## Finding Elements
### find()
Locate an element on screen using a natural language description.
```javascript theme={null}
const element = await testdriver.find(description)
```
**Parameters:**
* `description` (string) - Natural language description of the element to find
**Returns:** `Promise` - Element instance that has been located
**Example:**
```javascript theme={null}
// Find a button
const submitButton = await testdriver.find('the submit button');
// Find an input field with context
const emailField = await testdriver.find('email input field in the login form');
// Find an element by visual characteristics
const redButton = await testdriver.find('red button in the top right corner');
```
Be specific in your descriptions. Include visual details, location context, or nearby text to improve accuracy.
## Element Class
The `Element` class represents a located (or to-be-located) UI element. It provides methods for interaction and properties for element information.
### Methods
#### found()
Check if the element was successfully located.
```javascript theme={null}
element.found()
```
**Returns:** `boolean` - True if element coordinates were found
**Example:**
```javascript theme={null}
const element = await testdriver.find('login button');
if (element.found()) {
await element.click();
} else {
console.log('Element not found');
}
```
#### find()
Re-locate the element, optionally with a new description.
```javascript theme={null}
await element.find(newDescription)
```
**Parameters:**
* `newDescription` (string, optional) - New description to search for
**Returns:** `Promise` - This element instance
**Example:**
```javascript theme={null}
// Re-locate if the UI changed
const element = await testdriver.find('submit button');
// ... page updates ...
await element.find(); // Re-locate with same description
// Or update the description
await element.find('blue submit button'); // Now looking for blue button
```
#### click()
Click on the element.
```javascript theme={null}
await element.click(action)
```
**Parameters:**
* `action` (string, optional) - Type of click: `'click'` (default), `'double-click'`, `'right-click'`, `'hover'`, `'mouseDown'`, `'mouseUp'`
**Returns:** `Promise`
**Example:**
```javascript theme={null}
const button = await testdriver.find('submit button');
await button.click(); // Regular click
const file = await testdriver.find('document.txt');
await file.click('double-click'); // Double-click
const menu = await testdriver.find('settings icon');
await menu.click('right-click'); // Right-click
```
The element must be found before clicking. The `find()` method automatically locates the element.
#### hover()
Hover over the element without clicking.
```javascript theme={null}
await element.hover()
```
**Returns:** `Promise`
**Example:**
```javascript theme={null}
const tooltip = await testdriver.find('info icon');
await tooltip.hover();
// Wait to see tooltip
await new Promise(resolve => setTimeout(resolve, 1000));
```
#### doubleClick()
Double-click on the element.
```javascript theme={null}
await element.doubleClick()
```
**Returns:** `Promise`
**Example:**
```javascript theme={null}
const file = await testdriver.find('README.txt file icon');
await file.doubleClick();
```
#### rightClick()
Right-click on the element to open context menu.
```javascript theme={null}
await element.rightClick()
```
**Returns:** `Promise`
**Example:**
```javascript theme={null}
const folder = await testdriver.find('Documents folder');
await folder.rightClick();
```
#### mouseDown() / mouseUp()
Press or release mouse button on the element (for drag operations).
```javascript theme={null}
await element.mouseDown()
await element.mouseUp()
```
**Returns:** `Promise`
**Example:**
```javascript theme={null}
// Drag and drop
const item = await testdriver.find('draggable item');
await item.mouseDown();
// Move to drop target (using coordinates or another element)
const target = await testdriver.find('drop zone');
await target.hover();
await target.mouseUp();
```
### Properties
Element properties provide additional information about located elements. Properties are available after a successful `find()` call.
#### coordinates
Get the element's coordinates object containing all position information.
```javascript theme={null}
const coords = element.getCoordinates()
// or access directly
element.coordinates
```
**Returns:** `Object | null` - Coordinate object with `{ x, y, centerX, centerY }`
**Example:**
```javascript theme={null}
const button = await testdriver.find('submit button');
const coords = button.coordinates;
if (coords) {
console.log(`Top-left: (${coords.x}, ${coords.y})`);
console.log(`Center: (${coords.centerX}, ${coords.centerY})`);
}
```
#### x, y, centerX, centerY
Direct access to coordinate values. Always available after successful `find()`.
```javascript theme={null}
element.x // Top-left X coordinate (number)
element.y // Top-left Y coordinate (number)
element.centerX // Center X coordinate (number)
element.centerY // Center Y coordinate (number)
```
**Example:**
```javascript theme={null}
const button = await testdriver.find('submit button');
console.log(`Button at: (${button.x}, ${button.y})`);
console.log(`Button center: (${button.centerX}, ${button.centerY})`);
// Use for custom mouse operations
await testdriver.click(button.centerX, button.centerY);
```
#### width, height
Element dimensions in pixels. Available when AI detects element bounds.
```javascript theme={null}
element.width // Width in pixels (number | null)
element.height // Height in pixels (number | null)
```
**Example:**
```javascript theme={null}
const button = await testdriver.find('submit button');
if (button.width && button.height) {
console.log(`Button size: ${button.width}x${button.height}px`);
// Check if button is large enough
if (button.width < 50) {
console.warn('Button might be too small');
}
}
```
#### boundingBox
Complete bounding box information including position and dimensions.
```javascript theme={null}
element.boundingBox
```
**Returns:** `Object | null` - Bounding box with all dimension data
```typescript theme={null}
{
x: number, // Top-left X
y: number, // Top-left Y
width: number, // Width in pixels
height: number // Height in pixels
}
```
**Example:**
```javascript theme={null}
const element = await testdriver.find('dialog box');
if (element.boundingBox) {
const { x, y, width, height } = element.boundingBox;
console.log(`Dialog: ${width}x${height} at (${x}, ${y})`);
// Calculate if element is in viewport
const rightEdge = x + width;
const bottomEdge = y + height;
console.log(`Element extends to (${rightEdge}, ${bottomEdge})`);
}
```
#### screenshot
Base64-encoded PNG screenshot of the screen when element was found. Only available in DEBUG mode or when an error occurs.
```javascript theme={null}
element.screenshot
```
**Returns:** `string | null` - Base64-encoded PNG image
**Example:**
```javascript theme={null}
const element = await testdriver.find('error message');
if (element.screenshot) {
// Save screenshot to file
const fs = require('fs');
const base64Data = element.screenshot.replace(/^data:image\/\w+;base64,/, '');
fs.writeFileSync('element-screenshot.png', Buffer.from(base64Data, 'base64'));
console.log('Screenshot saved');
}
```
Screenshots can be large. They're automatically excluded from error messages to prevent memory issues.
#### text
Text content extracted from the element by AI (if available).
```javascript theme={null}
element.text
```
**Returns:** `string | null` - Element's text content
**Example:**
```javascript theme={null}
const message = await testdriver.find('notification message');
if (message.text) {
console.log('Message says:', message.text);
// Use text content in assertions
if (message.text.includes('success')) {
console.log('Success message detected');
}
}
// Another example - extracting button label
const button = await testdriver.find('blue button');
console.log('Button text:', button.text); // "Submit"
```
#### label
Accessible label or name of the element (if available). Useful for verifying accessibility.
```javascript theme={null}
element.label
```
**Returns:** `string | null` - Accessible label
**Example:**
```javascript theme={null}
const input = await testdriver.find('first input field');
if (input.label) {
console.log('Input label:', input.label); // "Email Address"
}
```
#### confidence
AI confidence score for the element match (0-1, where 1 is perfect confidence).
```javascript theme={null}
element.confidence
```
**Returns:** `number | null` - Confidence score between 0 and 1
**Example:**
```javascript theme={null}
const element = await testdriver.find('submit button');
if (element.confidence !== null) {
const percentage = (element.confidence * 100).toFixed(1);
console.log(`Match confidence: ${percentage}%`);
if (element.confidence < 0.8) {
console.warn('β οΈ Low confidence match - element might not be correct');
} else if (element.confidence > 0.95) {
console.log('β High confidence match');
}
}
```
Confidence scores below 0.8 may indicate the element description was ambiguous or the wrong element was found.
### Property Availability
| Property | When Available |
| ------------------------------ | ------------------------------------ |
| `x`, `y`, `centerX`, `centerY` | β Always after successful `find()` |
| `coordinates` | β Always after successful `find()` |
| `width`, `height` | β οΈ When AI detects element bounds |
| `boundingBox` | β οΈ When AI detects element bounds |
| `text` | β οΈ When AI extracts text content |
| `label` | β οΈ When element has accessible label |
| `confidence` | β Always after AI element finding |
| `screenshot` | β οΈ Only in DEBUG mode or on errors |
Properties marked with β οΈ may be `null` depending on what the AI could detect from the screenshot.
## JSON Serialization
Element objects can be safely serialized using `JSON.stringify()` for logging, debugging, and data storage. Circular references are automatically removed:
```javascript theme={null}
const element = await testdriver.find('login button');
// Safe to stringify - no circular reference errors!
console.log(JSON.stringify(element, null, 2));
```
**Serialized output includes:**
```json theme={null}
{
"description": "login button",
"coordinates": { "x": 100, "y": 200, "centerX": 150, "centerY": 225 },
"found": true,
"threshold": 0.01,
"x": 100,
"y": 200,
"cache": {
"hit": true,
"strategy": "pixel-diff",
"createdAt": "2025-12-09T10:30:00.000Z",
"diffPercent": 0.0023,
"imageUrl": "https://cache.testdriver.ai/..."
},
"similarity": 0.98,
"confidence": 0.95,
"selector": "button#login",
"aiResponse": "Found the blue login button in the center of the form..."
}
```
**Serialized properties:**
| Property | Type | Description |
| ------------------- | ------- | ------------------------------------------------- |
| `description` | string | Element search description |
| `coordinates` | object | Full coordinate object `{x, y, centerX, centerY}` |
| `found` | boolean | Whether element was located |
| `threshold` | number | Cache threshold used for this find |
| `x`, `y` | number | Top-left coordinates |
| `cache.hit` | boolean | Whether cache was used |
| `cache.strategy` | string | Cache strategy (e.g., "pixel-diff") |
| `cache.createdAt` | string | ISO timestamp when cache was created |
| `cache.diffPercent` | number | Pixel difference from cached image |
| `cache.imageUrl` | string | URL to cached screenshot |
| `similarity` | number | Similarity score (0-1) |
| `confidence` | number | AI confidence score (0-1) |
| `selector` | string | CSS/XPath selector if available |
| `aiResponse` | string | AI's explanation of what it found |
**Use cases:**
```javascript theme={null}
// Debugging element detection
const element = await testdriver.find('submit button');
if (!element.found()) {
console.error('Element not found:', JSON.stringify(element, null, 2));
}
// Logging cache performance
const data = JSON.parse(JSON.stringify(element));
if (data.cache.hit) {
console.log(`Cache hit! Diff: ${(data.cache.diffPercent * 100).toFixed(2)}%`);
}
// Sharing element data across processes
const elementData = JSON.stringify(element);
// Send to another process, log to file, etc.
```
Use JSON serialization when you need to log element data or when debugging why an element wasn't found. The serialized output excludes large binary data (screenshots) and circular references.
## Examples
### Basic Element Interaction
```javascript theme={null}
// Find and click
const submitButton = await testdriver.find('submit button');
await submitButton.click();
// Find, verify, then interact
const emailInput = await testdriver.find('email input field');
if (emailInput.found()) {
await emailInput.click();
await testdriver.type('user@example.com');
}
```
### Working with Forms
```javascript theme={null}
// Fill out a multi-field form
const nameField = await testdriver.find('name input field');
await nameField.click();
await testdriver.type('John Doe');
const emailField = await testdriver.find('email input field');
await emailField.click();
await testdriver.type('john@example.com');
const submitButton = await testdriver.find('submit button');
await submitButton.click();
```
### Conditional Interactions
```javascript theme={null}
// Check if element exists before interacting
const closeButton = await testdriver.find('close popup button');
if (closeButton.found()) {
await closeButton.click();
console.log('Popup closed');
} else {
console.log('No popup to close');
}
```
### Re-locating Dynamic Elements
```javascript theme={null}
// Element that moves or changes
const notification = await testdriver.find('success notification');
// Do something that might cause it to move
await testdriver.scroll('down', 300);
// Re-locate the element
await notification.find();
if (notification.found()) {
await notification.click();
}
```
## Best Practices
Include visual details, position context, and nearby text:
```javascript theme={null}
// β Too vague
await testdriver.find('button');
// β Specific
await testdriver.find('blue submit button below the email field');
```
Always verify elements were located before interacting:
```javascript theme={null}
const element = await testdriver.find('submit button');
if (!element.found()) {
throw new Error('Submit button not found');
}
await element.click();
```
If you need to interact with the same element multiple times, reuse the reference:
```javascript theme={null}
const input = await testdriver.find('search input');
await input.click();
await testdriver.type('first search');
await testdriver.pressKeys(['enter']);
// Re-use the same element reference
await input.click();
await testdriver.pressKeys(['ctrl', 'a']); // Select all
await testdriver.type('second search');
```
# Assert Test Example
Source: https://docs.testdriver.ai/v7/examples/assert
Example test that uses assert to verify a page loads.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="assert.test.mjs" {22-24} theme={null}
/**
* TestDriver SDK - Assert Test (Vitest)
* Converted from: testdriver/acceptance/assert.yaml
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Assert Test", () => {
it("should assert the testdriver login page shows", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP,
});
// provision.chrome() automatically calls ready() and starts dashcam
await testdriver.provision.chrome({
url: 'http://testdriver-sandbox.vercel.app/login',
});
// Take a screenshot
await testdriver.screenshot();
// Assert the TestDriver.ai Sandbox login page is displayed
const result = await testdriver.assert(
"the TestDriver.ai Sandbox login page is displayed",
);
expect(result).toBeTruthy();
});
// it("should assert the testdriver login page shows 2", async (context) => {
// const testdriver = TestDriver(context);
// // provision.chrome() automatically calls ready() and starts dashcam
// await testdriver.provision.chrome({
// url: 'http://testdriver-sandbox.vercel.app/login',
// });
// // Assert the TestDriver.ai Sandbox login page is displayed
// const result = await testdriver.assert(
// "the TestDriver.ai Sandbox login page is displayed",
// );
// expect(result).toBeTruthy();
// });
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/assert.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Chrome Extension Test Example
Source: https://docs.testdriver.ai/v7/examples/chrome-extension
Example test showing how to load Chrome extensions from local paths or the Chrome Web Store.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="chrome-extension.test.mjs" {31-33} theme={null}
/**
* TestDriver SDK - Chrome Extension Test (Vitest)
* Tests loading a Chrome extension using provision.chromeExtension()
*
* This test suite covers:
* 1. Loading extension from local path (extensionPath)
* 2. Loading extension from Chrome Web Store (extensionId)
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Chrome Extension Test", () => {
it("should load hello-world Chrome extension from local path", async (context) => {
console.log('connecting to', process.env.TD_IP)
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, cacheKey: new Date().getTime().toString() });
// Determine OS-specific paths and commands
const shell = testdriver.os === 'windows' ? 'pwsh' : 'sh';
const extensionsDir = testdriver.os === 'windows'
? 'C:\\Users\\testdriver\\Downloads\\chrome-extensions-samples'
: '/tmp/chrome-extensions-samples';
const extensionPath = testdriver.os === 'windows'
? `${extensionsDir}\\functional-samples\\tutorial.hello-world`
: `${extensionsDir}/functional-samples/tutorial.hello-world`;
// Clone the Chrome extensions samples repo
const cloneCmd = testdriver.os === 'windows'
? `git clone --depth 1 https://github.com/GoogleChrome/chrome-extensions-samples.git "${extensionsDir}"`
: `git clone --depth 1 https://github.com/GoogleChrome/chrome-extensions-samples.git ${extensionsDir}`;
await testdriver.exec(shell, cloneCmd, 60000, true);
// Launch Chrome with the hello-world extension loaded
await testdriver.provision.chromeExtension({
extensionPath: extensionPath
});
// Navigate to testdriver.ai (extensions don't load on New Tab)
const addressBar = await testdriver.find("Chrome address bar");
await addressBar.click();
await testdriver.type("testdriver.ai");
await testdriver.pressKeys(["enter"]);
// Wait for page to load
const pageResult = await testdriver.assert("I can see testdriver.ai");
expect(pageResult).toBeTruthy();
// The hello-world extension adds a puzzle piece icon to the toolbar
// When clicked, it shows a popup with "Hello Extensions"
// Retry opening the extension popup and verifying it up to 3 times
let popupResult;
let lastError;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
// Click on the extensions button (puzzle piece icon) in Chrome toolbar
const extensionsButton = await testdriver.find("The extensions button in the Chrome toolbar", {zoom: true, verify: true, timeout: 10000});
await extensionsButton.click();
// Look for the hello world extension in the extensions menu
const helloExtension = await testdriver.find("Hello Extensions extension in the extensions dropdown", {zoom: true, verify: true, timeout: 10000});
await helloExtension.click();
await testdriver.wait(2000); // wait for the popup to open
// Verify the extension popup shows "Hello Extensions" text
popupResult = await testdriver.assert("a popup shows with the text 'Hello Extensions'");
if (popupResult) break;
} catch (err) {
lastError = err;
console.log(`Attempt ${attempt} failed:`, err.message);
}
if (attempt < 3) {
// Dismiss any open popup/menu before retrying
await testdriver.pressKeys(["escape"]);
await testdriver.wait(1000);
}
}
if (!popupResult && lastError) throw lastError;
expect(popupResult).toBeTruthy();
});
// it("should load Loom from Chrome Web Store by extensionId", async (context) => {
// const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP});
// // Launch Chrome with Loom loaded by its Chrome Web Store ID
// // Loom ID: liecbddmkiiihnedobmlmillhodjkdmb
// await testdriver.provision.chromeExtension({
// extensionId: 'liecbddmkiiihnedobmlmillhodjkdmb'
// });
// // Navigate to testdriver.ai (extensions don't load on New Tab)
// const addressBar = await testdriver.find("Chrome address bar");
// await addressBar.click();
// await testdriver.type("testdriver.ai");
// await testdriver.pressKeys(["enter"]);
// // Wait for page to load
// const pageResult = await testdriver.assert("I can see testdriver.ai");
// expect(pageResult).toBeTruthy();
// // Click on the extensions button (puzzle piece icon) in Chrome toolbar
// const extensionsButton = await testdriver.find("The puzzle-shaped icon in the Chrome toolbar.", {zoom: true});
// await extensionsButton.click();
// // Look for Loom in the extensions menu
// const loomExtension = await testdriver.find("Loom extension in the extensions dropdown");
// expect(loomExtension.found()).toBeTruthy();
// });
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/chrome-extension.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Element Not Found Test
Source: https://docs.testdriver.ai/v7/examples/element-not-found
Example test showing how to gracefully handle elements that don't exist on the page.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="element-not-found.test.mjs" {16-18} theme={null}
/**
* TestDriver SDK - Element Not Found Test
* Tests that finding a non-existent element returns properly without timing out
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Element Not Found Test", () => {
it("should handle non-existent element gracefully without timing out", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, headless: true });
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
//
// Try to find an element that definitely doesn't exist
const element = await testdriver.find(
"a purple unicorn dancing on the moon",
);
// Should return an element that is not found
expect(element.found()).toBe(false);
expect(element.coordinates).toBeNull();
}); // 90 second timeout for the test (should complete much faster)
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/element-not-found.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# FindAll Test Example
Source: https://docs.testdriver.ai/v7/examples/findall-coffee-icons
TestDriver SDK - FindAll Coffee Icons Test Loads a random icon grid and uses findAll() to locate and click all 4 coffee cup icons.
## Overview
TestDriver SDK - FindAll Coffee Icons Test Loads a random icon grid and uses findAll() to locate and click all 4 coffee cup icons
Review the source code below to understand the implementation details and patterns used.
## Live Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="findall-coffee-icons.test.mjs" theme={null}
/**
* TestDriver SDK - FindAll Coffee Icons Test
* Loads a random icon grid and uses findAll() to locate and click all 4 coffee cup icons
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("FindAll Coffee Icons", () => {
it("should find and click all 4 coffee cup icons", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP,
headless: true,
});
await testdriver.provision.chrome({
url: "https://v0-random-icon-grid.vercel.app/",
});
// Use findAll to locate all coffee cup icons on the page
const coffeeIcons = await testdriver.findAll("coffee cup icon, there are exactly 4 on the page");
// Log each icon's coordinates
console.log(`Found ${coffeeIcons.length} coffee icons:`);
coffeeIcons.forEach((icon, i) => {
console.log(` Icon ${i + 1}: (${icon.x}, ${icon.y}) center=(${icon.centerX}, ${icon.centerY})`);
});
// Verify we found 3 or 4 coffee icons
expect(coffeeIcons.length).toBeGreaterThanOrEqual(3);
expect(coffeeIcons.length).toBeLessThanOrEqual(4);
// Click each coffee cup icon
for (const icon of coffeeIcons) {
await icon.click();
}
// Verify the selection count is displayed
await testdriver.assert("the page says 'Selected: 3 / 4' or 'Matched 4 of a kind!'");
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/findall-coffee-icons.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Clicking Image Example
Source: https://docs.testdriver.ai/v7/examples/hover-image
Example test that finds and clicks on an image element using AI-powered visual recognition.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="hover-image.test.mjs" {40-42,47} theme={null}
/**
* TestDriver SDK - Hover Image Test (Vitest)
* Converted from: testdriver/acceptance/hover-image.yaml
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
/**
* Perform login flow for SauceLabs demo app
* @param {import('../../sdk.js').default} client - TestDriver client instance
* @param {string} username - Username (default: 'standard_user')
*/
async function performLogin(client, username = "standard_user") {
await client.focusApplication("Google Chrome");
const password = await client.extract("the password");
const usernameField = await client.find(
"username input",
);
await usernameField.click();
await client.type(username);
await client.pressKeys(["tab"]);
await client.type(password, { secret: true });
await client.pressKeys(["tab"]);
await client.pressKeys(["enter"]);
}
describe("Hover Image Test", () => {
it("should click on shopping cart icon and verify empty cart", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP});
// provision.chrome() automatically calls ready() and starts dashcam
await testdriver.provision.chrome({
url: 'http://testdriver-sandbox.vercel.app/login'
});
// Perform login first
await performLogin(testdriver);
// Click on the shopping cart icon
await testdriver.focusApplication("Google Chrome");
const cartIcon = await testdriver.find(
"shopping cart icon next to the Cart text in the top right corner",
);
await cartIcon.click();
// Assert that you see an empty shopping cart
const result = await testdriver.assert("Your cart is empty");
expect(result).toBeTruthy();
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/hover-image.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Clicking on Text Example
Source: https://docs.testdriver.ai/v7/examples/hover-text
Example test that finds and clicks on text elements using natural language descriptions.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="hover-text.test.mjs" {13-15,18-20} theme={null}
/**
* TestDriver SDK - Hover Text Test (Vitest)
* Converted from: testdriver/acceptance/hover-text.yaml
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Hover Text Test", () => {
it("should click Sign In and verify error message", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP});
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
// Click on Sign In button using new find() API
const signInButton = await testdriver.find(
"Sign In, black button below the password field",
);
await signInButton.click();
// Assert that an error shows that fields are required
const result = await testdriver.assert(
"an error shows that fields are required",
);
expect(result).toBeTruthy();
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/hover-text.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Hover Text With Description Test Example
Source: https://docs.testdriver.ai/v7/examples/hover-text-with-description
TestDriver SDK - Hover Text With Description Test (Vitest) Converted from: testdriver/acceptance/hover-text-with-description.yaml.
## Overview
TestDriver SDK - Hover Text With Description Test (Vitest) Converted from: testdriver/acceptance/hover-text-with-description.yaml
Review the source code below to understand the implementation details and patterns used.
## Live Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="hover-text-with-description.test.mjs" theme={null}
/**
* TestDriver SDK - Hover Text With Description Test (Vitest)
* Converted from: testdriver/acceptance/hover-text-with-description.yaml
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
/**
* Perform login flow for SauceLabs demo app
* @param {TestDriver} client - TestDriver client
* @param {string} username - Username (default: 'standard_user')
*/
async function performLogin(client, username = "standard_user") {
await client.focusApplication("Google Chrome");
const password = await client.extract("the password");
const usernameField = await client.find("username input");
await usernameField.click();
await client.type(username);
await client.pressKeys(["tab"]);
await client.type(password, { secret: true });
await client.pressKeys(["tab"]);
await client.pressKeys(["enter"]);
}
describe("Hover Text With Description Test", () => {
it("should add TestDriver Hat to cart and verify", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP,
headless: true,
});
await testdriver.provision.chrome({
url: "http://testdriver-sandbox.vercel.app/login",
});
//
// Perform login first
await performLogin(testdriver);
// Click on "Add to Cart" under TestDriver Hat
const addToCartButton = await testdriver.find(
"Add to Cart, add to cart button under TestDriver Hat",
);
await addToCartButton.click();
// Click on the cart
const cartButton = await testdriver.find(
"Cart, cart button in the top right corner",
);
await cartButton.click();
// Assert the TestDriver Hat is in the cart
const result = await testdriver.assert("There is an item in the cart");
expect(result).toBeTruthy();
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/hover-text-with-description.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Linux App Installation Test Example
Source: https://docs.testdriver.ai/v7/examples/installer
Example test demonstrating how to download and install .deb packages and shell scripts on Linux.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="installer.test.mjs" {16-18} theme={null}
/**
* TestDriver SDK - Installer Test (Vitest)
* Tests the provision.installer() method for downloading and installing apps
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
const isLinux = (process.env.TD_OS || "linux") === "linux";
describe("Provision Installer", () => {
it.skipIf(!isLinux)(
"should download and install a .deb package on Linux",
async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP});
// Install bat (a cat clone with syntax highlighting) using provision.installer
const filePath = await testdriver.provision.installer({
url: 'https://github.com/sharkdp/bat/releases/download/v0.24.0/bat_0.24.0_amd64.deb',
});
// Verify the file was downloaded
expect(filePath).toContain('bat');
// Verify bat was installed by running it
await testdriver.exec('sh', 'bat --version', 10000);
},
);
it.skipIf(!isLinux)(
"should download a shell script and verify it exists",
async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP});
// Download a shell script (nvm installer)
const filePath = await testdriver.provision.installer({
url: 'https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh',
launch: false, // Don't auto-run the script
});
// Verify the file was downloaded
expect(filePath).toContain('install.sh');
// Verify the file is executable
const result = await testdriver.exec('sh', `ls -la "${filePath}"`, 10000);
expect(result).toBeTruthy();
},
);
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/installer.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Launching VS Code Test Example
Source: https://docs.testdriver.ai/v7/examples/launch-vscode-linux
Example test demonstrating how to launch VS Code and install extensions on Linux.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="launch-vscode-linux.test.mjs" {13,27-29} theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
const isLinux = (process.env.TD_OS || "linux") === "linux";
describe("Launch VS Code on Linux", () => {
it.skipIf(!isLinux)(
"should launch VS Code on Debian/Ubuntu",
async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP});
// provision.vscode() automatically calls ready() and starts dashcam
await testdriver.provision.vscode();
// Wait for VS Code to launch (polls every 5s until found or timeout)
const vsCodeWindow = await testdriver.find(
"Visual Studio Code window",
{ timeout: 60000 }
);
expect(vsCodeWindow.found()).toBeTruthy();
},
);
// it.skipIf(!isLinux)(
// "should install and use a VS Code extension",
// async (context) => {
// const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP});
// // Launch VS Code with the Prettier extension installed
// await testdriver.provision.vscode({
// extensions: ["esbenp.prettier-vscode"],
// });
// const vsCodeWindow = await testdriver.find(
// "Visual Studio Code window",
// { timeout: 60000 }
// );
// expect(vsCodeWindow.found()).toBeTruthy();
// // Open the extensions panel to verify Prettier is installed
// await testdriver.pressKeys(["ctrl", "shift", "x"]);
// // Wait for extensions panel to open
// await new Promise((resolve) => setTimeout(resolve, 2000));
// // Assert that Prettier extension is visible in the installed extensions
// const prettierVisible = await testdriver.assert(
// "Prettier extension is visible in the extensions panel or sidebar",
// );
// expect(prettierVisible).toBeTruthy();
// },
// );
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/launch-vscode-linux.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Parse Test Example
Source: https://docs.testdriver.ai/v7/examples/parse
TestDriver SDK - Parse Test (Vitest) Opens Airbnb and runs the .parse() SDK command to analyze the screen.
## Overview
TestDriver SDK - Parse Test (Vitest) Opens Airbnb and runs the .parse() SDK command to analyze the screen.
Review the source code below to understand the implementation details and patterns used.
## Live Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="parse.test.mjs" theme={null}
/**
* TestDriver SDK - Parse Test (Vitest)
* Opens Airbnb and runs the .parse() SDK command to analyze the screen.
*/
import { describe, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Parse Test", () => {
it("should open Airbnb and parse the screen", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP});
await testdriver.provision.chrome({ url: "https://www.airbnb.com" });
// The SDK automatically outputs elements as a formatted table
const result = await testdriver.parse();
console.log(`Found ${result.elements?.length || 0} elements`);
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/parse.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Pressing Keys Test Example
Source: https://docs.testdriver.ai/v7/examples/press-keys
Example test demonstrating keyboard shortcuts and key combinations for browser navigation.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="press-keys.test.mjs" {17,24,36} theme={null}
/**
* TestDriver SDK - Press Keys Test (Vitest)
* Converted from: testdriver/acceptance/press-keys.yaml
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Press Keys Test", () => {
it("should create tabs and navigate using keyboard shortcuts", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, headless: true });
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
const signInButton = await testdriver.find(
"Sign In, black button below the password field",
);
await signInButton.click();
// Open new tab
await testdriver.pressKeys(["ctrl", "t"]);
// Poll for "Learn more" to appear
let imagesLink = await testdriver.find("Images", {timeout: 5000});
expect(imagesLink.found()).toBeTruthy();
// Open DevTools
await testdriver.pressKeys(["ctrl", "shift", "i"]);
// Poll for "Elements" to appear
let elements = await testdriver.find("Elements", {timeout: 5000});
expect(elements.found()).toBeTruthy();
// Open another tab and navigate
await testdriver.pressKeys(["ctrl", "t"]);
await testdriver.type("google.com");
await testdriver.pressKeys(["enter"]);
// Assert Google appears
const result = await testdriver.assert("google appears");
expect(result).toBeTruthy();
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/press-keys.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Scrolling Test Example
Source: https://docs.testdriver.ai/v7/examples/scroll
Example test demonstrating page scrolling with configurable direction and scroll amount.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="scroll.test.mjs" {29} theme={null}
/**
* TestDriver SDK - Scroll Test (Vitest)
* Converted from: testdriver/acceptance/scroll.yaml
*
* UPDATED: Now using chrome preset for automatic setup
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Scroll Test", () => {
it("should navigate and scroll down the page", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, headless: true });
await testdriver.provision.chrome({ url: 'https://www.webhamster.com/' });
// Wait for page to load and click heading
const heading = await testdriver.find(
"The Hamster Dance, large heading at top of page",
);
await heading.click();
// Scroll down
await testdriver.scroll("down", { amount: 1000 });
// Assert page is scrolled
const result = await testdriver.assert("The text 'The Hamster Dance' is not visible on the webpage content. It's ok if it's visible in the tab title.");
expect(result).toBeTruthy();
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/scroll.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Scrolling with Keyboard Test Example
Source: https://docs.testdriver.ai/v7/examples/scroll-keyboard
Example test demonstrating page scrolling using keyboard-based scroll controls.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="scroll-keyboard.test.mjs" {26} theme={null}
/**
* TestDriver SDK - Scroll Keyboard Test (Vitest)
* Converted from: testdriver/acceptance/scroll-keyboard.yaml
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Scroll Keyboard Test", () => {
it("should navigate to webhamster.com and scroll with keyboard", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, headless: true });
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
//
// Navigate to https://www.webhamster.com/
await testdriver.focusApplication("Google Chrome");
const urlBar = await testdriver.find(
"the URL in the omnibox", {zoom: true}
);
await urlBar.click();
await testdriver.pressKeys(["ctrl", "a"]);
await testdriver.type("https://www.webhamster.com/");
await testdriver.pressKeys(["enter"]);
// Scroll down with keyboard 1000 pixels
const heading = await testdriver.find(
"The Hamster Dance, large heading at top of page",
);
await heading.click();
await testdriver.scroll("down", { amount: 1000 });
// Assert the page is scrolled down
const result = await testdriver.assert("The text 'The Hamster Dance' is not visible on the webpage content. It's ok if it's visible in the tab title.");
expect(result).toBeTruthy();
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/scroll-keyboard.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# Typing Test Example
Source: https://docs.testdriver.ai/v7/examples/type
Example test demonstrating how to type text into input fields and verify the content.
## Demo Test Run
Watch this test execute in a real sandbox environment:
## Source Code
```javascript title="type.test.mjs" {13} theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Type Test", () => {
it("should enter standard_user in username field", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, headless: true });
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
//
const usernameField = await testdriver.find(
"Username, input field for username",
);
await usernameField.click();
await testdriver.type("standard_user");
const result = await testdriver.assert(
'the username field contains "standard_user"',
);
expect(result).toBeTruthy();
});
it("should show validation message when clicking Sign In without password", async (context) => {
const testdriver = TestDriver(context, { ip: context.ip || process.env.TD_IP, headless: true });
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
// First fill in username
const usernameField = await testdriver.find(
"Username, input field for username",
);
await usernameField.click();
await testdriver.type("standard_user");
//
const signInButton = await testdriver.find(
"Sign in, black button below the password field",
);
await signInButton.click();
await testdriver.focusApplication("Google Chrome");
const result = await testdriver.assert(
"Please fill out this field is visible near the password field",
);
expect(result).toBeTruthy();
});
});
```
## Running This Example
```bash theme={null}
# Clone the TestDriver repository
git clone https://github.com/testdriverai/testdriverai
# Install dependencies
cd testdriverai
npm install
# Run this specific example
npx vitest run examples/type.test.mjs
```
Make sure you have `TD_API_KEY` set in your environment. Get one at [testdriver.ai](https://testdriver.ai).
# exec()
Source: https://docs.testdriver.ai/v7/exec
Execute shell or PowerShell commands in the sandbox
## Overview
Execute shell commands (Linux) or PowerShell commands (Windows) in the sandbox environment.
## Syntax
```javascript theme={null}
await testdriver.exec(language, code, timeout, silent)
```
## Parameters
Language to execute: `'sh'` (Shell/Linux) or `'pwsh'` (PowerShell/Windows)
Code or command to execute
Timeout in milliseconds
Suppress output if `true`
## Returns
`Promise` - Command output
## Shell Execution (Linux)
Execute shell commands in the Linux sandbox.
### Basic Commands
```javascript theme={null}
// List files
const files = await testdriver.exec('sh', 'ls -la', 5000);
// Check current directory
const pwd = await testdriver.exec('sh', 'pwd', 5000);
// Run a script
await testdriver.exec('sh', './setup.sh', 60000);
```
### File Operations
```javascript theme={null}
// Create a file
await testdriver.exec('sh', 'echo "Hello World" > test.txt', 5000);
// Read a file
const content = await testdriver.exec('sh', 'cat test.txt', 5000);
// Copy files
await testdriver.exec('sh', 'cp source.txt dest.txt', 5000);
// Delete files
await testdriver.exec('sh', 'rm test.txt', 5000);
```
## PowerShell Execution (Windows)
Execute PowerShell commands in the Windows sandbox.
### Software Installation
```javascript theme={null}
// Install npm package globally
await testdriver.exec('pwsh', 'npm install -g http-server', 30000);
// Install via Chocolatey
await testdriver.exec('pwsh', 'choco install firefox -y', 60000);
// Download and run installer
await testdriver.exec('pwsh', `
Invoke-WebRequest -Uri "https://example.com/setup.exe" -OutFile "C:\\setup.exe"
Start-Process -FilePath "C:\\setup.exe" -ArgumentList "/S" -Wait
`, 120000);
```
### File Operations
```javascript theme={null}
// Create a file
await testdriver.exec('pwsh', `
Set-Content -Path "C:\\test.txt" -Value "Hello World"
`, 5000);
// Read a file
const content = await testdriver.exec('pwsh',
'Get-Content -Path "C:\\test.txt"',
5000
);
// Copy files
await testdriver.exec('pwsh',
'Copy-Item -Path "C:\\source.txt" -Destination "C:\\dest.txt"',
5000
);
// Delete files
await testdriver.exec('pwsh',
'Remove-Item -Path "C:\\test.txt"',
5000
);
// List directory
const files = await testdriver.exec('pwsh',
'Get-ChildItem -Path "C:\\Users\\testdriver\\Documents"',
5000
);
```
### Process Management
```javascript theme={null}
// List running processes
const processes = await testdriver.exec('pwsh', 'Get-Process', 5000);
// Start application
await testdriver.exec('pwsh', `
Start-Process "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" -ArgumentList "--start-maximized", "https://example.com"
`, 5000);
// Kill a process
await testdriver.exec('pwsh', 'Stop-Process -Name "chrome" -Force', 5000);
// Wait for process
await testdriver.exec('pwsh', 'Start-Process notepad -Wait', 30000);
```
### Environment Variables
```javascript theme={null}
// Set environment variable (session)
await testdriver.exec('pwsh', '$env:MY_VAR = "value"', 5000);
// Get environment variable
const value = await testdriver.exec('pwsh', '$env:MY_VAR', 5000);
// Set persistent environment variable
await testdriver.exec('pwsh', `
[Environment]::SetEnvironmentVariable("MY_VAR", "value", "User")
`, 5000);
```
### Network Operations
```javascript theme={null}
// Test connectivity
const pingResult = await testdriver.exec('pwsh',
'Test-NetConnection google.com',
10000
);
// Download file
await testdriver.exec('pwsh', `
Invoke-WebRequest -Uri "https://example.com/file.zip" -OutFile "C:\\Downloads\\file.zip"
`, 30000);
// Check if port is open
const portOpen = await testdriver.exec('pwsh',
'Test-NetConnection -ComputerName localhost -Port 3000',
5000
);
```
## Silent Execution
Suppress output for background operations:
```javascript theme={null}
// Silent installation
await testdriver.exec('pwsh', 'npm install -g some-package', 30000, true);
// Start background process
await testdriver.exec('pwsh', 'Start-Process notepad', 5000, true);
// Run setup script silently
await testdriver.exec('pwsh', '.\\setup.ps1', 60000, true);
```
## Best Practices
**Use appropriate timeouts**
```javascript theme={null}
// Quick operations: 5000ms
await testdriver.exec('sh', 'ls -la', 5000);
// Installations: 30000-60000ms
await testdriver.exec('pwsh', 'npm install -g package', 30000);
// Downloads or complex operations: 60000-120000ms
await testdriver.exec('pwsh', 'Install-Module Something', 120000);
```
**Handle errors gracefully**
```javascript theme={null}
try {
await testdriver.exec('pwsh', 'Some-Command', 5000);
} catch (error) {
console.error('Command failed:', error.message);
// Fallback or retry logic
}
```
**Use silent mode for background tasks**
```javascript theme={null}
// Silent install
await testdriver.exec('pwsh', 'npm install -g tool', 30000, true);
// Background service
await testdriver.exec('pwsh', 'Start-Service MyService', 5000, true);
```
**Escape strings properly in PowerShell**
Use proper escaping for special characters:
```javascript theme={null}
// Use backticks for newlines
await testdriver.exec('pwsh', `
Write-Host "Line 1\`nLine 2"
`, 5000);
// Use single quotes to avoid variable expansion
await testdriver.exec('pwsh',
"Write-Host 'Text with $special chars'",
5000
);
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it } from 'vitest';
import TestDriver from 'testdriverai';
describe('Code Execution', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should execute shell commands on Linux', async () => {
// List directory
const files = await testdriver.exec('sh', 'ls -la', 5000);
console.log('Files:', files);
// Create a file
await testdriver.exec('sh', 'echo "Hello World" > test.txt', 5000);
// Read the file
const content = await testdriver.exec('sh', 'cat test.txt', 5000);
expect(content).toContain('Hello World');
});
it('should install and use tools on Windows', async () => {
// Install tool
await testdriver.exec('pwsh', 'npm install -g http-server', 30000, true);
// Create HTML file
await testdriver.exec('pwsh', `
Set-Content -Path "C:\\index.html" -Value "
Test Page
"
`, 5000);
// Start server in background
await testdriver.exec('pwsh', `
Start-Process pwsh -ArgumentList "-Command", "http-server C:\\ -p 8080"
`, 5000, true);
await new Promise(r => setTimeout(r, 3000));
// Launch browser
await testdriver.exec('pwsh', `
Start-Process chrome -ArgumentList "http://localhost:8080"
`, 5000);
await testdriver.focusApplication('Google Chrome');
});
});
```
## Related Methods
* [`focusApplication()`](/v7/focus-application) - Focus apps before exec
* [`find()`](/v7/find) - Locate elements visually
* [`type()`](/v7/type) - Type text into inputs
# extract()
Source: https://docs.testdriver.ai/v7/extract
Read information from the screen using AI and return it as a string
## Overview
Extract information from the current screen using AI and return it as a string. Describe what you want in natural language, and the AI reads the screen and returns the matching value β text, numbers, labels, status messages, or any other on-screen content.
Unlike [`assert()`](/v7/assert), which returns a boolean verdict, `extract()` returns the actual value so you can store it, compare it, or feed it into later steps and framework assertions.
## Syntax
```javascript theme={null}
const value = await testdriver.extract(description)
const value = await testdriver.extract({ description })
```
## Parameters
Natural language description of the information to read from the screen.
`extract()` also accepts an options object β `extract({ description })` β which is equivalent to the positional form. The bare string form is the most common.
## Returns
`Promise` β The information read from the screen. Returns the extracted value as text; parse or cast it yourself if you need a number or other type.
## Examples
### Basic Extraction
```javascript theme={null}
// Read text content
const title = await testdriver.extract('the page title');
const heading = await testdriver.extract('the main heading text');
// Read numbers and prices
const price = await testdriver.extract('the total price shown in the cart');
const count = await testdriver.extract('the number of items in the list');
// Read status and confirmation values
const status = await testdriver.extract('the order status');
const orderNumber = await testdriver.extract('the order confirmation number');
```
### Using the Extracted Value
```javascript theme={null}
// Store and reuse across steps
const orderNumber = await testdriver.extract('the order confirmation number');
console.log('Order:', orderNumber);
// Combine with framework assertions
import { expect } from 'vitest';
const message = await testdriver.extract('the success message text');
expect(message).toContain('successfully');
// Cast to a number when you need to compare
const totalText = await testdriver.extract('the cart total as a number without currency symbol');
expect(Number(totalText)).toBeGreaterThan(0);
```
## Best Practices
**Be specific about what to read**
Precise descriptions produce cleaner values:
```javascript theme={null}
// β Too vague β may return extra surrounding text
const price = await testdriver.extract('price');
// β Specific β targets a single value
const price = await testdriver.extract('the total price in the order summary, digits only');
```
**Ask for the format you want**
Steer the output by describing the desired shape in the prompt:
```javascript theme={null}
// Strip currency symbols
const total = await testdriver.extract('the order total as a number without the dollar sign');
// Isolate a single field
const email = await testdriver.extract('the email address shown in the profile header');
```
**Extract for detailed assertions**
Use `extract()` when a boolean [`assert()`](/v7/assert) isn't enough and you need the actual value to inspect:
```javascript theme={null}
const confirmation = await testdriver.extract('the confirmation number');
expect(confirmation).toMatch(/^ORD-\d{6}$/);
```
## Use Cases
```javascript theme={null}
const submitBtn = await testdriver.find('place order button');
await submitBtn.click();
// Read the confirmation the app generated
const orderNumber = await testdriver.extract('the order confirmation number');
const eta = await testdriver.extract('the estimated delivery date');
console.log(`Order ${orderNumber} arrives ${eta}`);
```
```javascript theme={null}
const icon = await testdriver.find('info icon next to the price');
await icon.hover();
const tooltipText = await testdriver.extract('the tooltip text');
expect(tooltipText).toContain('tax included');
```
```javascript theme={null}
// Read a value before an action
const before = await testdriver.extract('the account balance');
const addBtn = await testdriver.find('add funds button');
await addBtn.click();
// Read it again after and compare
const after = await testdriver.extract('the account balance');
expect(Number(after.replace(/[^0-9.]/g, ''))).toBeGreaterThan(
Number(before.replace(/[^0-9.]/g, ''))
);
```
```javascript theme={null}
// Read a generated code on one screen...
const resetCode = await testdriver.extract('the password reset code');
// ...and type it into the next
const codeField = await testdriver.find('reset code input');
await codeField.click();
await testdriver.type(resetCode);
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it, expect } from 'vitest';
import TestDriver from 'testdriverai';
describe('Extraction', () => {
let testdriver;
beforeAll(async () => {
testdriver = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should capture the order confirmation', async () => {
await testdriver.focusApplication('Google Chrome');
// Complete a checkout
const checkoutBtn = await testdriver.find('checkout button');
await checkoutBtn.click();
const placeOrderBtn = await testdriver.find('place order button');
await placeOrderBtn.click();
// Verify we reached confirmation
await testdriver.assert('the order confirmation page is displayed');
// Extract the details the app generated
const orderNumber = await testdriver.extract('the order confirmation number');
const total = await testdriver.extract('the order total as a number without currency symbol');
// Assert on the extracted values
expect(orderNumber).toBeTruthy();
expect(Number(total)).toBeGreaterThan(0);
});
});
```
## How It Works
1. TestDriver captures a screenshot of the current screen
2. The image and your description are sent to the TestDriver API
3. The AI reads the requested information from the screenshot
4. The extracted value is returned as a string
Like [assertions](/v7/making-assertions), `extract()` reads the screen fresh on every call β it is not cached β so it always reflects the current state of the app.
## Related Methods
* [`assert()`](/v7/assert) - Verify screen state with a boolean AI judgment
* [`find()`](/v7/find) - Locate elements to interact with
* [`parse()`](/v7/parse) - Detect all UI elements on screen
# find()
Source: https://docs.testdriver.ai/v7/find
Locate UI elements using natural language
## Overview
Locate UI elements on screen using AI-powered natural language descriptions. Returns an `Element` object that can be interacted with.
## Syntax
```javascript theme={null}
const element = await testdriver.find(description)
const element = await testdriver.find(description, options)
```
## Parameters
Natural language description of the element to find
Optional configuration for finding and caching
Custom cache key for storing element location. Use this to prevent cache pollution when using dynamic variables in prompts, or to share cache across tests.
Similarity threshold (0-1) for cache matching. Lower values require more similarity. Set to -1 to disable cache.
Maximum time in milliseconds to poll for the element. Retries every 5 seconds until found or timeout expires. Defaults to `10000` (10 seconds). Set to `0` to disable polling and make a single attempt.
Minimum confidence threshold (0-1). If the AI's confidence score for the found element is below this value, the find will be treated as a failure (`element.found()` returns `false`). Useful for ensuring high-quality matches in critical test steps.
Element type hint that wraps the description for better matching. Accepted values:
* `"text"` β Wraps the prompt as `The text "..."`
* `"image"` β Wraps the prompt as `The image "..."`
* `"ui"` β Wraps the prompt as `The UI element "..."`
* `"any"` β No wrapping, uses the description as-is (default behavior)
Two-phase zoom mode for better precision in crowded UIs with many similar elements. Disabled by default.
Enable AI verification of the located element. When `true`, a second AI call checks that the coordinates returned actually correspond to the requested element, catching hallucinated or incorrect positions. Disabled by default for lower latency. Defaults to the global `verify` option set on the [SDK constructor](/v7/client) when not specified per call.
AI sampling configuration for this find call (overrides global `ai` config from constructor).
Controls randomness. `0` = deterministic. Default: `0` for find verification.
Sampling parameters
Top-P (nucleus sampling). Range: 0-1.
Top-K sampling. `1` = most deterministic.
## Returns
`Promise` - Element instance that has been automatically located
## Examples
### Basic Element Finding
```javascript theme={null}
// Find by role
const button = await testdriver.find('submit button');
const input = await testdriver.find('email input field');
// Find by text content
const link = await testdriver.find('Contact Us link');
const heading = await testdriver.find('Welcome heading');
// Find by visual appearance
const icon = await testdriver.find('red warning icon');
const image = await testdriver.find('company logo image');
```
### Finding with Context
```javascript theme={null}
// Provide location context
const field = await testdriver.find('username input in the login form');
const button = await testdriver.find('delete button in the top right corner');
// Describe nearby elements
const input = await testdriver.find('input field below the email label');
const checkbox = await testdriver.find('checkbox next to "Remember me"');
// Describe visual position
const menu = await testdriver.find('hamburger menu icon in the top left');
```
### Interacting with Found Elements
```javascript theme={null}
// Find and click
const submitBtn = await testdriver.find('submit button');
await submitBtn.click();
// Find and verify
const message = await testdriver.find('success message');
if (message.found()) {
console.log('Success message appeared');
}
// Find and extract info
const price = await testdriver.find('product price');
console.log('Price location:', price.coordinates);
console.log('Price text:', price.text);
```
## Element Object
The returned `Element` object provides:
### Methods
* `found()` - Check if element was located
* `click(action)` - Click the element
* `hover()` - Hover over the element
* `doubleClick()` - Double-click the element
* `rightClick()` - Right-click the element
* `find(newDescription)` - Re-locate with optional new description
### Properties
* `coordinates` - Element position `{x, y, centerX, centerY}`
* `x`, `y` - Top-left coordinates
* `centerX`, `centerY` - Center coordinates
* `text` - Text content (if available)
* `screenshot` - Base64 screenshot (if available)
* `confidence` - AI confidence score
* `width`, `height` - Element dimensions
* `boundingBox` - Complete bounding box
See [Elements Reference](/v7/elements) for complete details.
### JSON Serialization
Elements can be safely serialized using `JSON.stringify()` for logging and debugging. Circular references are automatically removed:
```javascript theme={null}
const element = await testdriver.find('login button');
// Safe to stringify - no circular reference errors
console.log(JSON.stringify(element, null, 2));
// Output includes useful debugging info:
// {
// "description": "login button",
// "coordinates": { "x": 100, "y": 200, "centerX": 150, "centerY": 225 },
// "found": true,
// "threshold": 0.01,
// "x": 100,
// "y": 200,
// "cache": {
// "hit": true,
// "strategy": "pixel-diff",
// "createdAt": "2025-12-09T10:30:00Z",
// "diffPercent": 0.0023,
// "imageUrl": "https://..."
// },
// "similarity": 0.98,
// "confidence": 0.95,
// "selector": "button#login",
// "aiResponse": "Found the blue login button..."
// }
```
This is useful for:
* Debugging element detection issues
* Logging test execution details
* Sharing element information across processes
* Analyzing cache performance
## Best Practices
**Be specific in descriptions**
More specific descriptions improve accuracy:
```javascript theme={null}
// β Good
await testdriver.find('blue submit button below the email field');
// β Too vague
await testdriver.find('button');
```
**Always check if found**
Verify elements were located before interacting:
```javascript theme={null}
const element = await testdriver.find('login button');
if (!element.found()) {
throw new Error('Login button not found');
}
await element.click();
```
**Include visual or positional context**
```javascript theme={null}
// Include color
await testdriver.find('red error icon');
// Include position
await testdriver.find('search button in the top navigation bar');
// Include nearby text
await testdriver.find('checkbox next to "I agree to terms"');
```
## Confidence Threshold
Require a minimum AI confidence score for element matches. If the confidence is below the threshold, `find()` treats the result as not found:
```javascript theme={null}
// Require at least 90% confidence
const element = await testdriver.find('submit button', { confidence: 0.9 });
if (!element.found()) {
// AI found something but wasn't confident enough
throw new Error('Could not confidently locate submit button');
}
await element.click();
```
This is useful for:
* Critical test steps where an incorrect click could cause cascading failures
* Distinguishing between similar elements (e.g., multiple buttons)
* Failing fast when the UI has changed unexpectedly
```javascript theme={null}
// Combine with timeout for robust polling with confidence gate
const element = await testdriver.find('success notification', {
confidence: 0.85,
timeout: 15000,
});
```
The `confidence` value is a float between 0 and 1 (e.g., `0.9` = 90%). The AI returns its confidence with each find result, which you can also read from `element.confidence` after a successful find.
## Element Type
Use the `type` option to hint what kind of element you're looking for. This wraps your description into a more specific prompt for the AI, improving match accuracy β especially when users provide short or ambiguous descriptions.
```javascript theme={null}
// Find text on the page
const label = await testdriver.find('Sign In', { type: 'text' });
// AI prompt becomes: The text "Sign In"
// Find an image
const logo = await testdriver.find('company logo', { type: 'image' });
// AI prompt becomes: The image "company logo"
// Find a UI element (button, input, checkbox, etc.)
const btn = await testdriver.find('Submit', { type: 'ui' });
// AI prompt becomes: The UI element "Submit"
// No wrapping β same as omitting the option
const el = await testdriver.find('the blue submit button', { type: 'any' });
```
| Type | Prompt sent to AI |
| --------- | ---------------------------------- |
| `"text"` | `The text "..."` |
| `"image"` | `The image "..."` |
| `"ui"` | `The UI element "..."` |
| `"any"` | Original description (no wrapping) |
This is particularly useful for short descriptions like `"Submit"` or `"Login"` where the AI may not know whether to look for a button, a link, or visible text. Specifying `type` removes the ambiguity.
## Polling for Dynamic Elements
By default, `find()` polls for up to 10 seconds (retrying every 5 seconds) until the element is found. You can customize this with the `timeout` option:
```javascript theme={null}
// Uses default 10s timeout - polls every 5 seconds
const element = await testdriver.find('login button');
await element.click();
// Custom timeout - wait up to 30 seconds
const element = await testdriver.find('login button', { timeout: 30000 });
await element.click();
// Disable polling - single attempt only
const element = await testdriver.find('login button', { timeout: 0 });
```
The `timeout` option:
* Defaults to `10000` (10 seconds)
* Retries finding the element every 5 seconds
* Stops when the element is found or the timeout expires
* Logs progress during polling
* Returns the element (check `element.found()` if not throwing on failure)
* Set to `0` to disable polling and make a single attempt
## Zoom Mode
Zoom mode is **disabled by default**. It uses a two-phase approach for better precision when locating elements, especially in crowded UIs with many similar elements.
To enable zoom for a specific find call, pass `zoom: true`:
```javascript theme={null}
// Enable zoom for better precision in crowded UIs
const extensionsBtn = await testdriver.find('extensions puzzle icon in Chrome toolbar', { zoom: true });
await extensionsBtn.click();
// Without zoom (default)
const largeButton = await testdriver.find('big hero button');
```
### How Zoom Mode Works
1. **Phase 1**: AI identifies the approximate location of the element
2. **Phase 2**: A 30% crop of the screen is created around that location
3. **Phase 3**: AI performs precise location on the zoomed/cropped image
4. **Result**: Coordinates are converted back to absolute screen position
This two-phase approach gives the AI a higher-resolution view of the target area, improving accuracy when multiple similar elements are close together.
You may want to enable zoom with `zoom: true` when:
* Targeting small elements in crowded UIs with many similar elements
* You need extra precision for closely spaced UI elements
## Verify Mode
Verify mode is **disabled by default**. When enabled, a second AI call checks that the coordinates returned by `find()` actually correspond to the requested element, catching hallucinated or incorrect positions.
```javascript theme={null}
// Enable verification for critical interactions
const deleteBtn = await testdriver.find('delete account button', { verify: true });
await deleteBtn.click();
```
### How Verify Mode Works
1. **Phase 1**: AI locates the element and returns coordinates
2. **Phase 2**: A second AI call examines the screenshot at those coordinates to confirm the element matches the description
3. **Result**: If verification fails, the find is retried or marked as not found
### Combining Zoom and Verify
For maximum accuracy, enable both `zoom` and `verify` together. This is useful for critical interactions where clicking the wrong element could cause cascading failures:
```javascript theme={null}
// Maximum accuracy: zoom for precision + verify to catch hallucinations
const element = await testdriver.find('small cancel icon next to the subscription', {
zoom: true,
verify: true,
});
await element.click();
```
## Cache Options
When a test completes successfully, the result of each `find()` is cached. On later runs, TestDriver reuses the cached match instead of making a fresh AI call, which significantly speeds up locating the same element. The cache lives in your [dashboard](https://console.testdriver.ai/cache) and is shared across runs β see the [Cache](/v7/cache) page for how matching, thresholds, and invalidation work.
Control caching behavior to optimize performance, especially when using dynamic variables in prompts.
### Custom Cache Key
Use `cacheKey` to prevent cache pollution when prompts contain variables:
```javascript theme={null}
// β Without cacheKey - creates new cache entry for each email value
const email = 'user@example.com';
await testdriver.find(`input for ${email}`); // Cache miss every time
// β With cacheKey - reuses cache regardless of variable
const email = 'user@example.com';
await testdriver.find(`input for ${email}`, {
cacheKey: 'email-input'
});
// Also useful for dynamic IDs, names, or other changing data
const orderId = generateOrderId();
await testdriver.find(`order ${orderId} status`, {
cacheKey: 'order-status' // Same cache for all orders
});
```
### Cache Threshold
Control how similar a cached result must be to reuse it:
```javascript theme={null}
// Default: 95% similarity required
await testdriver.find('submit button');
// Strict threshold - 99% similarity required
await testdriver.find('submit button', {
cacheThreshold: 0.01
});
// Disable cache entirely for this call
await testdriver.find('submit button', {
cacheThreshold: -1
});
// Combine cacheKey with threshold
await testdriver.find('submit button', {
cacheKey: 'submit-btn',
cacheThreshold: 0.01
});
```
By default, TestDriver auto-generates a cache key from the SHA-256 hash of your test file. When you modify your test file, the hash changes automatically, invalidating stale cache entries.
### Manual Polling (Alternative)
If you need custom polling logic:
```javascript theme={null}
async function waitForElement(testdriver, description, timeout = 30000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const element = await testdriver.find(description);
if (element.found()) return element;
await new Promise(r => setTimeout(r, 1000));
}
throw new Error(`Element "${description}" not found after ${timeout}ms`);
}
// Usage
const button = await waitForElement(testdriver, 'submit button', 10000);
await button.click();
```
## Use Cases
```javascript theme={null}
const emailField = await testdriver.find('email input field');
await emailField.click();
await testdriver.type('user@example.com');
const passwordField = await testdriver.find('password input');
await passwordField.click();
await testdriver.type('MyP@ssw0rd');
```
```javascript theme={null}
const submitBtn = await testdriver.find('submit button');
await submitBtn.click();
const cancelLink = await testdriver.find('cancel link');
await cancelLink.click();
const menuIcon = await testdriver.find('hamburger menu icon');
await menuIcon.click();
```
```javascript theme={null}
// Wait for loading to complete
let content;
for (let i = 0; i < 30; i++) {
content = await testdriver.find('results table');
if (content.found()) break;
await new Promise(r => setTimeout(r, 1000));
}
// Interact with loaded content
const firstRow = await testdriver.find('first row in the results table');
await firstRow.click();
```
```javascript theme={null}
// Modals and dialogs
const modal = await testdriver.find('confirmation dialog');
if (modal.found()) {
const confirmBtn = await testdriver.find('confirm button in the dialog');
await confirmBtn.click();
}
// Dropdown menus
const dropdown = await testdriver.find('country dropdown');
await dropdown.click();
const option = await testdriver.find('United States option');
await option.click();
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it, expect } from 'vitest';
import TestDriver from 'testdriverai';
describe('Element Finding', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should find and interact with elements', async () => {
await testdriver.focusApplication('Google Chrome');
// Find login form elements
const usernameField = await testdriver.find('username input field');
expect(usernameField.found()).toBe(true);
await usernameField.click();
await testdriver.type('testuser');
// Find with context
const passwordField = await testdriver.find('password input below username');
await passwordField.click();
await testdriver.type('password123');
// Find button
const submitBtn = await testdriver.find('green submit button');
expect(submitBtn.found()).toBe(true);
console.log('Button location:', submitBtn.centerX, submitBtn.centerY);
await submitBtn.click();
// Wait for success message
let successMsg;
for (let i = 0; i < 10; i++) {
successMsg = await testdriver.find('success notification');
if (successMsg.found()) break;
await new Promise(r => setTimeout(r, 1000));
}
expect(successMsg.found()).toBe(true);
});
});
```
## Related Methods
* [`click()`](/v7/click) - Click on found elements
* [`hover()`](/v7/hover) - Hover over elements
* [`assert()`](/v7/assert) - Verify element states
* [Elements Reference](/v7/elements) - Complete Element API
***
## findAll()
Locate **all elements** matching a description, rather than just one.
### Syntax
```javascript theme={null}
const elements = await testdriver.findAll(description, options)
```
### Parameters
Natural language description of elements to find
Optional cache options (same as `find()`)
Cache key for storing element location
Similarity threshold (0-1) for cache matching. Set to -1 to disable cache.
### Returns
`Promise` - Array of Element instances
### Examples
#### Basic Usage
```javascript theme={null}
// Find all matching elements
const buttons = await testdriver.findAll('button');
console.log(`Found ${buttons.length} buttons`);
// Interact with specific element
if (buttons.length > 0) {
await buttons[0].click(); // Click first button
}
// Iterate over all
for (const button of buttons) {
console.log(`Button at (${button.x}, ${button.y})`);
}
```
#### Finding Multiple Items
```javascript theme={null}
// Find all list items
const items = await testdriver.findAll('list item');
// Find specific item by index
const thirdItem = items[2];
await thirdItem.click();
// Check all items
for (let i = 0; i < items.length; i++) {
console.log(`Item ${i + 1}: ${items[i].text || 'No text'}`);
}
```
#### With Caching
```javascript theme={null}
// Cache element locations for faster subsequent runs
const menuItems = await testdriver.findAll('menu item', {
cacheKey: 'main-menu-items'
});
// First run: ~2-3 seconds (AI call)
// Subsequent runs: ~100ms (cache hit)
```
#### Empty Results
```javascript theme={null}
// Returns empty array if nothing found (doesn't throw error)
const errors = await testdriver.findAll('error message');
if (errors.length === 0) {
console.log('No errors found - test passed!');
} else {
console.log(`Found ${errors.length} errors`);
}
```
### Differences from find()
| Feature | find() | findAll() |
| ---------------- | ------------------------------------- | ------------------------- |
| Return type | Single `Element` | Array of `Element[]` |
| If nothing found | Throws `ElementNotFoundError` | Returns empty array `[]` |
| Chainable | β Yes: `await find('button').click()` | β No (returns array) |
| Use case | One specific element | Multiple similar elements |
| Cache support | β Yes | β Yes |
### Use Cases
```javascript theme={null}
// Find all rows in a table
const rows = await testdriver.findAll('table row');
// Click every row
for (const row of rows) {
await row.click();
await new Promise(r => setTimeout(r, 500)); // Wait between clicks
}
// Or click specific row
await rows[2].click(); // Click third row
```
```javascript theme={null}
// Find all checkboxes
const checkboxes = await testdriver.findAll('checkbox');
// Check all boxes
for (const checkbox of checkboxes) {
await checkbox.click();
}
// Or select first unchecked
const unchecked = checkboxes[0];
await unchecked.click();
```
```javascript theme={null}
// Find all navigation links
const navLinks = await testdriver.findAll('navigation link');
// Validate all are present
expect(navLinks.length).toBeGreaterThan(0);
// Click specific link by text
const homeLink = navLinks.find(link =>
link.text?.toLowerCase().includes('home')
);
if (homeLink) {
await homeLink.click();
}
```
```javascript theme={null}
// Check if any error messages exist
const errors = await testdriver.findAll('error message');
if (errors.length > 0) {
console.log(`Found ${errors.length} validation errors`);
// Log each error location
errors.forEach((error, i) => {
console.log(`Error ${i + 1} at (${error.x}, ${error.y})`);
});
} else {
console.log('Form validation passed!');
}
```
### Complete Example
```javascript theme={null}
import { test, expect } from 'vitest';
import { chrome } from 'testdriverai/presets';
test('select multiple items from list', async (context) => {
const { testdriver } = await chrome(context, {
url: 'https://example.com/products'
});
// Find all product cards
const products = await testdriver.findAll('product card');
expect(products.length).toBeGreaterThan(0);
console.log(`Found ${products.length} products`);
// Click first 3 products
const productsToSelect = Math.min(3, products.length);
for (let i = 0; i < productsToSelect; i++) {
await products[i].click();
console.log(`Selected product ${i + 1}`);
await new Promise(r => setTimeout(r, 500)); // Brief pause
}
// Verify selections
const selectedBadges = await testdriver.findAll('selected badge');
expect(selectedBadges.length).toBe(productsToSelect);
});
```
### Best Practices
**Handle empty arrays gracefully**
```javascript theme={null}
// β Good - check length first
const items = await testdriver.findAll('list item');
if (items.length > 0) {
await items[0].click();
}
// β Bad - may throw error
const items = await testdriver.findAll('list item');
await items[0].click(); // Error if array is empty!
```
**Use find() for single elements**
```javascript theme={null}
// β Use find() when you need exactly one
const submitBtn = await testdriver.find('submit button');
await submitBtn.click();
// β Unnecessary - findAll() returns array
const buttons = await testdriver.findAll('submit button');
await buttons[0].click(); // Extra array handling
```
**Cache for performance**
```javascript theme={null}
// First run - slow (AI call)
const items = await testdriver.findAll('menu item', {
cacheKey: 'menu-items'
});
// Subsequent runs - fast (cache hit)
// ~10-20x faster than without cache
```
# focusApplication()
Source: https://docs.testdriver.ai/v7/focus-application
Bring an application window to the foreground
## Overview
Bring a specific application window to the foreground and make it the active window for interactions.
## Syntax
```javascript theme={null}
await testdriver.focusApplication(name)
```
## Parameters
Application name (e.g., `'Google Chrome'`, `'Microsoft Edge'`, `'Notepad'`)
## Returns
`Promise` - Result message
## Examples
### Common Applications
```javascript theme={null}
// Focus Chrome browser
await testdriver.focusApplication('Google Chrome');
// Focus Edge browser
await testdriver.focusApplication('Microsoft Edge');
// Focus Notepad
await testdriver.focusApplication('Notepad');
// Focus File Explorer
await testdriver.focusApplication('File Explorer');
// Focus Visual Studio Code
await testdriver.focusApplication('Visual Studio Code');
```
### After Opening Applications
```javascript theme={null}
// Open Chrome and focus it
await testdriver.exec('pwsh', `
Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "https://example.com"
`, 5000);
await new Promise(r => setTimeout(r, 2000)); // Wait for launch
// Focus the Chrome window
await testdriver.focusApplication('Google Chrome');
```
## Best Practices
**Focus before UI interactions**
Always focus the target application before interacting with its UI:
```javascript theme={null}
await testdriver.focusApplication('Google Chrome');
const button = await testdriver.find('submit button');
await button.click();
```
**Wait after launching apps**
Give applications time to open before focusing:
```javascript theme={null}
await testdriver.exec('pwsh', 'Start-Process notepad', 5000);
await new Promise(r => setTimeout(r, 1000)); // Wait for launch
await testdriver.focusApplication('Notepad');
```
**Use exact application names**
```javascript theme={null}
// β Correct
await testdriver.focusApplication('Google Chrome');
// β May not work
await testdriver.focusApplication('Chrome');
await testdriver.focusApplication('chrome.exe');
```
**Application must be running**
The application must already be running. `focusApplication()` won't launch applications, only bring existing windows to the foreground.
## Use Cases
```javascript theme={null}
// Test workflow across multiple apps
await testdriver.focusApplication('Google Chrome');
const data = await testdriver.extract('the order number');
await testdriver.focusApplication('Notepad');
await testdriver.type(data);
await testdriver.pressKeys(['ctrl', 's']);
await testdriver.focusApplication('Google Chrome');
const nextButton = await testdriver.find('next button');
await nextButton.click();
```
```javascript theme={null}
// Compare behavior in different browsers
await testdriver.focusApplication('Google Chrome');
await testdriver.assert('page loaded correctly in Chrome');
await testdriver.focusApplication('Microsoft Edge');
await testdriver.assert('page loaded correctly in Edge');
```
```javascript theme={null}
// Launch and focus desktop app
await testdriver.exec('pwsh', 'Start-Process notepad', 5000);
await new Promise(r => setTimeout(r, 1000));
await testdriver.focusApplication('Notepad');
await testdriver.type('Test content');
```
```javascript theme={null}
// Show desktop first
await testdriver.pressKeys(['winleft', 'd']);
// Click desktop icon
const icon = await testdriver.find('Chrome icon on desktop');
await icon.click();
await new Promise(r => setTimeout(r, 2000));
// Focus the opened window
await testdriver.focusApplication('Google Chrome');
```
## Common Application Names
### Browsers
* `'Google Chrome'`
* `'Microsoft Edge'`
* `'Mozilla Firefox'`
* `'Safari'` (macOS)
### Office Applications
* `'Microsoft Word'`
* `'Microsoft Excel'`
* `'Microsoft PowerPoint'`
* `'Microsoft Outlook'`
### Development Tools
* `'Visual Studio Code'`
* `'Visual Studio'`
* `'IntelliJ IDEA'`
* `'Sublime Text'`
### System Applications
* `'Notepad'`
* `'File Explorer'`
* `'Command Prompt'`
* `'Windows PowerShell'`
* `'Task Manager'`
### Communication
* `'Microsoft Teams'`
* `'Slack'`
* `'Discord'`
* `'Zoom'`
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it } from 'vitest';
import TestDriver from 'testdriverai';
describe('Multi-Application Workflow', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should work across multiple applications', async () => {
// Start in browser
await testdriver.focusApplication('Google Chrome');
// Get data from web page
const orderNumber = await testdriver.extract('the order number');
console.log('Order:', orderNumber);
// Open Notepad
await testdriver.exec('pwsh', 'Start-Process notepad', 5000);
await new Promise(r => setTimeout(r, 1500));
// Focus Notepad and save data
await testdriver.focusApplication('Notepad');
await testdriver.type(`Order Number: ${orderNumber}`);
await testdriver.type('\n');
await testdriver.type(`Date: ${new Date().toISOString()}`);
// Save file
await testdriver.pressKeys(['ctrl', 's']);
await new Promise(r => setTimeout(r, 500));
await testdriver.type('C:\\order-info.txt');
await testdriver.pressKeys(['enter']);
// Return to browser
await testdriver.focusApplication('Google Chrome');
const confirmButton = await testdriver.find('confirm order button');
await confirmButton.click();
await testdriver.assert('order confirmed');
});
it('should switch between browser tabs', async () => {
await testdriver.focusApplication('Google Chrome');
// Open new tab
await testdriver.pressKeys(['ctrl', 't']);
await new Promise(r => setTimeout(r, 500));
// Navigate to URL
await testdriver.pressKeys(['ctrl', 'l']);
await testdriver.type('https://example.com');
await testdriver.pressKeys(['enter']);
await new Promise(r => setTimeout(r, 2000));
// Ensure Chrome is still focused
await testdriver.focusApplication('Google Chrome');
await testdriver.assert('example.com page is loaded');
});
it('should handle dialog boxes', async () => {
await testdriver.focusApplication('Google Chrome');
const deleteButton = await testdriver.find('delete account button');
await deleteButton.click();
await new Promise(r => setTimeout(r, 500));
// Dialog appears - make sure it's focused
await testdriver.focusApplication('Google Chrome');
const confirmBtn = await testdriver.find('confirm deletion button');
await confirmBtn.click();
});
});
```
## Related Methods
* [`exec()`](/v7/exec) - Launch applications with PowerShell
* [`pressKeys()`](/v7/press-keys) - Use Alt+Tab to switch windows
* [`find()`](/v7/find) - Locate elements in the focused window
# Explore
Source: https://docs.testdriver.ai/v7/generating-tests
Generate tests by exploring your app with the AI vision agent
Start by exploring your app. This is where tests begin: you describe a flow in plain English, and the TestDriver vision agent clicks, types, and reads the screen to figure it out β then writes the test for you. No selectors, no DOM, no brittle locators. Just describe what you want to test and let the agent discover the rest.
There are two ways to explore: chatting interactively with your AI assistant through the TestDriver MCP server, or handing a coding agent our instructions file and prompting it to generate a test.
Both paths need an MCP-capable AI coding assistant. If you don't have one yet, start with **GitHub Copilot** β there's a [free tier](https://github.com/features/copilot/plans), no credit card required. See [Run β Setup](/v7/copilot/running-tests#setup) for the full install and sign-in walkthrough.
## Instructions for Coding Agents
We recommend starting with [our quickstart](./quickstart), then supplying your coding agent with our agent instructions file.
Copy the current version of our agent instructions to provide your coding agent with up-to-date instructions on how to generate TestDriver tests.
Then, you can prompt your coding agent to generate tests. Here is an example prompt:
```md theme={null}
Make me a TestDriver test that does the following steps:
Navigate to practicetestautomation.com
Type username student into Username field
Type password Password123 into Password field
Push Submit button
Verify new page contains expected text 'logged in'
```
Explicit commands are preferred for production tests, as they are cheaper, faster, and more reliable.
## Start a Conversation
With TestDriver's MCP server and your AI assistant (GitHub Copilot, Cursor, or Claude Desktop), you can create tests by chatting with an AI agent. The agent spawns a virtual machine, executes actions, and writes test code for you.
Open your AI assistant's chat. If your project has no other agents configured, the TestDriver agent is used by default. Otherwise, select **testdriver** from the agent dropdown in the chat panel.
Describe what you want to test:
```
Create a test that logs into my app at https://myapp.com
```
The agent will:
1. Start a new session and spawn a Linux virtual machine
2. Launch Chrome and navigate to your URL
3. Execute actions based on your instructions
4. Write the test code to a `.test.mjs` file
The TestDriver agent appears in the agent selection dropdown if you have other agents configured (like `copilot-instructions.md` or other `.agent.md` files). Make sure to select **testdriver** to use TestDriver's MCP tools.
## Live Preview Panel
When the agent starts a session, a **live preview panel** opens in your editor. This lets you:
* **Watch tests execute** in real-time
* **Interact with the VM** β click, type, and navigate manually
* **Debug issues** β see exactly what the AI sees
The live preview panel requires the TestDriver VS Code extension. Set `TD_PREVIEW=ide` in your MCP configuration to enable it. See the [Run page](/v7/copilot/running-tests) for all device and preview options.
## Interactive Workflow
The recommended workflow for exploring and creating tests:
Tell the agent what you want to test:
```
Test the checkout flow on https://shop.example.com
```
The agent will start executing. Watch the preview panel and provide guidance if needed:
```
The login modal appeared. Use test@example.com and password123
```
Ask the agent to verify specific conditions:
```
Assert that the order confirmation page is shown with the order number
```
The agent writes test code as it works. Review the generated `.test.mjs` file and make any adjustments.
## Example Conversation
Here's a complete example of exploring and creating a login test:
```
You: Create a test for the login flow at https://console.testdriver.ai
Agent: I'll create a test for the login flow. Starting a session...
[Screenshot: Login page loaded]
I can see the login page. Let me find the email input.
[Screenshot: Email input highlighted]
Now I'll type the email and find the password field...
You: Use test@example.com for the email
Agent: Got it. Typing test@example.com...
[Screenshot: Email entered]
Now entering the password and clicking Sign In...
[Screenshot: Dashboard loaded]
β Login successful! I've written the test to tests/login.test.mjs
```
## Generated Test Code
The agent generates standard Vitest test files:
```javascript tests/login.test.mjs theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Login Flow", () => {
it("should log in successfully", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({
url: "https://console.testdriver.ai"
});
const emailInput = await testdriver.find("email input field");
await emailInput.click();
await testdriver.type("test@example.com");
const passwordInput = await testdriver.find("password input field");
await passwordInput.click();
await testdriver.type("password123");
const signInButton = await testdriver.find("Sign In button");
await signInButton.click();
const result = await testdriver.assert("dashboard is visible");
expect(result).toBeTruthy();
});
});
```
## Tips for Better Tests
Instead of "click the button", say "click the blue Sign In button in the header". More context helps the AI find the right element.
If your app has animations or loading states, tell the agent to wait:
```
Wait for the loading spinner to disappear before continuing
```
Add assertions after each major action to catch regressions early:
```
Assert that the product was added to the cart
```
For long workflows, create the test incrementally and verify each step works before moving on.
## Next
Once the agent has explored your app, TestDriver caches what it discovers so your tests replay instantly without re-reasoning over the screen every time.
# Hosted
Source: https://docs.testdriver.ai/v7/hosted
The fastest way to get started with TestDriver. Just set your API key and start testing.
Hosted pricing is based on **device-seconds**: the amount of time your tests run on **our infrastructure**.
* **Zero Setup** β Start testing immediately. No DevOps required.
* **Free Tier** β Get started with a limited preview at no cost.
* **Pay As You Go** β Only pay for the device-seconds you use.
## Hosted Plans
**\$0/month**
* 1 Concurrent Sandbox
* 60 Minutes Included
* 1 Team User
* Community Support
**\$20/month per user**
* 2 Concurrent Sandboxes
* 600 Minutes Included
* Overage: \$0.001/second
* Test Recordings
* Community Support
**\$600/month**
* 8 Concurrent Sandboxes
* 10,000 Minutes Included
* Overage: \$0.001/second
* 5 Team Users
* Test Recordings
* Private Support
* Test Analytics
* CPU, RAM, & Network Profiles
## Get Started
Hosted is the default when you follow the Quickstart guide.
Set your API key and start testing in minutes.
## Parallel Testing Limits
Your account has a set number of **license slots** that determine how many devices can run simultaneously. You can view your available slots in the [TestDriver Dashboard](https://console.testdriver.ai).
**When is a slot in use?** A license slot is occupied when a test client is connected. As soon as your device is destroyed the slot becomes available immediately.
## Avoiding Slot Conflicts
To prevent tests from failing due to exceeding your license slot limit, we recommend two key configurations:
Limit concurrent tests to match your available license slots:
```javascript vitest.config.mjs theme={null}
import { defineConfig } from 'vitest/config';
import TestDriver from 'testdriverai/vitest';
export default defineConfig({
test: {
testTimeout: 900000,
hookTimeout: 900000,
maxConcurrency: 5, // Set to your license slot limit
reporters: ['default', TestDriver()],
setupFiles: ['testdriverai/vitest/setup'],
},
});
```
Check your slot count at [console.testdriver.ai](https://console.testdriver.ai) and set `maxConcurrency` to that number or lower.
Prevent multiple workflow runs from competing for the same slots by using [GitHub's concurrency controls](https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs):
```yaml .github/workflows/test.yml theme={null}
name: Tests
on:
push:
branches: [main]
pull_request:
# Prevent concurrent runs from competing for license slots
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Run tests
run: vitest run
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
```
The `concurrency` block ensures:
* Only one workflow run per branch runs at a time
* New pushes cancel in-progress runs on the same branch
* Different branches/PRs can run in parallel (up to your slot limit)
## When to Consider Self-Hosted
Hosted is perfect for getting started and for teams that want zero infrastructure management. However, you might consider [Self-Hosted](/v7/self-hosted) if you:
* Want to escape per-second billing with a flat license fee
* Require greater concurrency than offered in Cloud plans
* Need full control over your infrastructure and privacy
* Want to use your own AI API keys
* Require custom hardware configurations
* Have high test volumes that make self-hosting more economical
Learn about self-hosting for unlimited test execution at a flat rate.
# hover()
Source: https://docs.testdriver.ai/v7/hover
Hover over elements or coordinates
## Overview
Move the mouse cursor over elements or specific coordinates without clicking, useful for revealing tooltips, dropdowns, and hover effects.
## Element Hover
Hover over a located element.
### Syntax
```javascript theme={null}
await element.hover()
```
### Returns
`Promise`
### Examples
```javascript theme={null}
// Find and hover
const tooltip = await testdriver.find('info icon');
await tooltip.hover();
// Wait to see tooltip
await new Promise(r => setTimeout(r, 1000));
// Hover over menu to reveal submenu
const menu = await testdriver.find('Products menu');
await menu.hover();
const submenu = await testdriver.find('Laptops submenu item');
await submenu.click();
```
## Coordinate Hover
Hover at specific screen coordinates.
### Syntax
```javascript theme={null}
await testdriver.hover(x, y)
```
### Parameters
X coordinate
Y coordinate
### Returns
`Promise`
### Examples
```javascript theme={null}
// Hover at coordinates
await testdriver.hover(500, 300);
// Hover and wait
await testdriver.hover(500, 300);
await new Promise(r => setTimeout(r, 1000));
```
## Best Practices
**Prefer element hover over coordinates**
```javascript theme={null}
// β Preferred
const icon = await testdriver.find('info icon');
await icon.hover();
// β Avoid
await testdriver.hover(500, 300);
```
**Wait after hovering for dynamic content**
```javascript theme={null}
const menuItem = await testdriver.find('Settings menu');
await menuItem.hover();
// Wait for submenu to appear
await new Promise(r => setTimeout(r, 500));
const subItem = await testdriver.find('Profile submenu');
await subItem.click();
```
**Verify element was found before hovering**
```javascript theme={null}
const element = await testdriver.find('tooltip trigger');
if (!element.found()) {
throw new Error('Element not found');
}
await element.hover();
```
## Use Cases
```javascript theme={null}
// Hover to show tooltip
const icon = await testdriver.find('help icon');
await icon.hover();
await new Promise(r => setTimeout(r, 1000));
// Read tooltip content
const tooltipText = await testdriver.extract('the tooltip text');
console.log('Tooltip:', tooltipText);
```
```javascript theme={null}
// Hover to reveal dropdown
const menuItem = await testdriver.find('Products menu item');
await menuItem.hover();
// Wait for dropdown animation
await new Promise(r => setTimeout(r, 500));
// Click submenu option
const category = await testdriver.find('Electronics category');
await category.click();
```
```javascript theme={null}
// Hover over thumbnail to see preview
const thumbnail = await testdriver.find('product thumbnail');
await thumbnail.hover();
await new Promise(r => setTimeout(r, 800));
// Verify preview appears
await testdriver.assert('product preview is displayed');
```
```javascript theme={null}
// Test hover state styling
const button = await testdriver.find('call to action button');
await button.hover();
await new Promise(r => setTimeout(r, 500));
// Verify hover effect
await testdriver.assert('button background changed to blue');
```
```javascript theme={null}
// Use hover in drag and drop
const item = await testdriver.find('draggable item');
await item.mouseDown();
// Hover over drop zone
const dropZone = await testdriver.find('drop area');
await dropZone.hover();
await new Promise(r => setTimeout(r, 300));
await dropZone.mouseUp();
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it } from 'vitest';
import TestDriver from 'testdriverai';
describe('Hover Interactions', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should show tooltip on hover', async () => {
await testdriver.focusApplication('Google Chrome');
// Find and hover over icon
const infoIcon = await testdriver.find('information icon');
await infoIcon.hover();
// Wait for tooltip to appear
await new Promise(r => setTimeout(r, 1000));
// Verify tooltip is visible
await testdriver.assert('tooltip is displayed');
// Extract tooltip text
const tooltipText = await testdriver.extract('the tooltip message');
console.log('Tooltip says:', tooltipText);
});
it('should navigate dropdown menu', async () => {
// Hover over main menu
const menu = await testdriver.find('Navigation menu');
await menu.hover();
// Wait for dropdown
await new Promise(r => setTimeout(r, 500));
// Hover over submenu item
const submenu = await testdriver.find('Account submenu');
await submenu.hover();
await new Promise(r => setTimeout(r, 300));
// Click nested menu item
const settings = await testdriver.find('Settings option');
await settings.click();
await testdriver.assert('settings page is displayed');
});
it('should preview images on hover', async () => {
// Hover over product image
const productImg = await testdriver.find('product 1 thumbnail');
await productImg.hover();
// Wait for preview
await new Promise(r => setTimeout(r, 800));
// Verify preview appeared
await testdriver.assert('large product preview is shown');
// Move away
const otherElement = await testdriver.find('page heading');
await otherElement.hover();
// Verify preview disappeared
await new Promise(r => setTimeout(r, 500));
await testdriver.assert('product preview is hidden');
});
});
```
## Related Methods
* [`find()`](/v7/find) - Locate elements to hover
* [`click()`](/v7/click) - Click after hovering
* [`mouseDown()`](/v7/click) - Start drag operations
# Validate
Source: https://docs.testdriver.ai/v7/making-assertions
Locate elements and verify app state with AI-powered assertions
Once a test runs, validate that the app did what it should. Validation has two parts: locating the elements you want to check, and making assertions about the state of your app. TestDriver uses AI as a judge, returning a boolean plus reasoning about whether your app is in the expected state.
## Locating Elements
### Locating Single Elements
Use natural language to describe elements. Descriptions should be specific enough to locate the element, but not too-specific that they break with minor UI changes. For example:
```javascript theme={null}
await testdriver.find('email input field');
await testdriver.find('first product card in the grid');
await testdriver.find('dropdown menu labeled "Country"');
```
TestDriver will cache found elements for improved performance on subsequent calls. Learn more about [element caching here](/v7/caching).
### Debugging Found Elements
After finding an element, you can inspect its properties for debugging:
```javascript theme={null}
const button = await testdriver.find('submit button');
console.log(button);
```
This outputs all element properties:
```javascript theme={null}
{
description: 'submit button',
found: true,
x: 150,
y: 300,
coordinates: { x: 150, y: 300, centerX: 200, centerY: 320 },
threshold: 0.8,
confidence: 0.95,
similarity: 0.92,
selector: 'button[type="submit"]',
cache: {
hit: true,
strategy: 'pixel-diff',
createdAt: '2025-01-15T10:30:00Z',
diffPercent: 0.02,
imageUrl: 'https://...'
}
}
```
### Working with Multiple Elements
Find and interact with multiple elements:
```javascript theme={null}
// Find all matching elements
const products = await testdriver.findAll('product card');
console.log(`Found ${products.length} products`);
// Interact with each
for (const product of products) {
const title = await product.find('title text');
console.log('Product:', title.text);
await product.find('add to cart button').click();
}
// Or find specific element
const firstProduct = products[0];
await firstProduct.click();
```
## Making Assertions
Use AI-powered assertions to verify application state. TestDriver acts as a judge: it evaluates your natural-language statement against the current state of the app and returns a boolean plus reasoning explaining the verdict.
```javascript theme={null}
// Verify visibility
await testdriver.assert('login page is displayed');
await testdriver.assert('submit button is visible');
await testdriver.assert('loading spinner is not visible');
// Verify content
await testdriver.assert('page title is "Welcome"');
await testdriver.assert('success message says "Account created"');
await testdriver.assert('error message contains "Invalid email"');
// Verify state
await testdriver.assert('checkbox is checked');
await testdriver.assert('dropdown shows "United States"');
await testdriver.assert('button is disabled');
// Verify visual appearance
await testdriver.assert('submit button is blue');
await testdriver.assert('form has red border');
```
Assertions are not cached and always re-evaluated to ensure accuracy.
## Next
Drive your app forward by performing actions on the elements you've located and validated.
# mouseDown
Source: https://docs.testdriver.ai/v7/mouse-down
Press the mouse button without releasing it
## Overview
The `mouseDown()` method presses the mouse button at an element's location without releasing it. This is useful for drag operations, custom gestures, or when you need precise control over mouse events. You can either call it on an [`Element`](/v7/core-concepts/elements) instance or use it directly with a selector.
## Syntax
```javascript theme={null}
// Mouse down on an element
await element.mouseDown();
// Mouse down using a selector
await ai.mouseDown('selector');
```
## Parameters
When called on an `Element`, no parameters are required.
When called directly on the AI client:
| Parameter | Type | Description |
| ---------- | -------- | ---------------------------------------------------------------------------- |
| `selector` | `string` | The selector describing the element where the mouse button should be pressed |
## Returns
Returns a `Promise` that resolves when the mouse button is pressed.
## Examples
### Basic Drag Operation
```javascript theme={null}
// Start dragging an item
const dragItem = await ai.find('file to drag');
await dragItem.mouseDown();
// Move to drop target
const dropTarget = await ai.find('folder to drop into');
await dropTarget.hover();
// Release
await ai.mouseUp();
```
### Drag and Drop with Direct Selectors
```javascript theme={null}
await ai.mouseDown('draggable card');
await ai.hover('drop zone');
await ai.mouseUp();
```
### Selecting Multiple Items
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from '@testdriver/sdk';
test('selects multiple items with click and drag', async () => {
const { ai } = await chrome('https://app.example.com');
// Start selection at first item
await ai.mouseDown('first item in grid');
// Drag to last item
await ai.hover('last item in grid');
// Release to complete selection
await ai.mouseUp();
// Verify multiple items selected
const selectedItems = await ai.find('selected items count');
expect(selectedItems.text).toContain('5 items selected');
});
```
### Custom Drawing Application
```javascript theme={null}
test('draws on canvas', async () => {
const { ai } = await chrome('https://drawing-app.example.com');
// Start drawing
await ai.mouseDown('canvas at top-left corner');
// Draw a line by moving mouse
await ai.hover('canvas at center');
await ai.hover('canvas at bottom-right corner');
// Stop drawing
await ai.mouseUp();
// Verify something was drawn
const canvas = await ai.exec('document.querySelector("canvas").toDataURL()');
expect(canvas).toBeTruthy();
});
```
### Long Press Gesture
```javascript theme={null}
test('triggers long press menu', async () => {
const { ai } = await chrome('https://mobile-app.example.com');
// Press and hold
await ai.mouseDown('message in chat');
// Wait for long-press menu
await new Promise(resolve => setTimeout(resolve, 500));
// Verify menu appeared before releasing
const menu = await ai.find('message options menu');
expect(menu).toBeTruthy();
// Release
await ai.mouseUp();
});
```
### Resizing UI Elements
```javascript theme={null}
test('resizes panel', async () => {
const { ai } = await vscode();
// Grab resize handle
await ai.mouseDown('sidebar resize handle');
// Drag to new position
await ai.hover('position 300 pixels from left edge');
// Release
await ai.mouseUp();
// Verify new size
const sidebar = await ai.find('sidebar');
expect(sidebar.width).toBeGreaterThan(250);
});
```
## Important Notes
* Always pair `mouseDown()` with [`mouseUp()`](/v7/mouse-up) to complete the gesture
* The mouse button remains pressed until `mouseUp()` is called
* Use [`hover()`](/v7/hover) to move the mouse while the button is pressed
* For simple drag operations, consider using `ai()` with a natural language description like `"drag file to folder"`
## Related Methods
* [`mouseUp()`](/v7/mouse-up) - Release the mouse button
* [`hover()`](/v7/hover) - Move mouse to element
* [`click()`](/v7/click) - Full click (mouseDown + mouseUp)
* [`doubleClick()`](/v7/double-click) - Double-click on element
* [`rightClick()`](/v7/right-click) - Right-click for context menu
# mouseUp
Source: https://docs.testdriver.ai/v7/mouse-up
Release the mouse button
## Overview
The `mouseUp()` method releases the mouse button, completing a drag operation or custom mouse gesture that was started with [`mouseDown()`](/v7/mouse-down). You can call it without parameters to release at the current mouse position.
## Syntax
```javascript theme={null}
// Release mouse button at current position
await ai.mouseUp();
```
## Parameters
None. The mouse button is released at the current cursor position.
## Returns
Returns a `Promise` that resolves when the mouse button is released.
## Examples
### Complete Drag and Drop
```javascript theme={null}
// Start dragging
await ai.mouseDown('file to drag');
// Move to drop location
await ai.hover('target folder');
// Complete the drop
await ai.mouseUp();
```
### Selecting Multiple Files
```javascript theme={null}
import { test } from 'vitest';
import { vscode } from '@testdriver/sdk';
test('selects range of files', async () => {
const { ai } = await vscode();
// Click first file
await ai.click('first-file.js in explorer');
// Hold shift and click last file
await ai.pressKeys('Shift');
await ai.mouseDown('last-file.js in explorer');
await ai.mouseUp();
// Verify multiple files selected
const selectedCount = await ai.find('status bar showing file count');
expect(selectedCount.text).toContain('5 files');
});
```
### Drawing Application
```javascript theme={null}
test('draws a shape', async () => {
const { ai } = await chrome('https://drawing-app.example.com');
// Select pencil tool
await ai.click('pencil tool');
// Draw a line
await ai.mouseDown('canvas near top-left');
await ai.hover('canvas center');
await ai.hover('canvas bottom-right');
await ai.mouseUp();
// Verify drawing exists
const strokes = await ai.exec('canvas.getContext("2d").getImageData(0,0,100,100)');
expect(strokes).toBeTruthy();
});
```
### Resizing Window Panels
```javascript theme={null}
test('resizes editor panel', async () => {
const { ai } = await vscode();
// Grab the divider
await ai.mouseDown('panel resize divider');
// Drag to new position
await ai.hover('position 400 pixels from left');
// Release to complete resize
await ai.mouseUp();
// Verify new panel size
const panel = await ai.find('editor panel');
expect(panel.width).toBeGreaterThan(350);
});
```
### Drag to Reorder List Items
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from '@testdriver/sdk';
test('reorders tasks in list', async () => {
const { ai } = await chrome('https://todo-app.example.com');
// Start dragging first task
await ai.mouseDown('drag handle on first task');
// Move down to third position
await ai.hover('third task position');
// Drop the task
await ai.mouseUp();
// Verify new order
const thirdTask = await ai.find('third task in list');
expect(thirdTask.text).toContain('Original first task');
});
```
### Text Selection with Mouse
```javascript theme={null}
test('selects text with mouse drag', async () => {
const { ai } = await chrome('https://document.example.com');
// Start selection at beginning of word
await ai.mouseDown('start of "TestDriver" word');
// Drag to end of word
await ai.hover('end of "TestDriver" word');
// Complete selection
await ai.mouseUp();
// Verify selection
const selection = await ai.exec('window.getSelection().toString()');
expect(selection).toBe('TestDriver');
});
```
## Important Notes
* `mouseUp()` must be preceded by [`mouseDown()`](/v7/mouse-down) to have an effect
* Releases the button at the current cursor position
* Completes any drag or selection operation that was in progress
* For simple clicks, use [`click()`](/v7/click) instead of mouseDown/mouseUp pair
## Related Methods
* [`mouseDown()`](/v7/mouse-down) - Press mouse button without releasing
* [`hover()`](/v7/hover) - Move mouse to element
* [`click()`](/v7/click) - Complete click (mouseDown + mouseUp)
* [`doubleClick()`](/v7/double-click) - Double-click on element
* [`rightClick()`](/v7/right-click) - Right-click for context menu
# parse()
Source: https://docs.testdriver.ai/v7/parse
Detect all UI elements on screen using OmniParser
## Overview
Parse the current screen using OmniParser v2 to detect all visible UI elements. Returns structured data including element types, text content, interactivity levels, and bounding box coordinates.
This method analyzes the entire screen and returns every detected element. It's useful for:
* Understanding the full UI layout of a screen
* Finding all clickable or interactive elements
* Building custom element-based logic
* Debugging what elements TestDriver can detect
* Accessibility auditing
**Availability**: `parse()` requires an enterprise or self-hosted plan. It uses OmniParser v2 server-side for element detection.
## Syntax
```javascript theme={null}
const result = await testdriver.parse()
```
## Parameters
None.
## Returns
`Promise` - Object containing detected UI elements
### ParseResult
| Property | Type | Description |
| ------------------- | ----------------- | --------------------------------------------------- |
| `elements` | `ParsedElement[]` | Array of detected UI elements |
| `annotatedImageUrl` | `string` | URL of the annotated screenshot with bounding boxes |
| `imageWidth` | `number` | Width of the analyzed screenshot |
| `imageHeight` | `number` | Height of the analyzed screenshot |
### ParsedElement
| Property | Type | Description |
| --------------- | -------- | ------------------------------------------------------------- |
| `index` | `number` | Element index |
| `type` | `string` | Element type (e.g. `"text"`, `"icon"`, `"button"`) |
| `content` | `string` | Text content or description of the element |
| `interactivity` | `string` | Interactivity level (e.g. `"clickable"`, `"non-interactive"`) |
| `bbox` | `object` | Bounding box in pixel coordinates `{x0, y0, x1, y1}` |
| `boundingBox` | `object` | Bounding box as `{left, top, width, height}` |
## Examples
### Get All Elements on Screen
```javascript theme={null}
const result = await testdriver.parse();
console.log(`Found ${result.elements.length} elements`);
result.elements.forEach((el, i) => {
console.log(`${i + 1}. [${el.type}] "${el.content}" (${el.interactivity})`);
});
```
### Find Clickable Elements
```javascript theme={null}
const result = await testdriver.parse();
const clickable = result.elements.filter(e => e.interactivity === 'clickable');
console.log(`Found ${clickable.length} clickable elements`);
clickable.forEach(el => {
console.log(`- "${el.content}" at (${el.bbox.x0}, ${el.bbox.y0})`);
});
```
### Find and Click an Element by Content
```javascript theme={null}
const result = await testdriver.parse();
// Find a "Submit" button
const submitBtn = result.elements.find(e =>
e.content.toLowerCase().includes('submit') && e.interactivity === 'clickable'
);
if (submitBtn) {
// Calculate center of the bounding box
const x = Math.round((submitBtn.bbox.x0 + submitBtn.bbox.x1) / 2);
const y = Math.round((submitBtn.bbox.y0 + submitBtn.bbox.y1) / 2);
await testdriver.click({ x, y });
}
```
### Filter by Element Type
```javascript theme={null}
const result = await testdriver.parse();
// Get all text elements
const textElements = result.elements.filter(e => e.type === 'text');
textElements.forEach(e => console.log(`Text: "${e.content}"`));
// Get all icons
const icons = result.elements.filter(e => e.type === 'icon');
console.log(`Found ${icons.length} icons`);
// Get all buttons
const buttons = result.elements.filter(e => e.type === 'button');
console.log(`Found ${buttons.length} buttons`);
```
### Build Custom Assertions
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Login Page", () => {
it("should have expected form elements", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({
url: 'https://myapp.com/login',
});
const result = await testdriver.parse();
// Assert expected elements exist
const textContent = result.elements.map(e => e.content.toLowerCase());
expect(textContent).toContain('email');
expect(textContent).toContain('password');
// Assert there are clickable elements
const clickable = result.elements.filter(e => e.interactivity === 'clickable');
expect(clickable.length).toBeGreaterThan(0);
});
});
```
### Use Bounding Box Coordinates
```javascript theme={null}
const result = await testdriver.parse();
result.elements.forEach(el => {
// Pixel coordinates
console.log(`Element "${el.content}":`);
console.log(` bbox: (${el.bbox.x0}, ${el.bbox.y0}) to (${el.bbox.x1}, ${el.bbox.y1})`);
console.log(` size: ${el.boundingBox.width}x${el.boundingBox.height}`);
console.log(` position: left=${el.boundingBox.left}, top=${el.boundingBox.top}`);
});
```
### View Annotated Screenshot
```javascript theme={null}
const result = await testdriver.parse();
// The annotated image shows all detected elements with bounding boxes
console.log('Annotated screenshot:', result.annotatedImageUrl);
console.log(`Image dimensions: ${result.imageWidth}x${result.imageHeight}`);
```
## How It Works
1. TestDriver captures a screenshot of the current screen
2. The image is sent to the TestDriver API
3. OmniParser v2 analyzes the image to detect all UI elements
4. Each element is classified by type (text, icon, button, etc.) and interactivity
5. Bounding box coordinates are returned in pixel coordinates matching the screen resolution
OmniParser detects elements visually β it works with any UI framework, native apps, and even non-standard interfaces. It does not rely on DOM or accessibility trees.
## Best Practices
For locating and interacting with a specific element, prefer `find()` which uses AI vision. Use `parse()` when you need a complete inventory of all elements on screen.
```javascript theme={null}
// Prefer this for clicking a specific element
await testdriver.find("Submit button").click();
// Use parse() for full UI analysis
const result = await testdriver.parse();
const allButtons = result.elements.filter(e => e.type === 'button');
```
Use the `interactivity` field to distinguish between clickable and non-interactive elements.
```javascript theme={null}
const result = await testdriver.parse();
const interactive = result.elements.filter(e => e.interactivity === 'clickable');
const static_ = result.elements.filter(e => e.interactivity === 'non-interactive');
```
If elements aren't being detected, the page may not be fully loaded. Add a wait first.
```javascript theme={null}
// Wait for page to stabilize
await testdriver.wait(2000);
// Then parse
const result = await testdriver.parse();
```
The `annotatedImageUrl` provides a visual overlay showing all detected elements with their bounding boxes β great for debugging.
```javascript theme={null}
const result = await testdriver.parse();
console.log('View annotated screenshot:', result.annotatedImageUrl);
```
## Related
* [find()](/v7/find) - AI-powered element location
* [assert()](/v7/assert) - Make AI-powered assertions about screen state
* [screenshot()](/v7/screenshot) - Capture screenshots
* [Elements Reference](/v7/elements) - Complete Element API
# Adapt
Source: https://docs.testdriver.ai/v7/performing-actions
Perform actions and handle dynamic, async UI so tests adapt to change
Real apps move, load, and change. Adapt your tests to handle it.
Once you've [generated](/v7/generating-tests) and [learned](/v7/caching) your tests and gotten them [running](/v7/copilot/running-tests), the next challenge is the real world: buttons appear after a spinner, pages navigate, animations play, and content streams in over the network. To keep tests reliable, you need to perform the right actions and handle timing so your tests adapt to how the UI actually behaves instead of breaking.
## Performing Actions
TestDriver provides a variety of actions you can perform, like [clicking](/v7/click), [typing](/v7/type), [hovering](/v7/hover), and [scrolling](/v7/scroll). For a full list, see the [API Reference](/v7/click).
```javascript theme={null}
// Clicking
await testdriver.find('submit button').click();
await testdriver.find('file item').doubleClick();
await testdriver.find('text area').rightClick();
// Typing
await testdriver.find('email input').type('user@example.com');
await testdriver.find('password input').type('secret', { secret: true });
// Keyboard shortcuts
await testdriver.pressKeys(['enter']);
await testdriver.pressKeys(['ctrl', 'c']);
// Hovering
await testdriver.find('dropdown menu').hover();
// Scrolling
await testdriver.scroll('down', 500);
// Waiting
await testdriver.wait(2000); // Wait 2 seconds for animation/state change
// Extracting information from screen
const price = await testdriver.extract('the total price');
const orderNumber = await testdriver.extract('the order confirmation number');
```
### Chaining Actions
TestDriver supports method chaining for cleaner code:
```javascript theme={null}
// Chain find() with actions
const button = await testdriver.find('submit button').click();
```
Or save element reference for later use:
```javascript theme={null}
const button = await testdriver.find('submit button');
await button.click();
```
## Waiting for Dynamic Content
By default, `find()` automatically polls for up to 10 seconds, retrying every 5 seconds until the element is found. This means most elements that appear after short async operations will be found without any extra configuration.
For longer operations, increase the `timeout`:
```javascript theme={null}
// Default behavior - polls for up to 10 seconds automatically
const element = await testdriver.find('Loading complete indicator');
await element.click();
// Wait up to 30 seconds for slower operations
const element = await testdriver.find('Loading complete indicator', { timeout: 30000 });
await element.click();
// Useful after actions that trigger loading states
await testdriver.find('submit button').click();
await testdriver.find('success message', { timeout: 15000 });
// Disable polling for instant checks
const toast = await testdriver.find('notification toast', { timeout: 0 });
```
### Flake Prevention
TestDriver automatically waits for the screen and network to stabilize after each action using **redraw detection**. This prevents flaky tests caused by animations, loading states, or dynamic content updates.
Redraw detection adds a small delay after each action but significantly reduces test flakiness.
For example, when clicking a submit button that navigates to a new page:
```javascript theme={null}
// Click submit - TestDriver automatically waits for the new page to load
await testdriver.find('submit button').click();
// By the time this runs, the page has fully loaded and stabilized
await testdriver.assert('dashboard is displayed');
await testdriver.find('welcome message');
```
Without redraw detection, you'd need manual waits or retries to handle the page transition. TestDriver handles this automatically by detecting when the screen stops changing and network requests complete.
You can disable redraw detection or customize its behavior:
```javascript theme={null}
// Disable redraw detection for faster tests (less reliable)
const testdriver = TestDriver(context, {
redraw: false
});
```
Here is an example of customizing redraw detection:
```javascript theme={null}
// Fine-tune redraw detection
const testdriver = TestDriver(context, {
redraw: {
enabled: true,
diffThreshold: 0.1, // Pixel difference threshold (0-1)
screenRedraw: true, // Monitor screen changes
networkMonitor: true, // Wait for network idle
}
});
```
### Simple Delays with `wait()`
For simple pauses β waiting for animations, transitions, or state changes after an action β use `wait()`:
```javascript theme={null}
// Wait for an animation to complete
await testdriver.find('menu toggle').click();
await testdriver.wait(2000);
// Wait for a page transition to settle
await testdriver.find('next page button').click();
await testdriver.wait(1000);
```
For waiting for specific **elements** to appear, prefer `find()` with a `timeout` option. Use `wait()` only for simple time-based pauses.
Once your tests can reliably act on a changing UI and [assert](/v7/making-assertions) the results, the next step is figuring out what happened when something does go wrong.
Use screenshots and run output to see exactly what your test saw and pinpoint failures.
# pressKeys()
Source: https://docs.testdriver.ai/v7/press-keys
Press keyboard keys and shortcuts
## Overview
Press one or more keyboard keys simultaneously, useful for keyboard shortcuts, navigation, and special keys.
## Syntax
```javascript theme={null}
await testdriver.pressKeys(keys)
```
## Parameters
Array of keys to press simultaneously
## Returns
`Promise`
## Common Keys
### Special Keys
* `'enter'`, `'tab'`, `'escape'`, `'backspace'`, `'delete'`
* `'space'`, `'up'`, `'down'`, `'left'`, `'right'`
* `'home'`, `'end'`, `'pageup'`, `'pagedown'`
### Modifier Keys
* `'ctrl'`, `'alt'`, `'shift'`
* `'command'` (macOS), `'win'` (Windows)
* `'ctrlleft'`, `'ctrlright'`, `'shiftleft'`, `'shiftright'`
### Function Keys
* `'f1'` through `'f24'`
## Examples
### Navigation
```javascript theme={null}
// Tab to next field
await testdriver.pressKeys(['tab']);
// Shift+Tab to previous field
await testdriver.pressKeys(['shift', 'tab']);
// Arrow keys
await testdriver.pressKeys(['down']);
await testdriver.pressKeys(['up']);
await testdriver.pressKeys(['left']);
await testdriver.pressKeys(['right']);
// Home/End
await testdriver.pressKeys(['home']); // Start of line
await testdriver.pressKeys(['end']); // End of line
// Page navigation
await testdriver.pressKeys(['pagedown']);
await testdriver.pressKeys(['pageup']);
```
### Keyboard Shortcuts
```javascript theme={null}
// Copy (Ctrl+C / Cmd+C)
await testdriver.pressKeys(['ctrl', 'c']);
// Paste (Ctrl+V / Cmd+V)
await testdriver.pressKeys(['ctrl', 'v']);
// Save (Ctrl+S)
await testdriver.pressKeys(['ctrl', 's']);
// Select All (Ctrl+A)
await testdriver.pressKeys(['ctrl', 'a']);
// Undo (Ctrl+Z)
await testdriver.pressKeys(['ctrl', 'z']);
// Redo (Ctrl+Y)
await testdriver.pressKeys(['ctrl', 'y']);
// Find (Ctrl+F)
await testdriver.pressKeys(['ctrl', 'f']);
// New tab (Ctrl+T)
await testdriver.pressKeys(['ctrl', 't']);
// Close tab (Ctrl+W)
await testdriver.pressKeys(['ctrl', 'w']);
// Refresh (F5 or Ctrl+R)
await testdriver.pressKeys(['f5']);
await testdriver.pressKeys(['ctrl', 'r']);
```
### System Shortcuts
```javascript theme={null}
// Alt+Tab (Windows - switch apps)
await testdriver.pressKeys(['alt', 'tab']);
// Alt+F4 (Windows - close window)
await testdriver.pressKeys(['alt', 'f4']);
// Win+D (Windows - show desktop)
await testdriver.pressKeys(['winleft', 'd']);
// Win+L (Windows - lock screen)
await testdriver.pressKeys(['winleft', 'l']);
// Cmd+Tab (macOS - switch apps)
await testdriver.pressKeys(['command', 'tab']);
// Cmd+Q (macOS - quit app)
await testdriver.pressKeys(['command', 'q']);
```
### Form Submission
```javascript theme={null}
// Submit form
await testdriver.pressKeys(['enter']);
// Cancel/Close
await testdriver.pressKeys(['escape']);
// Check checkbox
await testdriver.pressKeys(['space']);
```
### Text Editing
```javascript theme={null}
// Delete selected text
await testdriver.pressKeys(['delete']);
// Backspace
await testdriver.pressKeys(['backspace']);
// Select all and delete
await testdriver.pressKeys(['ctrl', 'a']);
await testdriver.pressKeys(['delete']);
// Cut text
await testdriver.pressKeys(['ctrl', 'x']);
```
## Best Practices
**Wait after shortcuts**
Some keyboard shortcuts trigger animations or navigation:
```javascript theme={null}
await testdriver.pressKeys(['ctrl', 't']); // New tab
await new Promise(r => setTimeout(r, 500)); // Wait for tab
await testdriver.pressKeys(['ctrl', 'l']); // Focus URL bar
```
**Use Tab for form navigation**
Tab is more reliable than clicking multiple fields:
```javascript theme={null}
const firstField = await testdriver.find('email input');
await firstField.click();
await testdriver.type('user@example.com');
await testdriver.pressKeys(['tab']);
await testdriver.type('password123');
await testdriver.pressKeys(['tab']);
await testdriver.pressKeys(['enter']); // Submit
```
**Platform-specific keys**
Use the appropriate modifier key for the platform:
* Windows/Linux: `'ctrl'`
* macOS: `'command'`
```javascript theme={null}
// For cross-platform, you might need to detect OS
const modKey = process.platform === 'darwin' ? 'command' : 'ctrl';
await testdriver.pressKeys([modKey, 'c']); // Copy
```
## Use Cases
```javascript theme={null}
// Fill form using Tab
const firstField = await testdriver.find('name field');
await firstField.click();
await testdriver.type('John Doe');
await testdriver.pressKeys(['tab']);
await testdriver.type('john@example.com');
await testdriver.pressKeys(['tab']);
await testdriver.type('555-0123');
await testdriver.pressKeys(['tab']);
await testdriver.pressKeys(['enter']); // Submit
```
```javascript theme={null}
const textArea = await testdriver.find('comment textarea');
await textArea.click();
// Select all existing text
await testdriver.pressKeys(['ctrl', 'a']);
// Copy it
await testdriver.pressKeys(['ctrl', 'c']);
// Type new text
await testdriver.type('New comment');
// Undo if needed
await testdriver.pressKeys(['ctrl', 'z']);
```
```javascript theme={null}
// Open new tab
await testdriver.pressKeys(['ctrl', 't']);
await new Promise(r => setTimeout(r, 500));
// Focus address bar
await testdriver.pressKeys(['ctrl', 'l']);
await testdriver.type('https://example.com');
await testdriver.pressKeys(['enter']);
// Refresh page
await testdriver.pressKeys(['f5']);
// Close tab
await testdriver.pressKeys(['ctrl', 'w']);
```
```javascript theme={null}
// Save document
await testdriver.pressKeys(['ctrl', 's']);
// Print
await testdriver.pressKeys(['ctrl', 'p']);
// Find in page
await testdriver.pressKeys(['ctrl', 'f']);
await testdriver.type('search term');
await testdriver.pressKeys(['escape']); // Close find
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it } from 'vitest';
import TestDriver from 'testdriverai';
describe('Keyboard Navigation', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should navigate form with keyboard', async () => {
await testdriver.focusApplication('Google Chrome');
// Find first field
const emailField = await testdriver.find('email input');
await emailField.click();
await testdriver.type('user@example.com');
// Tab through fields
await testdriver.pressKeys(['tab']);
await testdriver.type('John');
await testdriver.pressKeys(['tab']);
await testdriver.type('Doe');
await testdriver.pressKeys(['tab']);
await testdriver.type('password123');
// Submit with Enter
await testdriver.pressKeys(['tab']);
await testdriver.pressKeys(['enter']);
await testdriver.assert('form submitted successfully');
});
it('should use keyboard shortcuts', async () => {
// Open new browser tab
await testdriver.pressKeys(['ctrl', 't']);
await new Promise(r => setTimeout(r, 500));
// Focus address bar
await testdriver.pressKeys(['ctrl', 'l']);
await testdriver.type('https://example.com');
await testdriver.pressKeys(['enter']);
await new Promise(r => setTimeout(r, 2000));
// Select all page content
await testdriver.pressKeys(['ctrl', 'a']);
// Copy
await testdriver.pressKeys(['ctrl', 'c']);
// Refresh page
await testdriver.pressKeys(['f5']);
});
});
```
## Related Methods
* [`type()`](/v7/type) - Type text
* [`click()`](/v7/click) - Click elements
* [`scroll()`](/v7/scroll) - Scroll pages
# Quick Start
Source: https://docs.testdriver.ai/v7/quickstart
Run your first computer-use test in minutes.
TestDriver makes it easy to write automated computer-use tests for web browsers, desktop apps, and more. Follow the directions below to run your first TestDriver test.
Join our Discord if you have any questions or need help getting started!
Drop-in UI testing for any GitHub repository. Mention `@testdriverai` anywhere in your repo and it writes UI tests and catches regressions before they merge.
Install the TestDriver GitHub app and start testing in minutes β no setup required.
Click **Add to GitHub** above and install TestDriver on the repositories you want to test.
Open a pull request or issue and mention `@testdriverai` to have it write and run UI tests:
```
@testdriverai Write a test that verifies the homepage loads and the signup button works.
```
TestDriver spawns a sandbox, writes the test, and posts results right in the conversation.
Once your tests are committed, TestDriver runs them on every pull request and flags regressions before they merge.
Want to use TestDriver from GitHub Copilot or the GitHub Mobile app instead? See the full GitHub guide:
Use TestDriver from GitHub web, Copilot chat, PR reviews, and mobile.
Get started quickly with the TestDriver CLI.
Use `npx` to quickly set up an example project:
```bash theme={null}
npx testdriverai init
```
This will walk you through creating a new project folder, installing dependencies, setting up your API key, and configuring MCP for your preferred AI assistant (VS Code, Cursor, Claude Desktop, etc.).
TestDriver uses Vitest as the test runner. To run your test, use:
```bash theme={null}
vitest run
```
This will spawn a sandbox, launch Chrome, and run the example test!
Plug TestDriver into your AI client so you can write, run, and debug real end-to-end tests right from chat. There are three pieces, and `testdriverai init` installs all of them for you:
* **The agent** β an expert test-creator that drives a live sandbox, writes code after each step, and reruns the test until it passes.
* **Skills** β small instruction files that teach the agent the exact syntax for each TestDriver capability (`find`, `click`, `type`, `assert`, β¦).
* **The MCP server** β exposes TestDriver's computer-use tools over the [Model Context Protocol](https://modelcontextprotocol.io) so any MCP-capable client can use them.
### Quick install (recommended)
`testdriverai init` wires up the agent, skills, and MCP server for you, writing each client's config in the exact format and location it expects:
```bash theme={null}
# interactive β pick your client(s)
npx testdriverai init
# one client
npx testdriverai init --client claude-code
# several
npx testdriverai init --client claude-code,cursor,vscode
# everything
npx testdriverai init --client all
```
`init` detects clients already present in your project and pre-selects them in the picker. Re-running `init` is safe β it merges the TestDriver entry into existing config without overwriting your other servers.
You'll need a TestDriver API key. Create one at [console.testdriver.ai/team](https://console.testdriver.ai/team) and `init` will save it to `.env` as `TD_API_KEY`.
### The agent
The **TestDriver agent** is an expert test-creator that runs inside your AI client (Claude Code, Cursor, VS Code, and others). It writes, runs, and debugs real end-to-end tests by driving your app the same way a person would β using AI vision to find elements, click, type, and assert β through the TestDriver MCP server.
Unlike a chat assistant that only suggests code, the agent works **iteratively against a live sandbox**: it starts a session, performs each action, writes the generated code to your test file, verifies the result with a screenshot, and reruns the test until it passes.
During init you'll be asked which AI client(s) to install into. The agent is written to the location each client expects:
| Client | Agent location |
| ----------------- | ------------------------------------ |
| Claude Code | `.claude/agents/testdriver.md` |
| VS Code (Copilot) | `.github/agents/testdriver.agent.md` |
| Cursor | `.cursor/rules/testdriver.mdc` |
| Windsurf | `.windsurf/rules/testdriver.md` |
| Codex | `AGENTS.md` |
| Zed | `.rules` |
Once installed, invoke it from your client's chat:
```text theme={null}
@testdriver write a test that logs in and verifies the dashboard loads
```
The agent will spin up a sandbox, perform the steps live, write them into a test file under `tests/`, and run it for you.
### Skills
**Skills** are small, focused instruction files β one per TestDriver capability β that teach your AI client exactly how to use each part of the TestDriver SDK and MCP tools. They follow the [Anthropic `SKILL.md` format](https://code.claude.com/docs/en/skills): a folder per skill, each containing a `SKILL.md` with YAML frontmatter and a markdown body.
There are **106 skills**, generated directly from the TestDriver documentation, covering every action and concept: `find`, `click`, `type`, `assert`, `check`, `scroll`, `press-keys`, `provision`, caching, secrets, CI/CD, and more. They're written to the directory each client expects:
| Client | Skills location |
| --------------------------- | ---------------------------------------- |
| Claude Code | `.claude/skills//SKILL.md` |
| Zed | `.agents/skills//SKILL.md` |
| Codex | referenced from `AGENTS.md` |
| VS Code Β· Cursor Β· Windsurf | folded into the agent rules/instructions |
Skills are **generated, not hand-edited** β each is built from a `.mdx` docs page and carries a `DO NOT EDIT` marker. To change a skill, edit the corresponding documentation page and run `node docs/_scripts/generate-skills.js`.
### MCP server
The **TestDriver MCP server** exposes TestDriver's computer-use tools β `session_start`, `find`, `click`, `type`, `assert`, `check`, `screenshot`, and more β over the [Model Context Protocol](https://modelcontextprotocol.io). It runs as a local stdio process:
```bash theme={null}
npx -p testdriverai testdriverai-mcp
```
and authenticates with your `TD_API_KEY`. `testdriverai init` configures it for you, but you can also wire it up by hand:
| Client | Auto-install | MCP config file | Config key |
| ----------------- | ------------ | ------------------------------------- | ----------------- |
| Claude Code | β | `.mcp.json` | `mcpServers` |
| Claude Desktop | β | OS-specific | `mcpServers` |
| Cursor | β | `.cursor/mcp.json` | `mcpServers` |
| VS Code (Copilot) | β | `.vscode/mcp.json` | `servers` |
| Windsurf | β | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` |
| Codex | β | `~/.codex/config.toml` | `[mcp_servers]` |
| Zed | β | `.zed/settings.json` | `context_servers` |
| Lovable | βοΈ partial | GitHub `AGENTS.md` + UI | β |
| Replit | βοΈ partial | `replit.md` + UI | β |
| v0 (Vercel) | π manual | web UI only | β |
Each client uses a **different top-level key** for MCP servers. The most common mistake when configuring by hand is using `mcpServers` for VS Code (it wants `servers`), Codex (TOML `[mcp_servers]`), or Zed (`context_servers`).
Add to `.mcp.json` at your project root (or `~/.claude.json` for all projects):
```json theme={null}
{
"mcpServers": {
"testdriver": {
"type": "stdio",
"command": "npx",
"args": ["-p", "testdriverai", "testdriverai-mcp"],
"env": { "TD_API_KEY": "${TD_API_KEY}" }
}
}
}
```
Edit the Claude Desktop config file:
* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
* **Linux:** `~/.config/Claude/claude_desktop_config.json`
```json theme={null}
{
"mcpServers": {
"testdriver": {
"command": "npx",
"args": ["-p", "testdriverai", "testdriverai-mcp"],
"env": { "TD_API_KEY": "your_api_key" }
}
}
}
```
Restart Claude Desktop after saving.
Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
```json theme={null}
{
"mcpServers": {
"testdriver": {
"type": "stdio",
"command": "npx",
"args": ["-p", "testdriverai", "testdriverai-mcp"],
"env": { "TD_API_KEY": "${TD_API_KEY}" }
}
}
}
```
Add to `.vscode/mcp.json`. VS Code uses the `servers` key and an `inputs` prompt for secrets:
```json theme={null}
{
"servers": {
"testdriver": {
"type": "stdio",
"command": "npx",
"args": ["-p", "testdriverai", "testdriverai-mcp"],
"env": { "TD_API_KEY": "${input:testdriver-api-key}" }
}
},
"inputs": [
{
"type": "promptString",
"id": "testdriver-api-key",
"description": "TestDriver API Key From https://console.testdriver.ai/team",
"password": true
}
]
}
```
Windsurf reads MCP config globally. Add to `~/.codeium/windsurf/mcp_config.json`:
```json theme={null}
{
"mcpServers": {
"testdriver": {
"command": "npx",
"args": ["-p", "testdriverai", "testdriverai-mcp"],
"env": { "TD_API_KEY": "${TD_API_KEY}" }
}
}
}
```
Codex uses TOML. Add to `~/.codex/config.toml`:
```toml theme={null}
[mcp_servers.testdriver]
command = "npx"
args = ["-p", "testdriverai", "testdriverai-mcp"]
env = { TD_API_KEY = "${TD_API_KEY}" }
```
Zed calls them "context servers". Add to `.zed/settings.json` (project) or `~/.config/zed/settings.json` (global):
```json theme={null}
{
"context_servers": {
"testdriver": {
"command": "npx",
"args": ["-p", "testdriverai", "testdriverai-mcp"],
"env": { "TD_API_KEY": "${TD_API_KEY}" }
}
}
}
```
**Web-based clients** β Lovable, Replit, and v0 run in the browser, so the MCP server can't be launched as a local process. Configure them through each product's UI:
1. Connect your GitHub repo and run `npx testdriverai init --client lovable` β this writes `AGENTS.md` and the skills into the repo so Lovable's agent picks them up.
2. In Lovable, open **Settings β MCP** and add the TestDriver server.
1. Run `npx testdriverai init --client replit` to write `replit.md` with the TestDriver agent guidance.
2. In Replit, open **Tools β Integrations β MCP** and add a custom MCP server.
v0 is fully UI-driven and does not read repo files.
1. Open **[v0.app/chat/settings/mcp-connections](https://v0.app/chat/settings/mcp-connections)** and add the TestDriver MCP connection.
2. Paste the agent guidance into **Instructions** (the **+** in the prompt bar).
### Verifying the install
Open your client's chat and ask the agent to write a test:
```text theme={null}
@testdriver write a test that opens the homepage and asserts the title
```
If the MCP server is wired up correctly, the agent will start a session and you'll see screenshots come back as it works. If tools don't appear, check that `TD_API_KEY` is set and restart the client.
Install TestDriver and manually create the files yourself.
You will need a TestDriver account to get an API key.
Start with 60 free device minutes, no credit-card required!
Install Vitest and TestDriver as dev dependencies:
```bash theme={null}
npm install --save-dev vitest testdriverai
```
In your project root, create a `vitest.config.js` file with the following content:
```js vitest.config.js theme={null}
import TestDriver from 'testdriverai/vitest';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
testTimeout: 900000,
hookTimeout: 900000,
reporters: [
'default',
TestDriver()
],
setupFiles: ['testdriverai/vitest/setup'],
},
});
```
Add your API key to the example test file below and save it as `test.mjs` in your project root.
```js test.mjs highlight={9} theme={null}
import { describe, expect, it } from "vitest";
// Import TestDriver from the vitest hooks
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Google Search Example", () => {
it("should search for TestDriver", async (context) => {
// Create TestDriver instance - automatically connects to sandbox
const testdriver = TestDriver(context, {
apiKey: 'YOUR_API_KEY_HERE' // supply your API key here
});
// Provision Chrome browser with a URL
// This also starts dashcam recording automatically
await testdriver.provision.chrome({ url: "https://duckduckgo.com" });
// Find and interact with elements using natural language
const searchBox = await testdriver.find("DuckDuckGo search input field");
await searchBox.click();
// Type into the focused element
await testdriver.type("testdriver.ai");
// Press Enter to search
await testdriver.pressKeys(["enter"]);
// Assert something is visible on the page
const result = await testdriver.assert("search results are displayed");
expect(result).toBeTruthy();
});
});
```
TestDriver uses Vitest as the test runner. To run your test, use:
```bash theme={null}
vitest run
```
This will spawn a sandbox, launch Chrome, and run the example test!
# Reusable Code Snippets
Source: https://docs.testdriver.ai/v7/reusable-code
Build maintainable test suites with reusable code patterns
As your test suite grows, you'll want to extract common patterns into reusable code. This keeps tests DRY, readable, and easy to maintain.
## Helper Functions
The simplest approach is extracting common actions into helper functions. Create a `helpers/` directory for shared utilities:
```javascript test/helpers/auth.js theme={null}
export async function login(testdriver, { email, password }) {
const emailInput = await testdriver.find('email input');
await emailInput.click();
await testdriver.type(email);
const passwordInput = await testdriver.find('password input');
await passwordInput.click();
await testdriver.type(password);
const loginButton = await testdriver.find('login button');
await loginButton.click();
const result = await testdriver.assert('user is logged in');
return result;
}
export async function logout(testdriver) {
const userMenu = await testdriver.find('user menu');
await userMenu.click();
const logoutButton = await testdriver.find('logout button');
await logoutButton.click();
}
```
**Avoid hardcoding dynamic values in element descriptions.** Element selectors should describe the *type* of element, not specific content that might change.
**β Bad:** `await testdriver.find('profile name TestDriver in the top right')`\
**β Good:** `await testdriver.find('user profile name in the top right')`
Hardcoded values like usernames, product names, or prices will cause tests to fail when the data changes. Use generic descriptions that work regardless of the specific content displayed.
Now import and use these helpers in any test:
```javascript test/checkout.test.mjs theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
import { login } from './helpers/auth.js';
describe("Checkout", () => {
it("should complete checkout as logged in user", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({
url: 'https://shop.example.com',
});
// Use the helper
await login(testdriver, {
email: 'user@example.com',
password: 'password123'
});
// Continue with checkout steps...
const cartButton = await testdriver.find('cart button');
await cartButton.click();
});
});
```
## Page Objects
For larger test suites, the Page Object pattern encapsulates all interactions with a specific page or component:
```javascript test/pages/LoginPage.js theme={null}
export class LoginPage {
constructor(testdriver) {
this.td = testdriver;
}
async enterEmail(email) {
const input = await this.td.find('email input');
await input.click();
await this.td.type(email);
}
async enterPassword(password) {
const input = await this.td.find('password input');
await input.click();
await this.td.type(password);
}
async submit() {
const button = await this.td.find('submit button');
await button.click();
}
async login(email, password) {
await this.enterEmail(email);
await this.enterPassword(password);
await this.submit();
}
async assertError(message) {
return await this.td.assert(`error message shows "${message}"`);
}
async assertLoggedIn() {
return await this.td.assert('user dashboard is visible');
}
}
```
Use the page object in your tests:
```javascript test/auth.test.mjs theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
import { LoginPage } from './pages/LoginPage.js';
describe("Authentication", () => {
it("should show error for invalid credentials", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({
url: 'https://app.example.com/login',
});
const loginPage = new LoginPage(testdriver);
await loginPage.login('invalid@test.com', 'wrongpassword');
const hasError = await loginPage.assertError('Invalid credentials');
expect(hasError).toBeTruthy();
});
it("should redirect to dashboard on success", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({
url: 'https://app.example.com/login',
});
const loginPage = new LoginPage(testdriver);
await loginPage.login('valid@test.com', 'correctpassword');
const isLoggedIn = await loginPage.assertLoggedIn();
expect(isLoggedIn).toBeTruthy();
});
});
```
## Shared Test Fixtures
Create reusable fixtures for common test setup scenarios:
```javascript test/fixtures/index.js theme={null}
export const testUsers = {
admin: { email: 'admin@example.com', password: 'admin123' },
regular: { email: 'user@example.com', password: 'user123' },
guest: { email: 'guest@example.com', password: 'guest123' },
};
export const testUrls = {
staging: 'https://staging.example.com',
production: 'https://example.com',
};
export async function setupAuthenticatedSession(testdriver, user = testUsers.regular) {
const emailInput = await testdriver.find('email input');
await emailInput.click();
await testdriver.type(user.email);
const passwordInput = await testdriver.find('password input');
await passwordInput.click();
await testdriver.type(user.password);
const loginButton = await testdriver.find('login button');
await loginButton.click();
await testdriver.assert('user is logged in');
}
```
```javascript test/admin.test.mjs theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
import { testUsers, testUrls, setupAuthenticatedSession } from './fixtures/index.js';
describe("Admin Panel", () => {
it("should access admin settings", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({
url: `${testUrls.staging}/login`,
});
await setupAuthenticatedSession(testdriver, testUsers.admin);
const settingsLink = await testdriver.find('admin settings link');
await settingsLink.click();
const result = await testdriver.assert('admin settings panel is visible');
expect(result).toBeTruthy();
});
});
```
## Suggested Project Structure
| Folder | Purpose |
| ----------- | ----------------------------- |
| `fixtures/` | Test data and setup utilities |
| `helpers/` | Reusable helper functions |
| `pages/` | Page object classes |
| `specs/` | Test files |
Start simple with helper functions. Only introduce page objects when you find yourself duplicating the same element interactions across multiple tests.
# rightClick
Source: https://docs.testdriver.ai/v7/right-click
Perform a right-click action to open context menus
## Overview
The `rightClick()` method performs a right-click action on an element, typically used to open context menus. You can either call it on an [`Element`](/v7/core-concepts/elements) instance or use it directly with a selector.
## Syntax
```javascript theme={null}
// Right-click on an element
await element.rightClick();
// Right-click using a selector
await ai.rightClick('selector');
```
## Parameters
When called on an `Element`, no parameters are required.
When called directly on the AI client:
| Parameter | Type | Description |
| ---------- | -------- | -------------------------------------------------- |
| `selector` | `string` | The selector describing the element to right-click |
## Returns
Returns a `Promise` that resolves when the right-click action completes.
## Examples
### Right-Click to Open Context Menu
```javascript theme={null}
const fileItem = await ai.find('README.md file');
await fileItem.rightClick();
// Select menu option
await ai.click('Delete from context menu');
```
### Direct Right-Click with Selector
```javascript theme={null}
await ai.rightClick('image in the gallery');
await ai.click('Save image as');
```
### VS Code Context Menu
```javascript theme={null}
import { test } from 'vitest';
import { vscode } from '@testdriver/sdk';
test('renames a file via context menu', async () => {
const { ai } = await vscode();
// Right-click on a file
await ai.rightClick('test.js in the file explorer');
// Click rename option
await ai.click('Rename');
// Type new name
await ai.type('test.spec.js');
await ai.pressKeys('Enter');
// Verify rename
const renamedFile = await ai.find('test.spec.js in the file explorer');
expect(renamedFile).toBeTruthy();
});
```
### Browser Context Menu
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from '@testdriver/sdk';
test('opens link in new tab', async () => {
const { ai } = await chrome('https://example.com');
// Right-click on a link
await ai.rightClick('Documentation link');
// Select "Open in new tab"
await ai.click('Open link in new tab');
});
```
### Custom Context Menu in Web App
```javascript theme={null}
test('uses custom context menu', async () => {
const { ai } = await chrome('https://app.example.com');
// Right-click on custom element
await ai.rightClick('project item in the list');
// Wait for custom menu to appear
await ai.find('custom context menu');
// Click menu option
await ai.click('Duplicate project');
// Verify duplication
const duplicatedProject = await ai.find('project item (copy)');
expect(duplicatedProject).toBeTruthy();
});
```
## Related Methods
* [`click()`](/v7/click) - Single click on an element
* [`doubleClick()`](/v7/double-click) - Double-click on an element
* [`mouseDown()`](/v7/mouse-down) - Press mouse button without releasing
* [`mouseUp()`](/v7/mouse-up) - Release mouse button
* [`hover()`](/v7/hover) - Move mouse over element without clicking
# screenshot()
Source: https://docs.testdriver.ai/v7/screenshot
Capture and save screenshots during test execution
## Overview
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.
**Automatic Screenshots**: TestDriver can automatically capture screenshots before and after every command (click, type, find, etc.). These are saved with descriptive filenames like `001-click-before-L42-submit-button.png` that include the line number from your test file. Enable this with `autoScreenshots: true` in your TestDriver options.
## Syntax
```javascript theme={null}
const filePath = await testdriver.screenshot(filename)
```
## Parameters
Custom filename for the screenshot (without .png extension). If not provided, a timestamp-based filename is generated automatically.
## Returns
`Promise` - The absolute file path where the screenshot was saved
## File Organization
Screenshots are automatically saved to `.testdriver/screenshots//` in your project root:
```
.testdriver/
screenshots/
login.test/
001-find-before-L15-email-input.png # Auto: before find()
002-find-after-L15-email-input.png # Auto: after find()
003-click-before-L16-email-input.png # Auto: before click()
004-click-after-L16-email-input.png # Auto: after click()
005-type-before-L17-userexamplecom.png # Auto: before type()
006-type-after-L17-userexamplecom.png # Auto: after type()
custom-screenshot.png # Manual: screenshot("custom-screenshot")
checkout.test/
001-find-before-L12-checkout-button.png
...
```
### Automatic Screenshot Naming
When `autoScreenshots` is enabled, filenames follow this format:
`---L-.png`
| Component | Description | Example |
| ------------- | ------------------------------------ | ----------------------- |
| `seq` | Sequential number (001, 002, ...) | `001` |
| `action` | Command name | `click`, `type`, `find` |
| `phase` | Before, after, or error | `before`, `after` |
| `L` | Line number from test file | `L42` |
| `description` | Element description or action target | `submit-button` |
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.
## Examples
### Basic Screenshot
```javascript theme={null}
// Capture a screenshot with auto-generated filename
const screenshotPath = await testdriver.screenshot();
console.log('Screenshot saved to:', screenshotPath);
```
### Custom Filename
```javascript theme={null}
// Save with a descriptive filename
await testdriver.screenshot("login-page");
// Saves to: .testdriver/screenshots//login-page.png
await testdriver.screenshot("after-click");
// Saves to: .testdriver/screenshots//after-click.png
```
### Debugging with Screenshots
```javascript theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Login Flow", () => {
it("should log in successfully", async (context) => {
const testdriver = TestDriver(context);
await testdriver.provision.chrome({
url: 'https://myapp.com/login',
});
// Capture initial state
await testdriver.screenshot();
// Fill in login form
const emailInput = await testdriver.find("email input");
await emailInput.click();
await testdriver.type("user@example.com");
// Capture state after typing
await testdriver.screenshot();
const passwordInput = await testdriver.find("password input");
await passwordInput.click();
await testdriver.type("password123");
// Capture before clicking login
await testdriver.screenshot();
const loginButton = await testdriver.find("Login button");
await loginButton.click();
// Capture after login attempt
await testdriver.screenshot();
const result = await testdriver.assert("dashboard is visible");
expect(result).toBeTruthy();
});
});
```
## Automatic Screenshots
By default, TestDriver captures screenshots **automatically** before and after every command. This creates a complete visual timeline of your test execution without any additional code.
### Enabling/Disabling
```javascript theme={null}
// Auto-screenshots enabled by default
const testdriver = TestDriver(context);
// Explicitly disable if needed (not recommended)
const testdriver = TestDriver(context, {
autoScreenshots: false
});
```
### What Gets Captured
Automatic screenshots are taken around these commands:
* `find()` / `findAll()`
* `click()` / `hover()` / `doubleClick()` / `rightClick()`
* `type()` / `pressKeys()`
* `scroll()`
* `waitForText()` / `waitForImage()`
* `focusApplication()`
* `assert()` / `extract()` / `exec()`
### Example Output
For this test code:
```javascript theme={null}
// Line 15: Find email input
const emailInput = await testdriver.find("email input");
// Line 16: Click it
await emailInput.click();
// Line 17: Type email
await testdriver.type("user@example.com");
```
TestDriver automatically saves:
```
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.png
```
If an error occurs, the phase will be `error` instead of `after`.
## Best Practices
With `autoScreenshots: true`, you get comprehensive coverage without adding manual `screenshot()` calls. Only add manual screenshots for specific named checkpoints.
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.
Take a screenshot before making assertions. If the assertion fails, you'll have a visual record of what the screen looked like.
```javascript theme={null}
await testdriver.screenshot();
const result = await testdriver.assert("checkout button is visible");
```
Add `.testdriver/screenshots/` to your `.gitignore` to avoid committing screenshots to version control:
```
# .gitignore
.testdriver/screenshots/
```
## Viewing Saved Screenshots
After saving screenshots during test execution, you can view them using TestDriver MCP commands. This is especially useful for debugging failed tests or verifying test behavior.
### MCP Commands for Screenshot Viewing
**List all saved screenshots:**
```
list_local_screenshots()
```
**View a specific screenshot:**
```
view_local_screenshot({ path: "/full/path/to/screenshot.png" })
```
These commands allow you to:
* View screenshots from failed tests to understand what went wrong
* Review test execution flow by examining screenshots in chronological order
* Compare screenshots across test runs to identify flaky behavior
For detailed workflows and examples of using these MCP commands for debugging, see the [Debugging with Screenshots](/v7/debugging-with-screenshots) guide.
## Related
* [Debugging with Screenshots](/v7/debugging-with-screenshots) - View and analyze saved screenshots using MCP
* [assert()](/v7/assert) - Make AI-powered assertions
* [find()](/v7/find) - Locate elements on screen
# scroll()
Source: https://docs.testdriver.ai/v7/scroll
Scroll pages and elements
## Overview
Scroll the page or active element in any direction using mouse wheel or keyboard.
**Focus Requirements**
Scrolling requires the page or a frame to be focused. If an input field or other interactive element has focus, scroll commands may not work as expected. Before scrolling, ensure focus is on the page by:
* Clicking on a non-interactive area (e.g., page background)
* Pressing the Escape key to unfocus interactive elements
* Clicking outside of input fields or text areas
**If scroll is still not working**, try using Page Down/Page Up keys directly:
```javascript theme={null}
await testdriver.pressKeys(['pagedown']); // Scroll down
await testdriver.pressKeys(['pageup']); // Scroll up
```
## Syntax
```javascript theme={null}
await testdriver.scroll(direction, options)
```
## Parameters
Direction to scroll: `'up'`, `'down'`
Amount to scroll in pixels
## Returns
`Promise`
## Examples
### Basic Scrolling
```javascript theme={null}
// Scroll down (default)
await testdriver.scroll();
// Scroll down 5 clicks
await testdriver.scroll('down', { amount: 5 });
// Scroll up
await testdriver.scroll('up');
// Scroll up 2 clicks
await testdriver.scroll('up', { amount: 2 });
```
### Horizontal Scrolling
```javascript theme={null}
// Scroll right
await testdriver.scroll('right', { amount: 3 });
// Scroll left
await testdriver.scroll('left', { amount: 3 });
```
### Scroll Methods
```javascript theme={null}
// Mouse wheel scroll (default)
await testdriver.scroll('down', { amount: 3 });
// For keyboard-based scrolling, use pressKeys instead
await testdriver.pressKeys(['pagedown']);
```
## Best Practices
**Ensure page has focus before scrolling**
```javascript theme={null}
// After typing in an input, unfocus it first
await testdriver.find('email input').click();
await testdriver.type('user@example.com');
// Click elsewhere or press Escape before scrolling
await testdriver.pressKeys(['escape']);
// Or click a non-interactive area
// await testdriver.find('page background').click();
// Now scroll will work properly
await testdriver.scroll('down');
// If scroll still doesn't work, use Page Down directly
// await testdriver.pressKeys(['pagedown']);
```
**Control scroll distance with the options object**
```javascript theme={null}
// For web pages, mouse scroll works well
await testdriver.scroll('down', { amount: 3 });
// For desktop apps or when mouse doesn't work, use keyboard
await testdriver.pressKeys(['pagedown']);
```
**Keyboard scroll uses Page Down/Up**
Keyboard scrolling typically moves by one "page" at a time, which may be more than the specified click amount. It's more compatible but less precise than mouse scrolling.
## Use Cases
```javascript theme={null}
// Scroll multiple times for infinite scroll
for (let i = 0; i < 5; i++) {
await testdriver.scroll('down', { amount: 5 });
await new Promise(r => setTimeout(r, 1000)); // Wait for load
}
```
```javascript theme={null}
// Navigate horizontal carousel
await testdriver.scroll('right', { amount: 3 });
await new Promise(r => setTimeout(r, 500));
const nextImage = await testdriver.find('next image in carousel');
await nextImage.click();
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it } from 'vitest';
import TestDriver from 'testdriverai';
describe('Scrolling', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should scroll to find elements', async () => {
await testdriver.focusApplication('Google Chrome');
// Scroll down the page
await testdriver.scroll('down', { amount: 5 });
// Click footer link
const privacyLink = await testdriver.find('Privacy Policy link');
await privacyLink.click();
await testdriver.assert('privacy policy page is displayed');
});
it('should handle infinite scroll', async () => {
await testdriver.focusApplication('Google Chrome');
// Scroll multiple times to load content
for (let i = 0; i < 3; i++) {
await testdriver.scroll('down', { amount: 5 });
await new Promise(r => setTimeout(r, 1500)); // Wait for load
}
// Verify content loaded
await testdriver.assert('more than 10 items are visible');
});
});
```
## Related Methods
* [`find()`](/v7/find) - Locate elements after scrolling
* [`pressKeys()`](/v7/press-keys) - Use Page Down/Up keys
* [`wait()`](/v7/wait) - Wait after scrolling
# Using Secrets
Source: https://docs.testdriver.ai/v7/secrets
Securely manage passwords and sensitive data in your tests
Protect sensitive information like passwords, API keys, and tokens in your TestDriver tests.
## Typing Secrets Securely
When typing sensitive information like passwords, use the `secret: true` option to prevent the value from being logged or stored:
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from 'testdriverai/presets';
test('login with secure password', async (context) => {
const { testdriver } = await chrome(context, {
url: 'https://myapp.com/login'
});
await testdriver.find('email input').click();
await testdriver.type(process.env.TD_USERNAME);
await testdriver.find('password input').click();
// Password is masked in logs and recordings
await testdriver.type(process.env.TD_PASSWORD, { secret: true });
await testdriver.find('login button').click();
await testdriver.assert('dashboard is visible');
});
```
When `secret: true` is set, the typed text appears as `****` in all logs, recordings, and dashcam output.
## Storing Secrets in GitHub
Store sensitive credentials as GitHub repository secrets so they're never exposed in your code:
Go to your GitHub repository β **Settings** β **Secrets and variables** β **Actions**
Click **New repository secret** and add your secrets:
* `TD_API_KEY` - Your TestDriver API key
* `TD_USERNAME` - Test account username
* `TD_PASSWORD` - Test account password
Reference secrets in your workflow file:
```yaml .github/workflows/test.yml theme={null}
- name: Run TestDriver tests
env:
TD_API_KEY: ${{ secrets.TD_API_KEY }}
TD_USERNAME: ${{ secrets.TD_USERNAME }}
TD_PASSWORD: ${{ secrets.TD_PASSWORD }}
run: vitest run
```
## Local Development
For local development, store secrets in a `.env` file:
```bash .env theme={null}
TD_API_KEY=your_api_key_here
TD_USERNAME=testuser@example.com
TD_PASSWORD=your_secure_password
```
Never commit `.env` files to version control. Add `.env` to your `.gitignore` file.
## Complete Example
Here's a full login test with proper secrets handling:
```javascript tests/login.test.js theme={null}
import { test, expect } from 'vitest';
import { chrome } from 'testdriverai/presets';
test('secure login flow', async (context) => {
const { testdriver } = await chrome(context, {
url: process.env.TD_WEBSITE || 'https://staging.myapp.com'
});
// Enter username (not sensitive)
await testdriver.find('email input').click();
await testdriver.type(process.env.TD_USERNAME);
// Enter password securely
await testdriver.find('password input').click();
await testdriver.type(process.env.TD_PASSWORD, { secret: true });
// Submit login
await testdriver.find('login button').click();
// Verify successful login
const loggedIn = await testdriver.assert('user is logged in');
expect(loggedIn).toBeTruthy();
});
```
* **Always use `secret: true`** when typing passwords, tokens, or sensitive data
* **Use environment variables** to keep secrets out of code
* **Store secrets in your CI provider** (GitHub Actions, GitLab CI, etc.)
* **Never commit secrets** to version control
* **Rotate secrets regularly** to maintain security
# Self-Hosted (Enterprise)
Source: https://docs.testdriver.ai/v7/self-hosted
Our enterprise solution with unlimited test execution, assisted setup, and dedicated support.
Self-hosted is our enterprise solution for teams that need unlimited test execution, infrastructure control, and dedicated support. Pricing is based on **parallel test capacity** with a flat license fee β no per-second billing.
Run as many tests as you want with no device-second metering. Predictable monthly costs.
Our team helps you deploy, configure, and optimize your infrastructure. Dedicated engineering support included.
Use your own AI keys, custom hardware, specific software, and network configurations. RDP into test machines for debugging.
Keep data in your environment. Air-gapped deployment available for regulated industries.
## Deployment Options
Choose the level of control you need:
| Component | Standard | Air-Gapped |
| ------------------- | ------------------------ | ------------------------------------------ |
| **Test Sandboxes** | Your AWS | Your infrastructure (any cloud or on-prem) |
| **Dashboard** | TestDriver hosted | Your infrastructure |
| **API** | TestDriver hosted | Your infrastructure |
| **AI Processing** | Your API keys | Your infrastructure |
| **Data Storage** | Your AWS account | 100% your infrastructure |
| **Network** | Internet access required | Fully air-gapped |
| **Cloud Providers** | AWS | AWS, Azure, GCP, on-prem |
### Standard Deployment
Run test sandboxes on your AWS infrastructure while using TestDriver's hosted dashboard and API:
* **Quick setup** via CloudFormation β deploy in hours
* **Dashboard access** at [console.testdriver.ai](https://console.testdriver.ai)
* **Your AI keys** β control costs with your own OpenAI, Anthropic, or other provider
* **Custom AMIs** β install specific software, configure networking
Step-by-step instructions for deploying TestDriver on your AWS infrastructure using CloudFormation.
### Air-Gapped Deployment
Deploy the entire TestDriver stack in your environment for complete isolation:
* **Full stack** β dashboard, API, and test infrastructure all in your environment
* **No external dependencies** β data never leaves your network perimeter
* **Any infrastructure** β AWS, Azure, GCP, or on-premises
* **Regulated industries** β government, defense, healthcare, finance
## Custom VM Images
Build test environments with your applications, dependencies, and user data pre-installed. You get full access to:
* **Golden VM** β our pre-configured base image with TestDriver agent, drivers, and optimizations
* **Packer scripts** β build custom AMIs with your applications, user data, and configurations
* **Faster test startup** β skip installation steps by baking dependencies into your image
* **Consistent environments** β every test runs on an identical, reproducible machine
- Install applications (browsers, desktop apps, dev tools)
- Configure user accounts and credentials
- Set up network proxies and certificates
- Install fonts, language packs, and locales
- Pre-seed databases or test fixtures
- Configure Windows/Linux settings
1. We provide our golden VM base image and Packer scripts
2. You customize the scripts to install your software and configuration
3. Run Packer to build your custom AMI
4. Configure TestDriver to use your custom AMI for test sandboxes
5. Tests spin up with everything pre-installed β no setup time wasted
## Implementation Process
Discuss your requirements, security constraints, and integration needs with our team.
Our engineers design a deployment architecture that meets your security and compliance requirements.
We work with your team to deploy TestDriver, including assisted setup and configuration.
Connect TestDriver to your CI/CD pipelines, internal tools, and workflows.
Comprehensive training for your team on operating and maintaining the deployment.
## What's Included
* **Flat license fee** per parallel test slot
* **Unlimited test execution** β no device-second charges
* **Assisted setup** β our team helps you deploy and configure
* **Dedicated support** β direct access to our engineering team
* **Custom contract terms** β volume-based pricing, custom SLAs
* **Professional services** β implementation assistance and training
## Comparison: Hosted vs Self-Hosted
| Feature | Hosted | Self-Hosted |
| ---------------------- | ------------------ | --------------------- |
| **Setup Time** | Minutes | Hours (assisted) |
| **Pricing Model** | Device-seconds | Flat license fee |
| **Infrastructure** | TestDriver | Your AWS or any cloud |
| **AI API Keys** | TestDriver's | Your own |
| **Custom Software** | Limited | Full control |
| **Hardware Selection** | Standard | Your choice |
| **Debugging Access** | Replays only | Full RDP access |
| **Support** | Community/Standard | Dedicated engineering |
| **Air-Gapped Option** | No | Yes |
## Get Started
Discuss your requirements with our team and get a custom proposal for your self-hosted deployment.
# Test Result JSON
Source: https://docs.testdriver.ai/v7/test-results-json
Per-test JSON result files with metadata, versions, and infrastructure details
## Overview
TestDriver automatically writes a JSON result file for each test case after it finishes. These files contain comprehensive metadata about the test run, including SDK and runner versions, infrastructure details, interaction statistics, and links to recordings.
Result files are written to:
```
.testdriver/results//.json
```
For example, a test file `tests/login.test.mjs` with a test named `"should log in"` produces:
```
.testdriver/results/tests/login.test.mjs/should_log_in.json
```
Test names are sanitized for filesystem use β special characters are replaced with underscores and names are truncated to 200 characters.
## Enabling
No configuration is required. The JSON files are written automatically by the TestDriver Vitest reporter plugin whenever tests run.
## JSON Schema
Each result file is organized into logical groups:
### `versions`
| Field | Type | Description |
| --------------------------- | ---------------- | ------------------------------------------------------- |
| `versions.sdk` | `string \| null` | TestDriver SDK version (e.g. `"7.8.0"`) |
| `versions.vitest` | `string \| null` | Vitest version used to run the test |
| `versions.api` | `string \| null` | TestDriver API server version |
| `versions.runnerBefore` | `string \| null` | Runner version at sandbox start |
| `versions.runnerAfter` | `string \| null` | Runner version after auto-update |
| `versions.runnerWasUpdated` | `boolean` | Whether the runner was auto-updated during provisioning |
### `test`
| Field | Type | Description |
| ----------------- | ---------------- | ------------------------------------ |
| `test.file` | `string \| null` | Relative path to the test file |
| `test.name` | `string \| null` | Name of the test case |
| `test.suite` | `string \| null` | Name of the parent `describe` block |
| `test.passed` | `boolean` | Whether the test passed |
| `test.caseId` | `string \| null` | Database ID for this test case |
| `test.runId` | `string \| null` | Database ID for the overall test run |
| `test.error` | `string \| null` | Error message if the test failed |
| `test.errorStack` | `string \| null` | Error stack trace if the test failed |
### `urls`
| Field | Type | Description |
| -------------- | ---------------- | -------------------------------------------- |
| `urls.api` | `string \| null` | API root URL used for this test |
| `urls.console` | `string \| null` | TestDriver console base URL |
| `urls.vnc` | `string \| null` | VNC URL for the sandbox |
| `urls.testRun` | `string \| null` | Direct link to this test case in the console |
### `replay`
The `replay` object contains the recording replay URL and derived embed links. The `gifUrl` and `embedUrl` are generated automatically from the replay URL.
| Field | Type | Description |
| ----------------- | ---------------- | ---------------------------------------------------------- |
| `replay.url` | `string \| null` | Recording replay URL |
| `replay.gifUrl` | `string \| null` | Animated GIF thumbnail of the recording |
| `replay.embedUrl` | `string \| null` | Embeddable replay URL (appends `&embed=true`) |
| `replay.markdown` | `string \| null` | Ready-to-use Markdown embed with GIF linking to the replay |
The `replay.markdown` field produces a clickable GIF badge you can paste directly into PR comments, README files, or issue descriptions:
```markdown theme={null}
[](https://console.testdriver.ai/replay/abc123?share=xyz)
```
### `date`
| Field | Type | Description |
| ------ | -------- | ----------------------------------------- |
| `date` | `string` | ISO 8601 timestamp when the test finished |
### `team`
| Field | Type | Description |
| ---------------- | ---------------- | ------------------------ |
| `team.id` | `string \| null` | Team ID from the sandbox |
| `team.sessionId` | `string \| null` | SDK session ID |
### `infrastructure`
| Field | Type | Description |
| ------------------------------ | ---------------- | ---------------------------------------------------------- |
| `infrastructure.sandboxId` | `string \| null` | Sandbox instance ID |
| `infrastructure.instanceId` | `string \| null` | Instance ID |
| `infrastructure.os` | `string \| null` | Operating system of the sandbox (`"linux"` or `"windows"`) |
| `infrastructure.amiId` | `string \| null` | AWS AMI ID used for provisioning |
| `infrastructure.e2bTemplateId` | `string \| null` | E2B template ID used for provisioning |
| `infrastructure.imageVersion` | `string \| null` | Sandbox image version |
### `realtime`
| Field | Type | Description |
| ----------------------- | ---------------- | ---------------------------------------------------- |
| `realtime.channel` | `string \| null` | Ably channel name used for communication |
| `realtime.messageCount` | `number` | Number of messages published to the realtime channel |
### `interactions`
| Field | Type | Description |
| --------------------- | -------- | ------------------------------------------------------------------ |
| `interactions.total` | `number` | Total number of interactions recorded |
| `interactions.cached` | `number` | Number of interactions served from cache |
| `interactions.byType` | `object` | Breakdown of interactions by type (e.g. `find`, `click`, `assert`) |
## Example Output
```json theme={null}
{
"sdkVersion": "7.8.0",
"vitestVersion": "4.0.0",
"apiVersion": "1.45.0",
"runnerVersionBefore": "2.1.0",
"runnerVersionAfter": "2.1.1",
"wasUpdated": true,
"apiUrl": "https://api.testdriver.ai",
"consoleUrl": "https://console.testdriver.ai",
"testRunLink": "https://console.testdriver.ai/runs/abc123/def456",
"dashcamUrl": "https://app.dashcam.io/replay/abc123",
"vncUrl": "wss://sandbox-123.testdriver.ai/vnc",
"date": "2025-01-15T14:30:00.000Z",
"team": {
"id": "team_abc123",
"sessionId": "sess_xyz789"
},
"infrastructure": {
"sandboxId": "sandbox-123",
"instanceId": "i-abc123",
"os": "linux",
"amiId": "ami-0abc123",
"e2bTemplateId": null,
"imageVersion": "v2.1.0"
},
"realtime": {
"channel": "sandbox:sandbox-123",
"messageCount": 42
},
"interactions": {
"total": 15,
"cached": 3,
"byType": {
"find": 8,
"click": 5,
"assert": 2
}
}
}
```
## Using Result Files in CI
Result files are useful for extracting test metadata in CI pipelines without parsing log output.
### GitHub Actions Example
Use `fromJSON` to parse a result file into a GitHub Actions expression you can reference in subsequent steps:
```yaml theme={null}
- name: Run tests
run: npx vitest run tests/login.test.mjs
- name: Parse result
id: result
run: |
# Read the first JSON result file
FILE=$(find .testdriver/results -name '*.json' | head -n 1)
echo "json=$(cat "$FILE")" >> "$GITHUB_OUTPUT"
- name: Comment on PR
if: fromJSON(steps.result.outputs.json).test.passed == false
uses: actions/github-script@v7
with:
script: |
const result = ${{ steps.result.outputs.json }};
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: [
`β **${result.test.name}** failed`,
``,
`Error: ${result.test.error}`,
``,
result.replay.markdown,
``,
`[View full recording](${result.urls.testRun})`
].join('\n')
});
```
You can also load all results into a matrix or iterate over them:
```yaml theme={null}
- name: Run tests
run: npx vitest run tests/*.test.mjs
- name: Collect results
id: results
run: |
# Merge all result files into a JSON array
echo "json=$(find .testdriver/results -name '*.json' -exec cat {} + | jq -s '.')" >> "$GITHUB_OUTPUT"
- name: Summary
run: |
echo '## Test Results' >> $GITHUB_STEP_SUMMARY
RESULTS='${{ steps.results.outputs.json }}'
echo "$RESULTS" | jq -r '.[] | "| \(.test.name) | \(if .test.passed then "β " else "β" end) | \(.urls.testRun) |"' >> $GITHUB_STEP_SUMMARY
```
### Reading Results Programmatically
```javascript theme={null}
import fs from "fs";
import path from "path";
const resultsDir = ".testdriver/results";
function readResults(dir) {
const results = [];
for (const testDir of fs.readdirSync(dir, { recursive: true })) {
const fullPath = path.join(dir, testDir);
if (fullPath.endsWith(".json") && fs.statSync(fullPath).isFile()) {
results.push(JSON.parse(fs.readFileSync(fullPath, "utf-8")));
}
}
return results;
}
const results = readResults(resultsDir);
const passed = results.filter(r => r.test.passed);
const failed = results.filter(r => !r.test.passed);
console.log(`${passed.length} passed, ${failed.length} failed`);
for (const r of failed) {
console.log(` FAIL: ${r.test.name} β ${r.test.error}`);
console.log(` Recording: ${r.urls.testRun}`);
console.log(` Embed: ${r.replay.markdown}`);
}
```
# type()
Source: https://docs.testdriver.ai/v7/type
Type text into focused input fields
## Overview
Type text or numbers into the currently focused input field with optional delay between keystrokes.
## Syntax
```javascript theme={null}
await testdriver.type(text, options)
```
## Parameters
Text to type (can be a string or number)
Typing options (or legacy delay number)
Delay between keystrokes in milliseconds
If `true`, treats text as sensitive data (won't be logged or stored in debug info/dashcam)
## Returns
`Promise`
## Examples
### Basic Typing
```javascript theme={null}
// Type text
await testdriver.type('hello@example.com');
// Type numbers
await testdriver.type(12345);
// Type with custom delay (legacy syntax)
await testdriver.type('slow typing', 500); // 500ms between each character
// Type with options object
await testdriver.type('text', { delay: 500 });
```
### Password/Secret Handling
```javascript theme={null}
// β SECURE - Mark as secret to prevent logging
const passwordField = await testdriver.find('password input');
await passwordField.click();
await testdriver.type('MySecureP@ssw0rd', { secret: true });
// Password NOT logged in dashcam or debug output
// β INSECURE - Password will be logged
await testdriver.type('MySecureP@ssw0rd');
// Password appears in logs, dashcam replay, and debug info
// Use secret for any sensitive data
await testdriver.find('api key input').click();
await testdriver.type('sk-1234567890abcdef', { secret: true });
await testdriver.find('credit card input').click();
await testdriver.type('4111111111111111', { secret: true });
```
**Always use `secret: true` for passwords and sensitive data!**
Without this option, typed text appears in:
* Dashcam video replays
* TestDriver logs
* Debug screenshots
* Error messages
### Form Filling
```javascript theme={null}
// Focus field and type
const emailField = await testdriver.find('email input');
await emailField.click();
await testdriver.type('user@example.com');
// Tab to next field and type
await testdriver.pressKeys(['tab']);
await testdriver.type('John Doe');
// Type password securely
await testdriver.pressKeys(['tab']);
await testdriver.type('MySecureP@ssw0rd', { secret: true });
```
### Clearing and Replacing Text
```javascript theme={null}
const searchBox = await testdriver.find('search input');
await searchBox.click();
// Clear existing text
await testdriver.pressKeys(['ctrl', 'a']); // Select all
await testdriver.type('new search query');
```
## Best Practices
**Focus the field first**
Always click the input field or navigate to it before typing:
```javascript theme={null}
const input = await testdriver.find('username input');
await input.click();
await testdriver.type('testuser');
```
**Use Tab for navigation**
Navigate between fields using Tab instead of clicking each one:
```javascript theme={null}
const firstField = await testdriver.find('first name');
await firstField.click();
await testdriver.type('John');
await testdriver.pressKeys(['tab']);
await testdriver.type('Doe');
await testdriver.pressKeys(['tab']);
await testdriver.type('john@example.com');
```
**Clear fields before typing**
Clear existing content to avoid appending:
```javascript theme={null}
const input = await testdriver.find('search field');
await input.click();
await testdriver.pressKeys(['ctrl', 'a']); // Select all
await testdriver.type('new search');
```
**Field must be focused**
Typing will only work if an input field is currently focused. If no field is focused, the text may be lost or trigger unexpected keyboard shortcuts.
## Use Cases
```javascript theme={null}
await testdriver.focusApplication('Google Chrome');
const usernameField = await testdriver.find('username input');
await usernameField.click();
await testdriver.type('testuser@example.com');
await testdriver.pressKeys(['tab']);
await testdriver.type('MyP@ssword123', { secret: true });
await testdriver.pressKeys(['enter']);
```
```javascript theme={null}
const searchBox = await testdriver.find('search input');
await searchBox.click();
await testdriver.type('laptop computers');
await testdriver.pressKeys(['enter']);
// Wait for results
await new Promise(r => setTimeout(r, 2000));
```
```javascript theme={null}
// First field
const nameField = await testdriver.find('full name input');
await nameField.click();
await testdriver.type('Jane Smith');
// Navigate with Tab
await testdriver.pressKeys(['tab']);
await testdriver.type('jane.smith@example.com');
await testdriver.pressKeys(['tab']);
await testdriver.type('+1-555-0123');
await testdriver.pressKeys(['tab']);
await testdriver.type('123 Main Street');
```
```javascript theme={null}
const editor = await testdriver.find('text editor area');
await editor.click();
await testdriver.type('# My Document', 100);
await testdriver.pressKeys(['enter', 'enter']);
await testdriver.type('This is the first paragraph.', 50);
```
```javascript theme={null}
const quantityField = await testdriver.find('quantity input');
await quantityField.click();
// Clear field
await testdriver.pressKeys(['ctrl', 'a']);
// Type number
await testdriver.type(5);
```
## Typing Speed
Adjust the delay parameter based on your needs:
```javascript theme={null}
// Fast typing (100ms delay)
await testdriver.type('quick entry', 100);
// Normal typing (250ms - default)
await testdriver.type('standard speed');
// Slow typing (500ms delay) - useful for fields with live validation
await testdriver.type('slow and steady', 500);
// Very slow (1000ms delay) - for problematic fields
await testdriver.type('one by one', 1000);
```
Some applications with live validation or autocomplete may require slower typing speeds to avoid race conditions.
## Special Characters
```javascript theme={null}
// Email addresses
await testdriver.type('user@example.com');
// URLs
await testdriver.type('https://example.com/path?query=value');
// Passwords with special characters
await testdriver.type('P@ssw0rd!#$%');
// Paths
await testdriver.type('C:\\Users\\Documents\\file.txt');
// Multi-line text (use pressKeys for Enter)
await testdriver.type('Line 1');
await testdriver.pressKeys(['enter']);
await testdriver.type('Line 2');
```
## Complete Example
```javascript theme={null}
import { beforeAll, afterAll, describe, it, expect } from 'vitest';
import TestDriver from 'testdriverai';
describe('Form Filling with Type', () => {
let testdriver;
beforeAll(async () => {
client = new TestDriver(process.env.TD_API_KEY);
await testdriver.auth();
await testdriver.connect();
});
afterAll(async () => {
await testdriver.disconnect();
});
it('should fill out registration form', async () => {
await testdriver.focusApplication('Google Chrome');
// Email field
const emailField = await testdriver.find('email input field');
await emailField.click();
await testdriver.type('john.doe@example.com');
// Tab through form fields
await testdriver.pressKeys(['tab']);
await testdriver.type('John');
await testdriver.pressKeys(['tab']);
await testdriver.type('Doe');
await testdriver.pressKeys(['tab']);
await testdriver.type('MySecureP@ssword123');
await testdriver.pressKeys(['tab']);
await testdriver.type('MySecureP@ssword123'); // Confirm password
// Verify fields were filled
const result = await testdriver.assert('all form fields are filled');
expect(result).toBeTruthy();
});
it('should update search query', async () => {
const searchBox = await testdriver.find('search input');
await searchBox.click();
// Type initial search
await testdriver.type('laptops');
await testdriver.pressKeys(['enter']);
await new Promise(r => setTimeout(r, 2000));
// Update search
await searchBox.click();
await testdriver.pressKeys(['ctrl', 'a']); // Select all
await testdriver.type('gaming laptops');
await testdriver.pressKeys(['enter']);
// Verify new search
await testdriver.assert('search results for "gaming laptops" are shown');
});
});
```
## Related Methods
* [`pressKeys()`](/v7/press-keys) - Press keyboard keys and shortcuts
* [`find()`](/v7/find) - Locate input fields
* [`click()`](/v7/click) - Focus input fields
# Using Variables
Source: https://docs.testdriver.ai/v7/variables
Use dynamic data and secure secrets in your tests
Scale your testing with dynamic data and secure secrets management. Choose the right approach based on your testing needs.
## Environment Variables
Environment variables are ideal for **configuration that changes between environments** (dev, staging, production) or for **secrets that shouldn't be committed to code**. Use this approach when you need to run the same tests against different servers or with different credentials.
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from 'testdriverai/presets';
test('multi-environment testing', async (context) => {
const env = process.env.TEST_ENV || 'staging';
const urls = {
dev: 'https://dev.myapp.com',
staging: 'https://staging.myapp.com',
production: 'https://myapp.com'
};
const { testdriver } = await chrome(context, {
url: urls[env]
});
await testdriver.assert('app is running');
});
```
```bash theme={null}
# Run against different environments
TEST_ENV=dev vitest run
TEST_ENV=staging vitest run
TEST_ENV=production vitest run
```
## Test Fixtures
Test fixtures work best when you have **structured, reusable test data** that needs to be shared across multiple tests. Use fixtures when testing different user roles, product catalogs, or any scenario where you want to parameterize tests with a known set of data.
```javascript test/fixtures/users.js theme={null}
export const testUsers = [
{ email: 'admin@test.com', role: 'admin' },
{ email: 'user@test.com', role: 'user' },
{ email: 'guest@test.com', role: 'guest' }
];
export const products = [
{ name: 'Laptop', price: 999 },
{ name: 'Mouse', price: 29 },
{ name: 'Keyboard', price: 89 }
];
```
```javascript test/permissions.test.js theme={null}
import { test } from 'vitest';
import { chrome } from 'testdriverai/presets';
import { testUsers } from './fixtures/users.js';
test.each(testUsers)('$role can access dashboard', async ({ email, role }, context) => {
const { testdriver } = await chrome(context, { url });
await testdriver.find('email input').type(email);
await testdriver.find('password input').type('password123');
await testdriver.find('login button').click();
if (role === 'admin') {
await testdriver.assert('admin panel is visible');
} else {
await testdriver.assert('user dashboard is visible');
}
});
```
## Dynamic Data Generation
Dynamic data generation is perfect for **creating unique test data on each run**, avoiding conflicts with existing records, and **testing edge cases with realistic data**. Use libraries like Faker when you need fresh emails, names, or other data that won't collide with previous test runs.
```javascript theme={null}
import { test } from 'vitest';
import { chrome } from 'testdriverai/presets';
import { faker } from '@faker-js/faker';
test('user registration with dynamic data', async (context) => {
const { testdriver } = await chrome(context, { url });
// Generate unique test data for each run
const userData = {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
password: faker.internet.password({ length: 12 })
};
await testdriver.find('first name input').type(userData.firstName);
await testdriver.find('last name input').type(userData.lastName);
await testdriver.find('email input').type(userData.email);
await testdriver.find('password input').type(userData.password);
await testdriver.find('register button').click();
await testdriver.assert('registration successful');
console.log('Registered user:', userData.email);
});
```
```bash theme={null}
npm install --save-dev @faker-js/faker
```
# What is TestDriver?
Source: https://docs.testdriver.ai/v7/what-is-testdriver
Reliably test your most difficult user flows
## The problem with modern testing tools
Modern testing tools like Playwright are designed to test a single web application, running in a single browser tab using selectors.
However, selectors are often either unreliable or unavailable in complex scenarios, leading to brittle and flaky tests:
| Challenge | Problem | Examples |
| ------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- |
| **Fast moving teams** | Frequently change UI structure, breaking CSS/XPath selectors | Agile teams, startups, vibe-coders |
| **Dynamic content** | Cannot be targeted with selectors | AI chatbots, PDFs, images, videos |
| **Software you don't own** | May lack proper accessibility attributes | Other websites, extensions, third-party applications |
| **Multi-application workflows** | Cannot be tested with web-only tools | Desktop apps, browser extensions, IDEs |
| **Visual states** | Impossible to verify with code-based selectors | Charts, graphs, videos, images, spelling errors, UI layout |
## The TestDriver Solution
TestDriver is a complete testing platform built specifically for handling these scenarios. It consists of a Javascript SDK, hosted infrastructure, and debugging tools that make it easy to write, run, and maintain tests for your most difficult user flows.
### Javascript SDK
Here is an example of a TestDriver test that installs a production Chrome extension from the Chrome Web Store and verifies that it appears in the extensions menu:
```javascript Installing Loom from the Chrome Web Store theme={null}
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("Chrome Extension Test", () => {
const testdriver = TestDriver(context);
// Launch Chrome with Loom loaded by its Chrome Web Store ID
await testdriver.provision.chromeExtension({
extensionId: 'liecbddmkiiihnedobmlmillhodjkdmb'
});
// Click on the extensions button (puzzle piece icon) in Chrome toolbar
const extensionsButton = await testdriver.find("The puzzle-shaped icon in the Chrome toolbar.");
await extensionsButton.click();
// Look for Loom in the extensions menu
const loomExtension = await testdriver.find("Loom extension in the extensions dropdown");
expect(loomExtension.found()).toBeTruthy();
});
```
[vitest](https://vitest.dev/) is the preferred test runner for TestDriver.
,