testdriverai 7.2.2 → 7.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/docs.json CHANGED
@@ -192,15 +192,8 @@
192
192
  "groups": [
193
193
  {
194
194
  "group": "Getting Started",
195
- "icon": "rocket",
196
- "pages": [
197
- "/v7/getting-started/quickstart"
198
- ]
199
- },
200
- {
201
- "group": "Guides",
202
- "icon": "book",
203
195
  "pages": [
196
+ "/v7/getting-started/quickstart",
204
197
  "/v7/getting-started/writing-tests",
205
198
  "/v7/getting-started/running-tests",
206
199
  "/v7/getting-started/debugging-tests",
@@ -209,7 +202,6 @@
209
202
  },
210
203
  {
211
204
  "group": "Examples",
212
- "icon": "code",
213
205
  "pages": [
214
206
  "/v7/presets/chrome",
215
207
  "/v7/presets/chrome-extension",
@@ -218,8 +210,7 @@
218
210
  ]
219
211
  },
220
212
  {
221
- "group": "Features",
222
- "icon": "layer-group",
213
+ "group": "Guides",
223
214
  "pages": [
224
215
  {
225
216
  "group": "Selectorless Testing",
@@ -1,266 +1,329 @@
1
1
  # Provision API
2
2
 
3
- The `provision()` function is the easiest way to set up TestDriver for common applications. It automatically handles TestDriver initialization, application launching, and Dashcam recording.
3
+ The `provision` object provides easy setup methods for common applications. These methods automatically handle TestDriver initialization, Dashcam recording, and application launching.
4
4
 
5
5
  ## Quick Start
6
6
 
7
7
  ```javascript
8
- import { test } from 'vitest';
9
- import { provision } from 'testdriverai/presets';
8
+ import { describe, it, expect } from 'vitest';
9
+ import { TestDriver } from 'testdriverai/lib/vitest/hooks.mjs';
10
10
 
11
- test('my test', async (context) => {
12
- const { testdriver } = await provision('chrome', {
13
- url: 'https://example.com'
14
- }, context);
15
-
16
- await testdriver.find('Login button').click();
11
+ describe('My Test Suite', () => {
12
+ it('should test Chrome browser', async (context) => {
13
+ const testdriver = TestDriver(context, { newSandbox: true });
14
+
15
+ await testdriver.provision.chrome({
16
+ url: 'https://example.com'
17
+ });
18
+
19
+ await testdriver.find('Login button').click();
20
+ });
17
21
  });
18
22
  ```
19
23
 
20
- ## API
21
-
22
- ```typescript
23
- provision(appType, options, context)
24
- ```
24
+ ## Available Methods
25
25
 
26
- **Parameters:**
27
- - `appType` - Application type: `'chrome'`, `'vscode'`, `'electron'`, or `'webapp'`
28
- - `options` - Configuration options (varies by app type)
29
- - `context` - Vitest test context (from your test function parameter)
26
+ - `provision.chrome()` - Launch Chrome browser with URL
27
+ - `provision.vscode()` - Launch VS Code with optional extensions
28
+ - `provision.installer()` - Download and install applications
29
+ - `provision.electron()` - Launch Electron applications
30
30
 
31
- **Returns:**
32
- - `testdriver` - TestDriver instance ready to use
33
- - `dashcam` - Dashcam instance (if enabled)
34
- - Additional app-specific properties (like `vscode`, `app`)
31
+ ---
35
32
 
36
- ## Application Types
33
+ ## provision.chrome()
37
34
 
38
- ### Chrome Browser
35
+ Launch Google Chrome with automatic Dashcam recording.
39
36
 
40
37
  ```javascript
41
- const { testdriver } = await provision('chrome', {
38
+ await testdriver.provision.chrome({
42
39
  url: 'https://myapp.com',
43
- maximized: true, // Start maximized (default: true)
44
- guest: true, // Use guest mode (default: true)
45
- dashcam: true, // Enable Dashcam (default: true)
46
- os: 'linux' // Target OS (default: 'linux')
47
- }, context);
48
-
49
- await testdriver.find('username').type('user@example.com');
50
- await testdriver.find('Login').click();
40
+ maximized: true,
41
+ extensionPath: '/path/to/extension'
42
+ });
51
43
  ```
52
44
 
53
45
  **Options:**
54
- - `url` - URL to navigate to (default: 'http://testdriver-sandbox.vercel.app/')
55
- - `maximized` - Start browser maximized (default: `true`)
56
- - `guest` - Use guest/incognito mode (default: `true`)
57
- - `dashcam` - Enable Dashcam recording (default: `true`)
58
- - `os` - Target OS: `'linux'`, `'mac'`, or `'windows'` (default: `'linux'`)
46
+ | Option | Type | Default | Description |
47
+ |--------|------|---------|-------------|
48
+ | `url` | string | `'http://testdriver-sandbox.vercel.app/'` | URL to navigate to |
49
+ | `maximized` | boolean | `true` | Start browser maximized |
50
+ | `extensionPath` | string | - | Path to Chrome extension to load |
59
51
 
60
- **Returns:**
61
- - `testdriver` - TestDriver instance
62
- - `dashcam` - Dashcam instance (if enabled)
63
-
64
- ### VS Code
52
+ **Example:**
65
53
 
66
54
  ```javascript
67
- const { testdriver, vscode } = await provision('vscode', {
68
- workspace: '/path/to/project',
69
- extensions: ['ms-python.python'],
70
- dashcam: true,
71
- os: 'linux'
72
- }, context);
73
-
74
- await vscode.find('File menu').click();
75
- await vscode.find('New File').click();
55
+ it('should login to my app', async (context) => {
56
+ const testdriver = TestDriver(context, { newSandbox: true });
57
+
58
+ await testdriver.provision.chrome({
59
+ url: 'https://myapp.com/login'
60
+ });
61
+
62
+ await testdriver.find('email input').type('user@example.com');
63
+ await testdriver.find('password input').type('password123');
64
+ await testdriver.find('Login button').click();
65
+
66
+ const result = await testdriver.assert('Dashboard is visible');
67
+ expect(result).toBeTruthy();
68
+ });
76
69
  ```
77
70
 
78
- **Options:**
79
- - `workspace` - Workspace/folder path to open (optional)
80
- - `extensions` - Array of extension IDs to install (optional)
81
- - `dashcam` - Enable Dashcam recording (default: `true`)
82
- - `os` - Target OS (default: `'linux'`)
71
+ ---
83
72
 
84
- **Returns:**
85
- - `testdriver` - TestDriver instance
86
- - `vscode` - Alias for testdriver (semantic clarity)
87
- - `dashcam` - Dashcam instance (if enabled)
73
+ ## provision.vscode()
88
74
 
89
- ### Electron
75
+ Launch Visual Studio Code with optional extension installation. Automatically starts Dashcam recording.
90
76
 
91
77
  ```javascript
92
- const { testdriver, app } = await provision('electron', {
93
- appPath: '/path/to/app',
94
- args: ['--enable-logging'],
95
- dashcam: true,
96
- os: 'linux'
97
- }, context);
98
-
99
- await app.find('main window').click();
78
+ await testdriver.provision.vscode({
79
+ workspace: '/path/to/project',
80
+ extensions: ['esbenp.prettier-vscode', 'ms-python.python']
81
+ });
100
82
  ```
101
83
 
102
84
  **Options:**
103
- - `appPath` - Path to Electron application (required)
104
- - `args` - Additional command-line arguments (optional)
105
- - `dashcam` - Enable Dashcam recording (default: `true`)
106
- - `os` - Target OS (default: `'linux'`)
85
+ | Option | Type | Default | Description |
86
+ |--------|------|---------|-------------|
87
+ | `workspace` | string | - | Workspace/folder path to open |
88
+ | `extensions` | string[] | `[]` | VS Code extension IDs to install |
107
89
 
108
- **Returns:**
109
- - `testdriver` - TestDriver instance
110
- - `app` - Alias for testdriver (semantic clarity)
111
- - `dashcam` - Dashcam instance (if enabled)
112
-
113
- ### Web App
114
-
115
- Generic wrapper for web applications (currently uses Chrome):
90
+ **Example - Basic Launch:**
116
91
 
117
92
  ```javascript
118
- const { testdriver } = await provision('webapp', {
119
- url: 'https://example.com',
120
- browser: 'chrome' // Only 'chrome' supported currently
121
- }, context);
93
+ it('should launch VS Code', async (context) => {
94
+ const testdriver = TestDriver(context, { newSandbox: true });
95
+
96
+ await testdriver.provision.vscode();
97
+
98
+ const result = await testdriver.assert(
99
+ 'Visual Studio Code window is visible on screen'
100
+ );
101
+ expect(result).toBeTruthy();
102
+ });
122
103
  ```
123
104
 
124
- ## Complete Example
105
+ **Example - Install Extensions:**
125
106
 
126
107
  ```javascript
127
- import { describe, it, expect } from 'vitest';
128
- import { provision } from 'testdriverai/presets';
129
-
130
- describe('Login Flow', () => {
131
- it('should login successfully', async (context) => {
132
- // Provision Chrome with your app
133
- const { testdriver, dashcam } = await provision('chrome', {
134
- url: 'https://myapp.com/login',
135
- maximized: true
136
- }, context);
137
-
138
- // Interact with the application
139
- await testdriver.find('email input').type('user@example.com');
140
- await testdriver.find('password input').type('password123');
141
- await testdriver.find('Login button').click();
142
-
143
- // Verify results
144
- const result = await testdriver.assert('Welcome message is visible');
145
- expect(result).toBeTruthy();
146
-
147
- // Dashcam automatically stops and saves replay at test end
148
- // No cleanup needed - handled automatically!
108
+ it('should install and use a VS Code extension', async (context) => {
109
+ const testdriver = TestDriver(context, { newSandbox: true });
110
+
111
+ // Launch VS Code with extensions installed
112
+ await testdriver.provision.vscode({
113
+ extensions: ['esbenp.prettier-vscode']
149
114
  });
115
+
116
+ // Open the extensions panel
117
+ await testdriver.pressKeys(['ctrl', 'shift', 'x']);
118
+ await new Promise(resolve => setTimeout(resolve, 2000));
119
+
120
+ const result = await testdriver.assert(
121
+ 'Prettier extension is visible in the extensions panel'
122
+ );
123
+ expect(result).toBeTruthy();
150
124
  });
151
125
  ```
152
126
 
153
- ## How It Works
127
+ ---
154
128
 
155
- When you call `provision()`:
129
+ ## provision.installer()
156
130
 
157
- 1. **Creates TestDriver client** - Initializes and connects to sandbox
158
- 2. **Sets up Dashcam** - Authenticates and starts recording (if enabled)
159
- 3. **Launches application** - Opens the specified app with your configuration
160
- 4. **Focuses window** - Ensures the app is ready for interaction
161
- 5. **Returns ready-to-use instances** - Everything is set up and ready
131
+ Download and install applications from a URL. Automatically detects file type and runs the appropriate install command.
162
132
 
163
- At test end:
164
- - Dashcam automatically stops and saves replay URL
165
- - TestDriver automatically disconnects
166
- - All cleanup is handled for you
167
-
168
- ## Automatic Lifecycle
133
+ ```javascript
134
+ const filePath = await testdriver.provision.installer({
135
+ url: 'https://example.com/app.deb',
136
+ appName: 'MyApp'
137
+ });
138
+ ```
169
139
 
170
- The `provision()` function uses Vitest hooks under the hood to manage the entire lifecycle:
140
+ **Options:**
141
+ | Option | Type | Default | Description |
142
+ |--------|------|---------|-------------|
143
+ | `url` | string | **required** | URL to download the installer from |
144
+ | `filename` | string | auto-detected | Filename to save as |
145
+ | `appName` | string | - | Application name to focus after install |
146
+ | `launch` | boolean | `true` | Whether to launch/focus the app after installation |
147
+
148
+ **Returns:** `Promise<string>` - Path to the downloaded file
149
+
150
+ **Supported File Types:**
151
+
152
+ | Platform | Extensions | Install Command |
153
+ |----------|------------|-----------------|
154
+ | Linux | `.deb` | `sudo dpkg -i && apt-get install -f -y` |
155
+ | Linux | `.rpm` | `sudo rpm -i` |
156
+ | Linux | `.appimage` | `chmod +x` |
157
+ | Linux | `.sh` | `chmod +x && execute` |
158
+ | Windows | `.msi` | `msiexec /i /quiet /norestart` |
159
+ | Windows | `.exe` | `Start-Process /S` |
160
+ | macOS | `.dmg` | `hdiutil attach && cp to /Applications` |
161
+ | macOS | `.pkg` | `installer -pkg -target /` |
162
+
163
+ **Example - Install .deb Package:**
171
164
 
172
165
  ```javascript
173
- // This:
174
- const { testdriver } = await provision('chrome', { url }, context);
175
-
176
- // Is equivalent to manually doing:
177
- const client = new TestDriver(apiKey, { os: 'linux' });
178
- await client.auth();
179
- await client.connect();
166
+ it('should install a .deb package', async (context) => {
167
+ const testdriver = TestDriver(context, { newSandbox: true });
168
+
169
+ const filePath = await testdriver.provision.installer({
170
+ url: 'https://github.com/sharkdp/bat/releases/download/v0.24.0/bat_0.24.0_amd64.deb'
171
+ });
172
+
173
+ // Verify installation
174
+ await testdriver.exec('sh', 'bat --version', 10000);
175
+ });
176
+ ```
180
177
 
181
- const dashcam = new Dashcam(client);
182
- await dashcam.auth();
183
- await dashcam.start();
178
+ **Example - Download Only (No Auto-Launch):**
184
179
 
185
- await client.exec('sh', 'google-chrome --start-maximized --guest "https://example.com" &', 30000);
186
- await client.focusApplication('Google Chrome');
180
+ ```javascript
181
+ it('should download a script', async (context) => {
182
+ const testdriver = TestDriver(context, { newSandbox: true });
183
+
184
+ const filePath = await testdriver.provision.installer({
185
+ url: 'https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh',
186
+ launch: false
187
+ });
188
+
189
+ // Run custom post-download commands
190
+ await testdriver.exec('sh', `source "${filePath}"`, 30000);
191
+ });
192
+ ```
187
193
 
188
- // ... your test code ...
194
+ **Example - Custom Post-Install:**
189
195
 
190
- await dashcam.stop();
191
- await client.disconnect();
196
+ ```javascript
197
+ it('should run AppImage with custom flags', async (context) => {
198
+ const testdriver = TestDriver(context, { newSandbox: true });
199
+
200
+ const filePath = await testdriver.provision.installer({
201
+ url: 'https://example.com/app.AppImage',
202
+ launch: false
203
+ });
204
+
205
+ // Run with custom arguments
206
+ await testdriver.exec('sh', `"${filePath}" --no-sandbox &`, 10000);
207
+
208
+ await new Promise(resolve => setTimeout(resolve, 5000));
209
+
210
+ const result = await testdriver.assert('App window is visible');
211
+ expect(result).toBeTruthy();
212
+ });
192
213
  ```
193
214
 
194
- That's **15+ lines of boilerplate** reduced to **1 line**!
215
+ ---
195
216
 
196
- ## TypeScript Support
217
+ ## provision.electron()
197
218
 
198
- Full TypeScript definitions included:
219
+ Launch an Electron application.
199
220
 
200
- ```typescript
201
- import { provision } from 'testdriverai/presets';
202
-
203
- test('typed test', async (context) => {
204
- const { testdriver } = await provision('chrome', {
205
- url: 'https://example.com',
206
- maximized: true,
207
- os: 'linux' // ✅ Type-safe: only 'linux' | 'mac' | 'windows'
208
- }, context);
209
-
210
- // ✅ Full autocomplete for testdriver methods
211
- await testdriver.find('button').click();
221
+ ```javascript
222
+ await testdriver.provision.electron({
223
+ appPath: '/path/to/app',
224
+ args: ['--enable-logging']
212
225
  });
213
226
  ```
214
227
 
215
- ## Direct Preset Functions
228
+ **Options:**
229
+ | Option | Type | Default | Description |
230
+ |--------|------|---------|-------------|
231
+ | `appPath` | string | **required** | Path to Electron application |
232
+ | `args` | string[] | `[]` | Additional command-line arguments |
216
233
 
217
- You can also use the individual preset functions directly:
234
+ **Example:**
218
235
 
219
236
  ```javascript
220
- import { chrome, vscode, electron } from 'testdriverai/presets';
221
-
222
- // Same as provision('chrome', options, context)
223
- const { testdriver } = await chrome(context, {
224
- url: 'https://example.com'
225
- });
226
-
227
- // Same as provision('vscode', options, context)
228
- const { vscode } = await vscode(context, {
229
- workspace: '/path/to/project'
237
+ it('should launch Electron app', async (context) => {
238
+ const testdriver = TestDriver(context, { newSandbox: true });
239
+
240
+ await testdriver.provision.electron({
241
+ appPath: '/path/to/my-electron-app',
242
+ args: ['--enable-logging']
243
+ });
244
+
245
+ await testdriver.find('main window').click();
230
246
  });
231
247
  ```
232
248
 
233
- These are available for when you prefer explicit function names.
249
+ ---
234
250
 
235
- ## Best Practices
251
+ ## How Provision Methods Work
252
+
253
+ When you call a provision method:
236
254
 
237
- 1. **Always pass context** - Required for automatic cleanup
238
- 2. **Enable dashcam** - Great for debugging test failures
239
- 3. **Use descriptive variables** - `testdriver`, `vscode`, `app` based on what you're testing
240
- 4. **Leverage TypeScript** - Get autocomplete and type safety
241
- 5. **Keep URLs in config** - Use environment variables for different environments
255
+ 1. **Waits for connection** - Calls `ready()` to ensure sandbox is connected
256
+ 2. **Sets up Dashcam** - Creates log file and adds to Dashcam monitoring
257
+ 3. **Starts recording** - Automatically starts Dashcam if not already recording
258
+ 4. **Launches application** - Opens the specified app with your configuration
259
+ 5. **Focuses window** - Ensures the app is ready for interaction
242
260
 
243
- ## Error Handling
261
+ At test end:
262
+ - Dashcam automatically stops and saves replay URL
263
+ - TestDriver automatically disconnects
264
+ - All cleanup is handled for you
265
+
266
+ ## Complete Example
244
267
 
245
268
  ```javascript
246
- test('handles errors gracefully', async (context) => {
247
- try {
248
- const { testdriver } = await provision('chrome', {
249
- url: 'https://example.com'
250
- }, context);
269
+ import { describe, it, expect } from 'vitest';
270
+ import { TestDriver } from 'testdriverai/lib/vitest/hooks.mjs';
271
+
272
+ describe('Application Testing', () => {
273
+ it('should test Chrome login flow', async (context) => {
274
+ const testdriver = TestDriver(context, { newSandbox: true });
275
+
276
+ await testdriver.provision.chrome({
277
+ url: 'https://myapp.com/login'
278
+ });
279
+
280
+ await testdriver.find('email').type('user@example.com');
281
+ await testdriver.find('password').type('password123');
282
+ await testdriver.find('Login').click();
251
283
 
252
- await testdriver.find('button').click();
253
- } catch (error) {
254
- // Cleanup still happens automatically
255
- console.error('Test failed:', error);
256
- throw error; // Re-throw to mark test as failed
257
- }
284
+ const result = await testdriver.assert('Welcome message is visible');
285
+ expect(result).toBeTruthy();
286
+ });
287
+
288
+ it('should test VS Code extension', async (context) => {
289
+ const testdriver = TestDriver(context, { newSandbox: true });
290
+
291
+ await testdriver.provision.vscode({
292
+ extensions: ['ms-python.python']
293
+ });
294
+
295
+ await testdriver.pressKeys(['ctrl', 'shift', 'p']);
296
+ await testdriver.type('Python: Select Interpreter');
297
+ await testdriver.pressKeys(['enter']);
298
+
299
+ const result = await testdriver.assert('Python interpreter selector is visible');
300
+ expect(result).toBeTruthy();
301
+ });
302
+
303
+ it('should install and test CLI tool', async (context) => {
304
+ const testdriver = TestDriver(context, { newSandbox: true });
305
+
306
+ await testdriver.provision.installer({
307
+ url: 'https://github.com/sharkdp/bat/releases/download/v0.24.0/bat_0.24.0_amd64.deb'
308
+ });
309
+
310
+ // Verify the tool works
311
+ await testdriver.exec('sh', 'bat --help', 10000);
312
+ });
258
313
  });
259
314
  ```
260
315
 
316
+ ## Best Practices
317
+
318
+ 1. **Use `newSandbox: true`** - Each test gets a clean environment
319
+ 2. **Enable Dashcam** - Great for debugging test failures (enabled by default)
320
+ 3. **Check assertions** - Always verify the expected state after actions
321
+ 4. **Use appropriate provision method** - Match the method to your test target
322
+ 5. **Handle async properly** - All provision methods return Promises
323
+
261
324
  ## See Also
262
325
 
263
- - [Hooks API](./HOOKS.md) - For more control over lifecycle
264
- - [Core Classes](./CORE.md) - For full manual control
265
- - [Migration Guide](../MIGRATION.md) - Upgrading from v6
266
- - [Examples](../../testdriver/acceptance-sdk/presets-example.test.mjs) - Working examples
326
+ - [Hooks API](./hooks.mdx) - TestDriver initialization
327
+ - [Find API](../api/find.mdx) - Element finding
328
+ - [Exec API](../api/exec.mdx) - Running shell commands
329
+ - [Assert API](../api/assert.mdx) - AI-powered assertions
@@ -1,12 +1,16 @@
1
1
  ---
2
2
  title: "Quick Start"
3
3
  sidebarTitle: "Quickstart"
4
- description: "Get started with the TestDriver JavaScript SDK in minutes."
4
+ description: "Run your first computer-use test in minutes."
5
5
  icon: "rocket"
6
6
  mode: "wide"
7
7
  ---
8
8
 
9
- TestDriver makes it easy to write automated computer-use tests for web browsers, desktop apps, and more. In this quickstart, you'll write and run your first TestDriver test using Vitest in just a few minutes.
9
+ 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.
10
+
11
+ <Tip><a href="https://discord.com/invite/cWDFW8DzPm" target="_blank" rel="noreferrer">Join our Discord</a> if you have any questions or need help getting started!</Tip>
12
+
13
+ ## Get Started in 3 Steps
10
14
 
11
15
  <Steps>
12
16
  <Step title="Create a TestDriver Account">
@@ -20,7 +24,7 @@ TestDriver makes it easy to write automated computer-use tests for web browsers,
20
24
  arrow
21
25
  horizontal
22
26
  >
23
- Start for free. No credit-card required!
27
+ No credit-card required!
24
28
  </Card>
25
29
 
26
30
  </Step>
@@ -41,10 +45,10 @@ TestDriver makes it easy to write automated computer-use tests for web browsers,
41
45
  TestDriver uses Vitest as the test runner. To run your test, use:
42
46
 
43
47
  ```bash
44
- npx vitest run
48
+ vitest run
45
49
  ```
46
50
 
47
- This will spawn a sandbox, launch Chrome, navigate to the specified URL, and run your test commands.
51
+ This will spawn a sandbox, launch Chrome, and run the example test!
48
52
 
49
53
  </Step>
50
54
  </Steps>
@@ -84,14 +88,3 @@ TestDriver makes it easy to write automated computer-use tests for web browsers,
84
88
  Choose your complexity level
85
89
  </Card>
86
90
  </CardGroup>
87
-
88
- ## Why TestDriver v7?
89
-
90
- The v7 SDK offers advantages over YAML-based testing:
91
-
92
- - ✅ **Type Safety** - Full TypeScript support with IntelliSense
93
- - ✅ **Programmatic Control** - Use variables, loops, and functions
94
- - ✅ **Better Debugging** - Stack traces point to your actual code
95
- - ✅ **Automatic Lifecycle** - Presets handle setup and cleanup
96
- - ✅ **Framework Integration** - Works with Vitest, Jest, Mocha, etc.
97
- - ✅ **Video Replays** - Automatic Dashcam recording included
@@ -15,11 +15,11 @@
15
15
  * });
16
16
  */
17
17
 
18
+ import chalk from 'chalk';
18
19
  import fs from 'fs';
19
20
  import os from 'os';
20
21
  import path from 'path';
21
22
  import { vi } from 'vitest';
22
- import chalk from 'chalk';
23
23
  import TestDriverSDK from '../../sdk.js';
24
24
 
25
25
  /**
@@ -78,26 +78,30 @@ function setupConsoleSpy(client, taskId) {
78
78
  }
79
79
  };
80
80
 
81
+ // Store original console methods before spying
82
+ const originalLog = console.log.bind(console);
83
+ const originalError = console.error.bind(console);
84
+ const originalWarn = console.warn.bind(console);
85
+ const originalInfo = console.info.bind(console);
86
+
81
87
  // Create spies for each console method
82
88
  const logSpy = vi.spyOn(console, 'log').mockImplementation((...args) => {
83
- // Call through to original
84
- logSpy.mock.calls; // Track calls
85
- process.stdout.write(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ') + '\n');
89
+ originalLog(...args); // Call original (Vitest will capture this)
86
90
  forwardToSandbox(args);
87
91
  });
88
92
 
89
93
  const errorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
90
- process.stderr.write(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ') + '\n');
94
+ originalError(...args);
91
95
  forwardToSandbox(args);
92
96
  });
93
97
 
94
98
  const warnSpy = vi.spyOn(console, 'warn').mockImplementation((...args) => {
95
- process.stderr.write(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ') + '\n');
99
+ originalWarn(...args);
96
100
  forwardToSandbox(args);
97
101
  });
98
102
 
99
103
  const infoSpy = vi.spyOn(console, 'info').mockImplementation((...args) => {
100
- process.stdout.write(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ') + '\n');
104
+ originalInfo(...args);
101
105
  forwardToSandbox(args);
102
106
  });
103
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.2.2",
3
+ "version": "7.2.3",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "exports": {
package/sdk.js CHANGED
@@ -1702,26 +1702,49 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1702
1702
  * @returns {Promise<void>}
1703
1703
  */
1704
1704
  vscode: async (options = {}) => {
1705
- this._ensureConnected();
1705
+ // Automatically wait for connection to be ready
1706
+ await this.ready();
1706
1707
 
1707
1708
  const {
1708
1709
  workspace = null,
1709
1710
  extensions = [],
1710
1711
  } = options;
1711
1712
 
1713
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1714
+
1715
+ // If dashcam is available, set up file logging
1716
+ if (this._dashcam) {
1717
+ // Create the log file on the remote machine
1718
+ const logPath = this.os === "windows"
1719
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1720
+ : "/tmp/testdriver.log";
1721
+
1722
+ const createLogCmd = this.os === "windows"
1723
+ ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1724
+ : `touch ${logPath}`;
1725
+
1726
+ await this.exec(shell, createLogCmd, 10000, true);
1727
+ await this._dashcam.addFileLog(logPath, "TestDriver Log");
1728
+ }
1729
+
1730
+ // Automatically start dashcam if not already recording
1731
+ if (!this._dashcam || !this._dashcam.recording) {
1732
+ await this.dashcam.start();
1733
+ }
1734
+
1712
1735
  // Install extensions if provided
1713
1736
  for (const extension of extensions) {
1714
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1737
+ console.log(`[provision.vscode] Installing extension: ${extension}`);
1715
1738
  await this.exec(
1716
1739
  shell,
1717
- `code --install-extension ${extension}`,
1718
- 60000,
1740
+ `code --install-extension ${extension} --force`,
1741
+ 120000,
1719
1742
  true
1720
1743
  );
1744
+ console.log(`[provision.vscode] ✅ Extension installed: ${extension}`);
1721
1745
  }
1722
1746
 
1723
1747
  // Launch VS Code
1724
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1725
1748
  const workspaceArg = workspace ? `"${workspace}"` : '';
1726
1749
 
1727
1750
  if (this.os === 'windows') {
@@ -1738,10 +1761,148 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1738
1761
  );
1739
1762
  }
1740
1763
 
1764
+ // Wait for VS Code to start up
1765
+ await new Promise(resolve => setTimeout(resolve, 3000));
1766
+
1741
1767
  // Wait for VS Code to be ready
1742
1768
  await this.focusApplication('Visual Studio Code');
1743
1769
  },
1744
1770
 
1771
+ /**
1772
+ * Download and install an application
1773
+ * @param {Object} options - Installer options
1774
+ * @param {string} options.url - URL to download the installer from
1775
+ * @param {string} [options.filename] - Filename to save as (auto-detected from URL if not provided)
1776
+ * @param {string} [options.appName] - Application name to focus after install
1777
+ * @param {boolean} [options.launch=true] - Whether to launch the app after installation
1778
+ * @returns {Promise<string>} Path to the downloaded file
1779
+ * @example
1780
+ * // Install a .deb package on Linux (auto-detected)
1781
+ * await testdriver.provision.installer({
1782
+ * url: 'https://example.com/app.deb',
1783
+ * appName: 'MyApp'
1784
+ * });
1785
+ *
1786
+ * @example
1787
+ * // Download and run custom commands
1788
+ * const filePath = await testdriver.provision.installer({
1789
+ * url: 'https://example.com/app.AppImage',
1790
+ * launch: false
1791
+ * });
1792
+ * await testdriver.exec('sh', `chmod +x "${filePath}" && "${filePath}" &`, 10000);
1793
+ */
1794
+ installer: async (options = {}) => {
1795
+ // Automatically wait for connection to be ready
1796
+ await this.ready();
1797
+
1798
+ const {
1799
+ url,
1800
+ filename,
1801
+ appName,
1802
+ launch = true,
1803
+ } = options;
1804
+
1805
+ if (!url) {
1806
+ throw new Error('[provision.installer] url is required');
1807
+ }
1808
+
1809
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1810
+
1811
+ // If dashcam is available, set up file logging
1812
+ if (this._dashcam) {
1813
+ const logPath = this.os === "windows"
1814
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1815
+ : "/tmp/testdriver.log";
1816
+
1817
+ const createLogCmd = this.os === "windows"
1818
+ ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1819
+ : `touch ${logPath}`;
1820
+
1821
+ await this.exec(shell, createLogCmd, 10000, true);
1822
+ await this._dashcam.addFileLog(logPath, "TestDriver Log");
1823
+ }
1824
+
1825
+ // Automatically start dashcam if not already recording
1826
+ if (!this._dashcam || !this._dashcam.recording) {
1827
+ await this.dashcam.start();
1828
+ }
1829
+
1830
+ // Determine filename from URL if not provided
1831
+ const urlObj = new URL(url);
1832
+ const detectedFilename = filename || urlObj.pathname.split('/').pop() || 'installer';
1833
+
1834
+ // Determine download directory and full path
1835
+ const downloadDir = this.os === 'windows'
1836
+ ? 'C:\\Users\\testdriver\\Downloads'
1837
+ : '/tmp';
1838
+ const filePath = this.os === 'windows'
1839
+ ? `${downloadDir}\\${detectedFilename}`
1840
+ : `${downloadDir}/${detectedFilename}`;
1841
+
1842
+ console.log(`[provision.installer] Downloading ${url}...`);
1843
+
1844
+ // Download the file
1845
+ if (this.os === 'windows') {
1846
+ await this.exec(
1847
+ shell,
1848
+ `Invoke-WebRequest -Uri "${url}" -OutFile "${filePath}"`,
1849
+ 300000, // 5 min timeout for download
1850
+ true
1851
+ );
1852
+ } else {
1853
+ await this.exec(
1854
+ shell,
1855
+ `curl -L -o "${filePath}" "${url}"`,
1856
+ 300000,
1857
+ true
1858
+ );
1859
+ }
1860
+
1861
+ console.log(`[provision.installer] ✅ Downloaded to ${filePath}`);
1862
+
1863
+ // Auto-detect install command based on file extension
1864
+ const ext = detectedFilename.split('.').pop()?.toLowerCase();
1865
+ let installCommand = null;
1866
+
1867
+ if (this.os === 'windows') {
1868
+ if (ext === 'msi') {
1869
+ installCommand = `Start-Process msiexec -ArgumentList '/i', '"${filePath}"', '/quiet', '/norestart' -Wait`;
1870
+ } else if (ext === 'exe') {
1871
+ installCommand = `Start-Process "${filePath}" -ArgumentList '/S' -Wait`;
1872
+ }
1873
+ } else if (this.os === 'linux') {
1874
+ if (ext === 'deb') {
1875
+ installCommand = `sudo dpkg -i "${filePath}" && sudo apt-get install -f -y`;
1876
+ } else if (ext === 'rpm') {
1877
+ installCommand = `sudo rpm -i "${filePath}"`;
1878
+ } else if (ext === 'appimage') {
1879
+ installCommand = `chmod +x "${filePath}"`;
1880
+ } else if (ext === 'sh') {
1881
+ installCommand = `chmod +x "${filePath}" && "${filePath}"`;
1882
+ }
1883
+ } else if (this.os === 'darwin') {
1884
+ if (ext === 'dmg') {
1885
+ installCommand = `hdiutil attach "${filePath}" -mountpoint /Volumes/installer && cp -R /Volumes/installer/*.app /Applications/ && hdiutil detach /Volumes/installer`;
1886
+ } else if (ext === 'pkg') {
1887
+ installCommand = `sudo installer -pkg "${filePath}" -target /`;
1888
+ }
1889
+ }
1890
+
1891
+ if (installCommand) {
1892
+ console.log(`[provision.installer] Installing...`);
1893
+ await this.exec(shell, installCommand, 300000, true);
1894
+ console.log(`[provision.installer] ✅ Installation complete`);
1895
+ }
1896
+
1897
+ // Launch and focus the app if appName is provided and launch is true
1898
+ if (appName && launch) {
1899
+ await new Promise(resolve => setTimeout(resolve, 2000));
1900
+ await this.focusApplication(appName);
1901
+ }
1902
+
1903
+ return filePath;
1904
+ },
1905
+
1745
1906
  /**
1746
1907
  * Launch Electron app
1747
1908
  * @param {Object} options - Electron launch options
@@ -2606,6 +2767,8 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2606
2767
  createMarkdownLogger(this.emitter);
2607
2768
 
2608
2769
  // Set up basic event logging
2770
+ // Note: We only console.log here - the console spy in vitest/hooks.mjs
2771
+ // handles forwarding to sandbox. This prevents duplicate output to server.
2609
2772
  this.emitter.on("log:**", (message) => {
2610
2773
  const event = this.emitter.event;
2611
2774
  if (event === events.log.debug && !debugMode) return;
@@ -2614,9 +2777,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2614
2777
  ? `[${this.testContext}] ${message}`
2615
2778
  : message;
2616
2779
  console.log(prefixedMessage);
2617
-
2618
- // Also forward to sandbox for dashcam
2619
- this._forwardLogToSandbox(prefixedMessage);
2620
2780
  }
2621
2781
  });
2622
2782
 
@@ -2675,36 +2835,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2675
2835
  });
2676
2836
  }
2677
2837
 
2678
- /**
2679
- * Forward log message to sandbox for debugger display
2680
- * @private
2681
- * @param {string} message - Log message to forward
2682
- */
2683
- _forwardLogToSandbox(message) {
2684
- try {
2685
- // Only forward if sandbox is connected
2686
- if (this.sandbox && this.sandbox.instanceSocketConnected) {
2687
- // Don't send objects as they cause base64 encoding errors
2688
- if (typeof message === "object") {
2689
- return;
2690
- }
2691
-
2692
- // Add test context prefix if available
2693
- const prefixedMessage = this.testContext
2694
- ? `[${this.testContext}] ${message}`
2695
- : message;
2696
-
2697
- this.sandbox.send({
2698
- type: "output",
2699
- output: Buffer.from(prefixedMessage).toString("base64"),
2700
- });
2701
- }
2702
- } catch {
2703
- // Silently fail to avoid breaking the log flow
2704
- // console.error("Error forwarding log to sandbox:", error);
2705
- }
2706
- }
2707
-
2708
2838
  /**
2709
2839
  * Open URL in default browser
2710
2840
  * @private
@@ -0,0 +1,47 @@
1
+ /**
2
+ * TestDriver SDK - Installer Test (Vitest)
3
+ * Tests the provision.installer() method for downloading and installing apps
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { TestDriver } from "../../lib/vitest/hooks.mjs";
8
+
9
+ describe("Provision Installer", () => {
10
+ it(
11
+ "should download and install a .deb package on Linux",
12
+ async (context) => {
13
+ const testdriver = TestDriver(context, { newSandbox: true });
14
+
15
+ // Install bat (a cat clone with syntax highlighting) using provision.installer
16
+ const filePath = await testdriver.provision.installer({
17
+ url: 'https://github.com/sharkdp/bat/releases/download/v0.24.0/bat_0.24.0_amd64.deb',
18
+ });
19
+
20
+ // Verify the file was downloaded
21
+ expect(filePath).toContain('bat');
22
+
23
+ // Verify bat was installed by running it
24
+ await testdriver.exec('sh', 'bat --version', 10000);
25
+ },
26
+ );
27
+
28
+ it(
29
+ "should download a shell script and verify it exists",
30
+ async (context) => {
31
+ const testdriver = TestDriver(context, { newSandbox: true });
32
+
33
+ // Download a shell script (nvm installer)
34
+ const filePath = await testdriver.provision.installer({
35
+ url: 'https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh',
36
+ launch: false, // Don't auto-run the script
37
+ });
38
+
39
+ // Verify the file was downloaded
40
+ expect(filePath).toContain('install.sh');
41
+
42
+ // Verify the file is executable
43
+ const result = await testdriver.exec('sh', `ls -la "${filePath}"`, 10000);
44
+ expect(result).toBeTruthy();
45
+ },
46
+ );
47
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * TestDriver SDK - Launch VS Code on Linux Test (Vitest)
3
+ * Tests launching Visual Studio Code on Debian/Ubuntu using provision.vscode()
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { TestDriver } from "../../lib/vitest/hooks.mjs";
8
+
9
+ describe("Launch VS Code on Linux", () => {
10
+ it(
11
+ "should launch VS Code on Debian/Ubuntu",
12
+ async (context) => {
13
+ const testdriver = TestDriver(context, { newSandbox: true });
14
+
15
+ // provision.vscode() automatically calls ready() and starts dashcam
16
+ await testdriver.provision.vscode();
17
+
18
+ // Assert that VS Code is running
19
+ const result = await testdriver.assert(
20
+ "Visual Studio Code window is visible on screen",
21
+ );
22
+ expect(result).toBeTruthy();
23
+ },
24
+ );
25
+
26
+ it(
27
+ "should install and use a VS Code extension",
28
+ async (context) => {
29
+ const testdriver = TestDriver(context, { newSandbox: true });
30
+
31
+ // Launch VS Code with the Prettier extension installed
32
+ await testdriver.provision.vscode({
33
+ extensions: ["esbenp.prettier-vscode"],
34
+ });
35
+
36
+ // Assert that VS Code is running
37
+ const vsCodeVisible = await testdriver.assert(
38
+ "Visual Studio Code window is visible on screen",
39
+ );
40
+ expect(vsCodeVisible).toBeTruthy();
41
+
42
+ // Open the extensions panel to verify Prettier is installed
43
+ await testdriver.pressKeys(["ctrl", "shift", "x"]);
44
+
45
+ // Wait for extensions panel to open
46
+ await new Promise((resolve) => setTimeout(resolve, 2000));
47
+
48
+ // Assert that Prettier extension is visible in the installed extensions
49
+ const prettierVisible = await testdriver.assert(
50
+ "Prettier extension is visible in the extensions panel or sidebar",
51
+ );
52
+ expect(prettierVisible).toBeTruthy();
53
+ },
54
+ );
55
+ });
package/vitest.config.mjs CHANGED
@@ -11,7 +11,7 @@ export default defineConfig({
11
11
  testTimeout: 900000,
12
12
  hookTimeout: 900000,
13
13
  reporters: [
14
- 'default',
14
+ 'verbose',
15
15
  TestDriver(),
16
16
  ['junit', { outputFile: 'test-report.junit.xml' }]
17
17
  ],