testdriverai 7.8.0-test.71 → 7.8.0-test.73

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.
@@ -0,0 +1,176 @@
1
+ // sdk/agent/lib/provision-commands.js
2
+ // Canonical source of truth for sandbox-agent provisioning commands.
3
+ //
4
+ // These pure functions generate platform-specific command arrays for
5
+ // installing/updating the runner, writing agent config, and starting
6
+ // the sandbox agent. They are used by:
7
+ // - API: _provisionAgentCredentials (Windows SSM)
8
+ // - API: _createLinuxSandbox (E2B bash)
9
+ // - API: direct connection handler (returns commands for SDK to execute)
10
+ // - SDK: _sendSSMCommands (direct connection, client-side SSM)
11
+ //
12
+ // Published as part of the testdriverai npm package.
13
+
14
+ 'use strict';
15
+
16
+ /**
17
+ * Build the agent config object written to the sandbox.
18
+ *
19
+ * @param {Object} opts
20
+ * @param {string} opts.sandboxId
21
+ * @param {string} opts.apiRoot
22
+ * @param {string} [opts.apiKey]
23
+ * @param {string} [opts.sentryDsn]
24
+ * @param {string} [opts.sentryEnvironment]
25
+ * @param {string} [opts.sentryChannel]
26
+ * @param {Object} opts.ablyToken - Ably token object
27
+ * @param {string} opts.channelName - Ably channel name
28
+ * @returns {Object} Agent config to serialize as JSON
29
+ */
30
+ function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnvironment, sentryChannel, ablyToken, channelName }) {
31
+ return {
32
+ sandboxId,
33
+ apiRoot,
34
+ apiKey: apiKey || undefined,
35
+ sentryDsn: sentryDsn || undefined,
36
+ sentryEnvironment: sentryEnvironment || 'production',
37
+ sentryChannel: sentryChannel || undefined,
38
+ ably: {
39
+ token: ablyToken,
40
+ channel: channelName,
41
+ // Backward compat for old runners (<=7.5.x) that expect multi-channel format
42
+ channels: { commands: channelName, responses: channelName, control: channelName, files: channelName },
43
+ },
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Generate PowerShell commands to provision the sandbox agent on Windows.
49
+ *
50
+ * The returned array is suitable for SSM AWS-RunPowerShellScript Parameters.commands.
51
+ *
52
+ * @param {Object} opts
53
+ * @param {string} opts.channel - Release channel (dev|test|canary|stable)
54
+ * @param {string} opts.configJson - JSON.stringify'd agent config
55
+ * @param {string} opts.sandboxId - For logging
56
+ * @param {string} [opts.s3DownloadUrl] - S3 pre-signed URL for dev/test (omit for npm install)
57
+ * @returns {string[]} Array of PowerShell command strings
58
+ */
59
+ function windowsProvisionCommands({ channel, configJson, sandboxId, s3DownloadUrl }) {
60
+ var useS3 = (channel === 'dev' || channel === 'test') && s3DownloadUrl;
61
+ var commands = [];
62
+
63
+ // ── 1. Stop old runner ────────────────────────────────────────────
64
+ commands.push(
65
+ "Write-Host 'Stopping old runner...'",
66
+ 'Stop-ScheduledTask -TaskName RunTestDriverAgent -ErrorAction SilentlyContinue',
67
+ 'Stop-Process -Name node -Force -ErrorAction SilentlyContinue',
68
+ "Remove-Item 'C:\\Windows\\Temp\\testdriver-agent.json' -Force -ErrorAction SilentlyContinue"
69
+ );
70
+
71
+ // ── 2. Install / update runner ────────────────────────────────────
72
+ commands.push("Set-Location 'C:\\testdriver\\sandbox-agent'");
73
+
74
+ var agentScript;
75
+
76
+ if (useS3) {
77
+ // Dev/test: download tarball from S3, extract, npm install deps
78
+ agentScript = 'sandbox-agent.js';
79
+ commands.push(
80
+ "Write-Host 'Downloading runner from S3 (" + channel + ")...'",
81
+ "$tarball = 'C:\\Windows\\Temp\\runner-dev.tgz'",
82
+ "Invoke-WebRequest -Uri '" + s3DownloadUrl + "' -OutFile $tarball",
83
+ "Write-Host 'Extracting runner...'",
84
+ "tar -xzf $tarball -C 'C:\\Windows\\Temp'",
85
+ "xcopy 'C:\\Windows\\Temp\\package\\*' 'C:\\testdriver\\sandbox-agent\\' /E /Y /I",
86
+ "Remove-Item 'C:\\Windows\\Temp\\package' -Recurse -Force -ErrorAction SilentlyContinue",
87
+ 'Remove-Item $tarball -Force -ErrorAction SilentlyContinue',
88
+ 'npm install --omit=dev 2>&1 | Write-Host',
89
+ "Write-Host 'Runner install complete (s3)'"
90
+ );
91
+ } else {
92
+ // Canary/stable (or dev/test without S3 URL): npm install by dist-tag
93
+ agentScript = 'node_modules/@testdriverai/runner/sandbox-agent.js';
94
+ var runnerTag = channel === 'stable' ? 'latest' : channel;
95
+ commands.push(
96
+ "Write-Host 'Installing @testdriverai/runner@" + runnerTag + "...'",
97
+ 'npm install @testdriverai/runner@' + runnerTag + ' --omit=dev 2>&1 | Write-Host',
98
+ "Write-Host 'Runner install complete'"
99
+ );
100
+ }
101
+
102
+ // ── 3. Regenerate run_testdriver.ps1 ──────────────────────────────
103
+ // Overwrites the baked-in script so the entry point matches the install layout.
104
+ // Uses [IO.File]::WriteAllText to avoid PowerShell variable expansion issues.
105
+ var scriptContent = [
106
+ "Write-Output 'Starting sandbox agent...'",
107
+ "Set-Location 'C:\\testdriver\\sandbox-agent'",
108
+ 'while ($true) {',
109
+ ' & node ' + agentScript + ' 2>&1 | Tee-Object -Append -FilePath C:\\testdriver\\logs\\sandbox-agent.log',
110
+ " Write-Output 'Agent exited, restarting in 2 seconds...'",
111
+ ' Start-Sleep -Seconds 2',
112
+ '}',
113
+ ].join('\r\n');
114
+
115
+ commands.push(
116
+ "Write-Host 'Regenerating run_testdriver.ps1...'",
117
+ "[IO.File]::WriteAllText('C:\\testdriver\\run_testdriver.ps1', '" + scriptContent.replace(/'/g, "''") + "')"
118
+ );
119
+
120
+ // ── 4. Write agent config ─────────────────────────────────────────
121
+ commands.push(
122
+ "Write-Host '=== Writing config ==='",
123
+ "$config = '" + configJson.replace(/'/g, "''") + "'",
124
+ "[System.IO.File]::WriteAllText('C:\\Windows\\Temp\\testdriver-agent.json', $config)",
125
+ "Write-Host 'Config written for sandbox " + sandboxId + "'"
126
+ );
127
+
128
+ // ── 5. Start runner ───────────────────────────────────────────────
129
+ commands.push(
130
+ 'Start-Sleep -Seconds 1',
131
+ 'Start-ScheduledTask -TaskName RunTestDriverAgent',
132
+ "Write-Host 'Runner started'"
133
+ );
134
+
135
+ return commands;
136
+ }
137
+
138
+ /**
139
+ * Generate the bash command to install/update the runner on Linux (E2B).
140
+ *
141
+ * @param {Object} opts
142
+ * @param {string} opts.channel - Release channel
143
+ * @param {string} [opts.s3DownloadUrl] - S3 pre-signed URL for dev/test
144
+ * @param {string} [opts.runnerPath] - Default '/opt/testdriver-runner'
145
+ * @returns {string} Single bash command (steps joined with &&)
146
+ */
147
+ function linuxRunnerInstallCommand({ channel, s3DownloadUrl, runnerPath }) {
148
+ var rp = runnerPath || '/opt/testdriver-runner';
149
+ var useS3 = (channel === 'dev' || channel === 'test') && s3DownloadUrl;
150
+ var runnerTag = channel === 'stable' ? 'latest' : channel;
151
+
152
+ if (useS3) {
153
+ return [
154
+ 'sudo rm -rf ' + rp,
155
+ 'sudo mkdir -p ' + rp,
156
+ 'sudo chown -R user:user ' + rp,
157
+ "curl -sL '" + s3DownloadUrl + "' -o /tmp/runner.tgz",
158
+ 'tar -xzf /tmp/runner.tgz -C /tmp',
159
+ 'cp -r /tmp/package/* ' + rp + '/',
160
+ 'rm -rf /tmp/runner.tgz /tmp/package',
161
+ 'cd ' + rp + ' && npm install --omit=dev --no-audit --no-fund --loglevel=error',
162
+ ].join(' && ');
163
+ }
164
+
165
+ return [
166
+ 'sudo npm install -g @testdriverai/runner@' + runnerTag + ' --omit=dev --no-audit --no-fund --loglevel=error',
167
+ 'sudo rm -rf ' + rp,
168
+ 'sudo ln -sf $(npm root -g)/@testdriverai/runner ' + rp,
169
+ ].join(' && ');
170
+ }
171
+
172
+ module.exports = {
173
+ buildAgentConfig,
174
+ windowsProvisionCommands,
175
+ linuxRunnerInstallCommand,
176
+ };
@@ -712,19 +712,25 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
712
712
  }
713
713
 
714
714
  if (message.type === "direct") {
715
- // If the API returned agent config and we have an instanceId,
716
- // provision the config to the instance via SSM (client-side).
715
+ // If the API returned provisioning commands and we have an instanceId,
716
+ // send them to the instance via SSM (client-side).
717
717
  // This runs from the user's infrastructure where AWS permissions exist,
718
718
  // rather than from the API server.
719
719
  // NOTE: For direct connections, the user MUST provide the AWS instanceId
720
720
  // because the API only knows the sandboxId, not the actual EC2 instance ID.
721
721
  var instanceId = message.instanceId;
722
- if (reply.agentConfig && instanceId) {
723
- logger.log('Provisioning agent config to instance ' + instanceId + ' via SSM...');
722
+ if (instanceId && reply.provisionCommands) {
723
+ // New path: API generated full provisioning commands (runner install + config + start)
724
+ logger.log('Provisioning instance ' + instanceId + ' via SSM (API-generated commands)...');
725
+ await this._sendSSMCommands(instanceId, reply.provisionCommands);
726
+ logger.log('Instance provisioned successfully.');
727
+ } else if (reply.agentConfig && instanceId) {
728
+ // Fallback: older API that only returns agentConfig (config-only, no runner install)
729
+ logger.log('Provisioning agent config to instance ' + instanceId + ' via SSM (legacy)...');
724
730
  await this._provisionAgentConfig(instanceId, reply.agentConfig);
725
731
  logger.log('Agent config provisioned successfully.');
726
- } else if (reply.agentConfig && !instanceId) {
727
- logger.log('Warning: agentConfig returned but no instanceId provided - cannot provision via SSM');
732
+ } else if ((reply.agentConfig || reply.provisionCommands) && !instanceId) {
733
+ logger.log('Warning: agentConfig/provisionCommands returned but no instanceId provided - cannot provision via SSM');
728
734
  }
729
735
 
730
736
  // If the API returned agent credentials (reply.agent present),
@@ -1233,9 +1239,66 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
1233
1239
  this.ps = {};
1234
1240
  }
1235
1241
 
1242
+ /**
1243
+ * Send pre-generated PowerShell commands to an EC2 instance via AWS SSM.
1244
+ * The commands are generated by the API (sdk/agent/lib/provision-commands.js)
1245
+ * so provisioning logic lives in one place.
1246
+ */
1247
+ async _sendSSMCommands(instanceId, commands) {
1248
+ const { execSync } = require('child_process');
1249
+ const { writeFileSync, unlinkSync } = require('fs');
1250
+ const { join } = require('path');
1251
+ const { tmpdir } = require('os');
1252
+ const { randomUUID } = require('crypto');
1253
+
1254
+ const region = process.env.AWS_REGION || 'us-east-2';
1255
+ const paramsJson = JSON.stringify({ commands: commands });
1256
+ const tmpFile = join(tmpdir(), 'td-provision-' + randomUUID() + '.json');
1257
+ writeFileSync(tmpFile, paramsJson);
1258
+
1259
+ try {
1260
+ const output = execSync(
1261
+ 'aws ssm send-command --region "' + region + '" --instance-ids "' + instanceId + '" ' +
1262
+ '--document-name "AWS-RunPowerShellScript" ' +
1263
+ '--parameters file://' + tmpFile + ' --output json',
1264
+ { encoding: 'utf-8', timeout: 30000 }
1265
+ );
1266
+ var cmdId = JSON.parse(output).Command.CommandId;
1267
+ logger.log('SSM command sent: ' + cmdId);
1268
+
1269
+ // Wait for the command to complete
1270
+ execSync(
1271
+ 'aws ssm wait command-executed --region "' + region + '" ' +
1272
+ '--command-id "' + cmdId + '" --instance-id "' + instanceId + '"',
1273
+ { encoding: 'utf-8', timeout: 300000 } // 5 min — runner install can take a while
1274
+ );
1275
+
1276
+ // Get the command output for debugging
1277
+ try {
1278
+ var invocationOutput = execSync(
1279
+ 'aws ssm get-command-invocation --region "' + region + '" ' +
1280
+ '--command-id "' + cmdId + '" --instance-id "' + instanceId + '" --output json',
1281
+ { encoding: 'utf-8', timeout: 30000 }
1282
+ );
1283
+ var invocation = JSON.parse(invocationOutput);
1284
+ if (invocation.StandardOutputContent) {
1285
+ logger.log('SSM output:\n' + invocation.StandardOutputContent);
1286
+ }
1287
+ if (invocation.StandardErrorContent) {
1288
+ logger.warn('SSM errors:\n' + invocation.StandardErrorContent);
1289
+ }
1290
+ } catch (e) {
1291
+ logger.warn('Could not retrieve SSM command output: ' + e.message);
1292
+ }
1293
+ } finally {
1294
+ try { unlinkSync(tmpFile); } catch (e) { /* ignore */ }
1295
+ }
1296
+ }
1297
+
1236
1298
  /**
1237
1299
  * Write the agent config JSON to an EC2 instance via AWS SSM.
1238
1300
  * Runs client-side so the API doesn't need AWS permissions on user infra.
1301
+ * LEGACY: Used when connecting to an API that doesn't return provisionCommands.
1239
1302
  */
1240
1303
  async _provisionAgentConfig(instanceId, agentConfig) {
1241
1304
  const { execSync } = require('child_process');
@@ -31,8 +31,7 @@ class Dashcam {
31
31
  this.apiKey =
32
32
  options.apiKey ||
33
33
  client.apiKey ||
34
- client.config?.TD_API_KEY ||
35
- "4e93d8bf-3886-4d26-a144-116c4063522d";
34
+ client.config?.TD_API_KEY;
36
35
  this.autoStart = options.autoStart ?? false;
37
36
  this.logs = options.logs || [];
38
37
  this.recording = false;
@@ -132,21 +131,6 @@ class Dashcam {
132
131
  return `https://${prefix}-web.fly.dev`;
133
132
  }
134
133
 
135
- // Render PR previews: map API service to Web service
136
- // canary-api-pr-123.onrender.com -> canary-web-pr-123.onrender.com
137
- // testdriver-api-i4m4-pr-123.onrender.com -> web-i4m4-pr-123.onrender.com
138
- const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
139
- if (renderPrMatch) {
140
- const [, prefix, suffix, prSuffix] = renderPrMatch;
141
- let webPrefix;
142
- if (prefix === 'testdriver' && suffix) {
143
- webPrefix = 'web' + suffix;
144
- } else {
145
- webPrefix = prefix + '-web';
146
- }
147
- return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
148
- }
149
-
150
134
  // Cloudflare tunnels, custom domains, etc.: the web console is served
151
135
  // from the same origin as the API, so return apiRoot as-is.
152
136
  return apiRoot;
@@ -181,18 +181,33 @@ function setupConsoleSpy(client, taskId) {
181
181
 
182
182
  /**
183
183
  * Unregister a client so its sandbox no longer receives forwarded logs.
184
- * When the last client is removed we restore the original console methods so
185
- * the Vitest worker fork can exit cleanly (unreleased vi.spyOn mocks prevent
186
- * the worker from shutting down, producing "Worker exited unexpectedly").
187
- * If another test starts later (e.g. a retry), installConsoleSpy() will
188
- * re-install the spy on demand.
184
+ *
185
+ * Between sequential `it()` blocks we intentionally keep the spies installed.
186
+ * The `bufferConsoleToClients` function is a no-op when `activeClients` is
187
+ * empty, so leaving the spy in place is harmless and avoids a non-atomic
188
+ * restore/re-install race that can corrupt console method references.
189
+ *
190
+ * Spies are torn down once at process exit so the Vitest worker fork can
191
+ * shut down cleanly (unreleased vi.spyOn mocks prevent exit).
192
+ *
189
193
  * @param {import('../../sdk.js').default} client - TestDriver client instance
190
194
  */
191
195
  function cleanupConsoleSpy(client) {
192
196
  _consoleSpy.activeClients.delete(client);
193
197
 
194
- // Restore spies when no tests need them — allows clean worker exit
195
- if (_consoleSpy.activeClients.size === 0 && _consoleSpy.spies) {
198
+ if (debugConsoleSpy) {
199
+ process.stdout.write(
200
+ `[DEBUG cleanupConsoleSpy] clients remaining: ${_consoleSpy.activeClients.size}\n`,
201
+ );
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Tear down the singleton console spy completely.
207
+ * Called once on process exit so the Vitest worker can shut down cleanly.
208
+ */
209
+ function teardownConsoleSpy() {
210
+ if (_consoleSpy.spies) {
196
211
  _consoleSpy.spies.log.mockRestore();
197
212
  _consoleSpy.spies.error.mockRestore();
198
213
  _consoleSpy.spies.warn.mockRestore();
@@ -202,21 +217,26 @@ function cleanupConsoleSpy(client) {
202
217
  _consoleSpy.installed = false;
203
218
 
204
219
  if (debugConsoleSpy) {
205
- process.stdout.write("[DEBUG cleanupConsoleSpy] All spies restored\n");
220
+ process.stdout.write("[DEBUG teardownConsoleSpy] All spies restored\n");
206
221
  }
207
222
  }
208
-
209
- if (debugConsoleSpy) {
210
- process.stdout.write(
211
- `[DEBUG cleanupConsoleSpy] clients remaining: ${_consoleSpy.activeClients.size}\n`,
212
- );
213
- }
214
223
  }
215
224
 
225
+ // Restore console spies on process exit so the Vitest worker can exit cleanly
226
+ process.on("exit", teardownConsoleSpy);
227
+
216
228
  // Weak maps to store instances per test context
217
229
  const testDriverInstances = new WeakMap();
218
230
  const lifecycleHandlers = new WeakMap();
219
231
 
232
+ /**
233
+ * Module-level promise tracking the most recent test's disconnect.
234
+ * When sequential `it()` blocks run, the next test awaits this promise
235
+ * before connecting — ensuring the previous sandbox is fully torn down
236
+ * even if the cleanup's disconnect timeout fired early.
237
+ */
238
+ let _pendingDisconnect = null;
239
+
220
240
  /**
221
241
  * Upload buffered SDK + console logs directly to S3 via the existing Log system.
222
242
  * Extracts the replayId from the dashcam URL, calls POST /api/v1/logs to create
@@ -436,6 +456,14 @@ export function TestDriver(context, options = {}) {
436
456
  const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
437
457
 
438
458
  testdriver.__connectionPromise = (async () => {
459
+ // Wait for any previous test's disconnect to fully complete.
460
+ // This prevents the new sandbox connection from racing with a
461
+ // lingering disconnect when sequential `it()` blocks run.
462
+ if (_pendingDisconnect) {
463
+ await _pendingDisconnect.catch(() => {});
464
+ _pendingDisconnect = null;
465
+ }
466
+
439
467
  if (debugConsoleSpy) {
440
468
  console.log(
441
469
  "[DEBUG] Before auth - sandbox.instanceSocketConnected:",
@@ -709,10 +737,19 @@ export function TestDriver(context, options = {}) {
709
737
  await currentInstance.__connectionPromise.catch(() => { }); // Ignore connection errors during cleanup
710
738
  }
711
739
 
712
- // Disconnect with timeout
740
+ // Disconnect track the promise at module level so the *next* test
741
+ // can await it before connecting, even if the timeout fires first.
742
+ const disconnectPromise = currentInstance.disconnect().catch((err) => {
743
+ console.error("Error during disconnect:", err);
744
+ });
745
+ _pendingDisconnect = disconnectPromise;
746
+
747
+ // Allow up to 30 s for Ably presence leave / channel detach.
748
+ // If it takes longer, cleanup resolves but _pendingDisconnect
749
+ // keeps the reference so the next test still waits.
713
750
  await Promise.race([
714
- currentInstance.disconnect(),
715
- new Promise((resolve) => setTimeout(resolve, 5000)), // 5s timeout for disconnect
751
+ disconnectPromise,
752
+ new Promise((resolve) => setTimeout(resolve, 30000)),
716
753
  ]);
717
754
  } catch (error) {
718
755
  console.error("Error disconnecting client:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.8.0-test.71",
3
+ "version": "7.8.0-test.73",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",
package/vitest.config.mjs CHANGED
@@ -19,6 +19,10 @@ const sharedTestConfig = {
19
19
  maxConcurrency: 100,
20
20
  disableConsoleIntercept: false,
21
21
  silent: false,
22
+ // Use child-process forks so each test FILE gets a completely clean
23
+ // Node.js process — no shared Ably connections, module-level singletons,
24
+ // or Sentry globals bleeding between files.
25
+ pool: "forks",
22
26
  reporters: [
23
27
  "verbose",
24
28
  TestDriver()
@@ -1,458 +0,0 @@
1
- # Core Classes
2
-
3
- Direct access to TestDriver and Dashcam classes for full manual control.
4
-
5
- ## Overview
6
-
7
- The core module provides the fundamental building blocks without any automatic lifecycle management. Use this when you need complete control or when working outside of Vitest.
8
-
9
- ```javascript
10
- import { TestDriver, Dashcam } from 'testdriverai/core';
11
-
12
- const client = new TestDriver(apiKey, { os: 'linux' });
13
- await client.auth();
14
- await client.connect();
15
-
16
- const dashcam = new Dashcam(client);
17
- await dashcam.start();
18
-
19
- // Your test code
20
-
21
- await dashcam.stop();
22
- await client.disconnect();
23
- ```
24
-
25
- ## TestDriver Class
26
-
27
- ### Constructor
28
-
29
- ```javascript
30
- import { TestDriver } from 'testdriverai/core';
31
-
32
- const client = new TestDriver(apiKey, options);
33
- ```
34
-
35
- **Parameters:**
36
- - `apiKey` - Your TestDriver API key (string)
37
- - `options` - Configuration object
38
-
39
- **Options:**
40
- ```javascript
41
- {
42
- os: 'linux', // Target OS: 'linux', 'mac', 'windows'
43
- apiRoot: 'https://...', // API endpoint
44
- resolution: '1366x768', // Screen resolution
45
- newSandbox: true, // Create new sandbox
46
- analytics: true, // Enable analytics
47
- cacheThresholds: {
48
- find: 0.05,
49
- findAll: 0.05
50
- }
51
- }
52
- ```
53
-
54
- ### Methods
55
-
56
- #### auth()
57
- Authenticate with TestDriver API.
58
-
59
- ```javascript
60
- await client.auth();
61
- ```
62
-
63
- #### connect(options)
64
- Connect to a sandbox instance.
65
-
66
- ```javascript
67
- await client.connect({
68
- new: true // Create new sandbox (default: true)
69
- });
70
- ```
71
-
72
- #### disconnect()
73
- Disconnect from sandbox and clean up.
74
-
75
- ```javascript
76
- await client.disconnect();
77
- ```
78
-
79
- #### find(query)
80
- Find an element on screen.
81
-
82
- ```javascript
83
- const element = await client.find('Login button');
84
- await element.click();
85
-
86
- // Or chain:
87
- await client.find('Login button').then(el => el.click());
88
- ```
89
-
90
- #### findAll(query)
91
- Find all matching elements.
92
-
93
- ```javascript
94
- const buttons = await client.findAll('button');
95
- console.log('Found', buttons.length, 'buttons');
96
- ```
97
-
98
- #### click(target)
99
- Click an element.
100
-
101
- ```javascript
102
- await client.click('Submit button');
103
- ```
104
-
105
- #### type(text)
106
- Type text (at current cursor position).
107
-
108
- ```javascript
109
- await client.type('username@example.com');
110
- ```
111
-
112
- #### pressKeys(keys)
113
- Press keyboard keys or shortcuts.
114
-
115
- ```javascript
116
- await client.pressKeys(['ctrl', 'a']);
117
- await client.pressKeys(['enter']);
118
- ```
119
-
120
- #### exec(shell, command, timeout, ignoreError)
121
- Execute shell command.
122
-
123
- ```javascript
124
- const output = await client.exec(
125
- 'sh', // Shell: 'sh' or 'pwsh'
126
- 'google-chrome "https://..." &', // Command
127
- 30000, // Timeout (ms)
128
- false // Ignore errors
129
- );
130
- ```
131
-
132
- #### focusApplication(appName)
133
- Focus/activate an application window.
134
-
135
- ```javascript
136
- await client.focusApplication('Google Chrome');
137
- await client.focusApplication('Visual Studio Code');
138
- ```
139
-
140
- #### scroll(direction, amount)
141
- Scroll the page.
142
-
143
- ```javascript
144
- await client.scroll('down', 500);
145
- await client.scroll('up', 200);
146
- ```
147
-
148
- #### assert(query)
149
- Assert that something is true on screen.
150
-
151
- ```javascript
152
- const result = await client.assert('Login successful message appears');
153
- // Returns boolean
154
- ```
155
-
156
- ## Dashcam Class
157
-
158
- ### Constructor
159
-
160
- ```javascript
161
- import { Dashcam } from 'testdriverai/core';
162
-
163
- const dashcam = new Dashcam(client, options);
164
- ```
165
-
166
- **Parameters:**
167
- - `client` - TestDriver instance (required)
168
- - `options` - Configuration object (optional)
169
-
170
- **Options:**
171
- ```javascript
172
- {
173
- apiKey: process.env.TD_API_KEY // API key (same as TestDriver)
174
- }
175
- ```
176
-
177
- ### Methods
178
-
179
- #### auth(apiKey)
180
- Authenticate with Dashcam CLI.
181
-
182
- ```javascript
183
- await dashcam.auth(); // Uses TD_API_KEY env var (same as TestDriver)
184
- // Or:
185
- await dashcam.auth('your-api-key');
186
- ```
187
-
188
- #### start()
189
- Start recording.
190
-
191
- ```javascript
192
- await dashcam.start();
193
- ```
194
-
195
- #### stop()
196
- Stop recording and get replay URL.
197
-
198
- ```javascript
199
- const url = await dashcam.stop();
200
- console.log('Replay:', url);
201
- // Returns: https://app.dashcam.io/replay/...
202
- ```
203
-
204
- #### isRecording()
205
- Check if currently recording.
206
-
207
- ```javascript
208
- if (dashcam.isRecording()) {
209
- console.log('Recording in progress');
210
- }
211
- ```
212
-
213
- #### addFileLog(path, name)
214
- Add a file to Dashcam logs.
215
-
216
- ```javascript
217
- await dashcam.addFileLog('/var/log/app.log', 'Application Log');
218
- ```
219
-
220
- #### addApplicationLog(application, name)
221
- Track an application in Dashcam.
222
-
223
- ```javascript
224
- await dashcam.addApplicationLog('Google Chrome', 'Browser');
225
- ```
226
-
227
- ## Complete Examples
228
-
229
- ### Basic Test
230
-
231
- ```javascript
232
- import { TestDriver } from 'testdriverai/core';
233
-
234
- async function runTest() {
235
- const client = new TestDriver(process.env.TD_API_KEY, {
236
- os: 'linux',
237
- resolution: '1920x1080'
238
- });
239
-
240
- try {
241
- // Connect
242
- await client.auth();
243
- await client.connect({ new: true });
244
-
245
- // Focus browser
246
- await client.focusApplication('Google Chrome');
247
-
248
- // Navigate
249
- const urlBar = await client.find('URL bar');
250
- await urlBar.click();
251
- await client.type('https://example.com');
252
- await client.pressKeys(['enter']);
253
-
254
- // Test
255
- await client.find('heading').click();
256
- const result = await client.assert('page loaded successfully');
257
-
258
- console.log('Test passed:', result);
259
- } finally {
260
- // Always disconnect
261
- await client.disconnect();
262
- }
263
- }
264
-
265
- runTest();
266
- ```
267
-
268
- ### With Dashcam
269
-
270
- ```javascript
271
- import { TestDriver, Dashcam } from 'testdriverai/core';
272
-
273
- async function runRecordedTest() {
274
- const client = new TestDriver(process.env.TD_API_KEY, { os: 'linux' });
275
- const dashcam = new Dashcam(client);
276
-
277
- try {
278
- // Setup
279
- await client.auth();
280
- await client.connect();
281
-
282
- // Start recording
283
- await dashcam.auth();
284
- await dashcam.start();
285
-
286
- // Run test
287
- await client.focusApplication('Google Chrome');
288
- await client.find('button').click();
289
-
290
- // Stop recording
291
- const url = await dashcam.stop();
292
- console.log('Replay URL:', url);
293
-
294
- } finally {
295
- await client.disconnect();
296
- }
297
- }
298
-
299
- runRecordedTest();
300
- ```
301
-
302
- ### Multiple Operations
303
-
304
- ```javascript
305
- import { TestDriver } from 'testdriverai/core';
306
-
307
- async function complexTest() {
308
- const client = new TestDriver(process.env.TD_API_KEY, { os: 'linux' });
309
-
310
- await client.auth();
311
- await client.connect();
312
-
313
- try {
314
- // Launch application
315
- await client.exec(
316
- 'sh',
317
- 'google-chrome --start-maximized "https://example.com" &',
318
- 30000
319
- );
320
-
321
- await client.focusApplication('Google Chrome');
322
-
323
- // Fill form
324
- await client.find('username field').type('user@example.com');
325
- await client.find('password field').type('password123');
326
- await client.find('submit button').click();
327
-
328
- // Navigate
329
- await client.find('dashboard link').click();
330
-
331
- // Scroll and interact
332
- await client.scroll('down', 500);
333
- await client.find('settings button').click();
334
-
335
- // Verify
336
- const result = await client.assert('settings page is visible');
337
- console.log('Test result:', result);
338
-
339
- } finally {
340
- await client.disconnect();
341
- }
342
- }
343
-
344
- complexTest();
345
- ```
346
-
347
- ### Error Handling
348
-
349
- ```javascript
350
- import { TestDriver } from 'testdriverai/core';
351
-
352
- async function testWithErrorHandling() {
353
- const client = new TestDriver(process.env.TD_API_KEY, { os: 'linux' });
354
-
355
- try {
356
- await client.auth();
357
- await client.connect();
358
-
359
- await client.focusApplication('Google Chrome');
360
-
361
- // Try to find element
362
- const element = await client.find('optional button');
363
-
364
- if (element.found()) {
365
- await element.click();
366
- } else {
367
- console.log('Button not found, continuing...');
368
- }
369
-
370
- } catch (error) {
371
- console.error('Test failed:', error);
372
- throw error;
373
- } finally {
374
- // Always cleanup
375
- try {
376
- await client.disconnect();
377
- } catch (cleanupError) {
378
- console.error('Cleanup error:', cleanupError);
379
- }
380
- }
381
- }
382
- ```
383
-
384
- ## TypeScript Support
385
-
386
- ```typescript
387
- import { TestDriver, Dashcam, TestDriverOptions, DashcamOptions } from 'testdriverai/core';
388
-
389
- const options: TestDriverOptions = {
390
- os: 'linux',
391
- resolution: '1366x768',
392
- analytics: true
393
- };
394
-
395
- const client = new TestDriver(process.env.TD_API_KEY!, options);
396
-
397
- const dashcamOptions: DashcamOptions = {
398
- apiKey: process.env.TD_API_KEY // Same as TestDriver
399
- };
400
-
401
- const dashcam = new Dashcam(client, dashcamOptions);
402
- ```
403
-
404
- ## When to Use Core Classes
405
-
406
- **Use core classes when:**
407
- - You need full manual control
408
- - You're not using Vitest
409
- - You're integrating with another test framework
410
- - You're building custom abstractions
411
- - You're debugging lifecycle issues
412
- - You're writing scripts, not tests
413
-
414
- **Use hooks or provision() instead when:**
415
- - You're using Vitest
416
- - You want automatic cleanup
417
- - You prefer simpler APIs
418
- - You're testing common applications
419
-
420
- ## Best Practices
421
-
422
- 1. **Always disconnect** - Use try/finally to ensure cleanup
423
- 2. **Check element.found()** - Before using optional elements
424
- 3. **Handle errors gracefully** - Log and re-throw when appropriate
425
- 4. **Use TypeScript** - Get type safety and autocomplete
426
- 5. **Set reasonable timeouts** - Default is 30 seconds for exec()
427
- 6. **Focus applications** - Before interacting with them
428
-
429
- ## Platform Differences
430
-
431
- ### Shell Commands
432
-
433
- **Linux/Mac:**
434
- ```javascript
435
- await client.exec('sh', 'google-chrome "https://example.com" &', 30000);
436
- ```
437
-
438
- **Windows:**
439
- ```javascript
440
- await client.exec(
441
- 'pwsh',
442
- 'Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "https://example.com"',
443
- 30000
444
- );
445
- ```
446
-
447
- ### Application Names
448
-
449
- - Linux: `'Google Chrome'`, `'Firefox'`, `'Visual Studio Code'`
450
- - Mac: `'Google Chrome'`, `'Firefox'`, `'Visual Studio Code'`
451
- - Windows: `'Google Chrome'`, `'Firefox'`, `'Visual Studio Code'`
452
-
453
- ## See Also
454
-
455
- - [Provision API](./PROVISION.md) - Simplified API for common apps
456
- - [Hooks API](./HOOKS.md) - Automatic lifecycle management
457
- - [Migration Guide](../MIGRATION.md) - Upgrading from v6
458
- - [API Reference](../../sdk.js) - Full source code