testdriverai 7.3.34 → 7.3.36

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.
@@ -507,15 +507,64 @@ it("should incrementally build test", async (context) => {
507
507
 
508
508
  ```javascript
509
509
  const testdriver = TestDriver(context, {
510
- newSandbox: true, // Create new sandbox (default: true)
511
- preview: "browser", // "browser" | "ide" | "none" (default: "browser")
512
- reconnect: false, // Reconnect to last sandbox (default: false)
513
- keepAlive: 30000, // Keep sandbox alive after test (default: 30000ms / 30 seconds)
514
- os: "linux", // 'linux' | 'windows' (default: 'linux')
515
- resolution: "1366x768", // Sandbox resolution
516
- cache: true, // Enable element caching (default: true)
517
- cacheKey: "my-test", // Cache key for element finding
518
- autoScreenshots: true, // Capture screenshots before/after each command (default: true)
510
+ // === Sandbox & Connection ===
511
+ newSandbox: true, // Force creation of a new sandbox (default: true)
512
+ reconnect: false, // Reconnect to last sandbox (default: false)
513
+ keepAlive: 30000, // Keep sandbox alive after test in ms (default: 30000)
514
+ os: "linux", // 'linux' | 'windows' (default: 'linux')
515
+ resolution: "1366x768", // Sandbox resolution (e.g., '1920x1080')
516
+ ip: "203.0.113.42", // Direct IP for self-hosted sandbox
517
+ sandboxAmi: "ami-1234", // Custom AMI ID (AWS deployments)
518
+ sandboxInstance: "i3.metal", // EC2 instance type (AWS deployments)
519
+
520
+ // === Preview & Debugging ===
521
+ preview: "browser", // "browser" | "ide" | "none" (default: "browser")
522
+ headless: false, // @deprecated - use preview: "none" instead
523
+ debugOnFailure: false, // Keep sandbox alive on test failure for debugging
524
+
525
+ // === Caching ===
526
+ cache: true, // Enable element caching (default: true)
527
+ // Or use advanced caching config:
528
+ // cache: {
529
+ // enabled: true,
530
+ // thresholds: {
531
+ // find: { screen: 0.05, element: 0.8 },
532
+ // assert: 0.05
533
+ // }
534
+ // },
535
+ cacheKey: "my-test", // Cache key for element finding operations
536
+
537
+ // === Recording & Screenshots ===
538
+ dashcam: true, // Enable/disable Dashcam video recording (default: true)
539
+ autoScreenshots: true, // Capture screenshots before/after each command (default: true)
540
+
541
+ // === AI Configuration ===
542
+ ai: { // Global AI sampling configuration
543
+ temperature: 0, // 0 = deterministic, higher = more creative
544
+ top: {
545
+ p: 0.9, // Top-P nucleus sampling (0-1)
546
+ k: 40, // Top-K sampling (1 = most likely, 0 = disabled)
547
+ },
548
+ },
549
+
550
+ // === Screen Change Detection ===
551
+ redraw: true, // Enable redraw detection (default: true)
552
+ // Or use advanced redraw config:
553
+ // redraw: {
554
+ // enabled: true,
555
+ // thresholds: {
556
+ // screen: 0.05, // Pixel diff threshold (0-1), false to disable
557
+ // network: false, // Monitor network activity (default: false)
558
+ // }
559
+ // },
560
+
561
+ // === Logging & Analytics ===
562
+ logging: true, // Enable console logging output (default: true)
563
+ analytics: true, // Enable analytics tracking (default: true)
564
+
565
+ // === Advanced ===
566
+ apiRoot: "https://...", // API endpoint URL (for self-hosted deployments)
567
+ environment: {}, // Additional environment variables for the sandbox
519
568
  });
520
569
  ```
521
570
 
@@ -22,7 +22,7 @@ on:
22
22
 
23
23
  jobs:
24
24
  test:
25
- runs-on: ubuntu-latest
25
+ runs-on: testdriver-32
26
26
 
27
27
  steps:
28
28
  - name: Checkout repository
@@ -39,19 +39,8 @@ jobs:
39
39
  - name: Install dependencies
40
40
  run: npm ci
41
41
 
42
- - name: Debug Environment
43
- run: |
44
- echo "Checking environment variables..."
45
- if [ -n "${{ secrets.TWOCAPTCHA_API_KEY }}" ]; then
46
- echo "TWOCAPTCHA_API_KEY is set (length: ${#TWOCAPTCHA_API_KEY})"
47
- else
48
- echo "TWOCAPTCHA_API_KEY is NOT set"
49
- fi
50
- env:
51
- TWOCAPTCHA_API_KEY: ${{ secrets.TWOCAPTCHA_API_KEY }}
52
-
53
- - name: Run Windows tests with self-hosted instances
54
- run: set -o pipefail && npx vitest run ${{ inputs.test_pattern }} 2>&1 | tee test-output.log
42
+ - name: Run Windows tests
43
+ run: set -o pipefail && npx vitest run ${{ inputs.test_pattern }} --maxWorkers 32 --sequence.concurrent 2>&1 | tee test-output.log
55
44
  env:
56
45
  TD_API_KEY: ${{ secrets.TD_API_KEY }}
57
46
  TWOCAPTCHA_API_KEY: ${{ secrets.TWOCAPTCHA_API_KEY }}
package/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [7.3.36](https://github.com/testdriverai/testdriverai/compare/v7.3.35...v7.3.36) (2026-02-24)
2
+
3
+
4
+ ### Reverts
5
+
6
+ * Revert "Fix hanging node processes on Ctrl+C (#654)" (#683) ([5e68748](https://github.com/testdriverai/testdriverai/commit/5e6874825c6718e006bbf84e2ba5edae57d173ac)), closes [#654](https://github.com/testdriverai/testdriverai/issues/654) [#683](https://github.com/testdriverai/testdriverai/issues/683)
7
+
8
+
9
+
10
+ ## [7.3.35](https://github.com/testdriverai/testdriverai/compare/v7.3.34...v7.3.35) (2026-02-24)
11
+
12
+
13
+
1
14
  ## [7.3.34](https://github.com/testdriverai/testdriverai/compare/v7.3.33...v7.3.34) (2026-02-24)
2
15
 
3
16
 
package/agent/index.js CHANGED
@@ -1635,105 +1635,6 @@ ${regression}
1635
1635
  this.emitter.emit(events.log.log, `${inputFile} (end)`);
1636
1636
  }
1637
1637
 
1638
- // Returns the path to the last sandbox file
1639
- getLastSandboxFilePath() {
1640
- const testdriverDir = path.join(process.cwd(), ".testdriver");
1641
- return path.join(testdriverDir, "last-sandbox");
1642
- }
1643
-
1644
- // Returns full sandbox info from last-sandbox file (no timeout - let API validate)
1645
- getLastSandboxId() {
1646
- const lastSandboxFile = this.getLastSandboxFilePath();
1647
-
1648
- if (fs.existsSync(lastSandboxFile)) {
1649
- try {
1650
- const fileContent = fs.readFileSync(lastSandboxFile, "utf-8").trim();
1651
-
1652
- // Parse sandbox info (supports both old format and new format)
1653
- let sandboxInfo;
1654
- try {
1655
- sandboxInfo = JSON.parse(fileContent);
1656
- } catch {
1657
- return { sandboxId: fileContent || null };
1658
- }
1659
-
1660
- return {
1661
- sandboxId: sandboxInfo.sandboxId || sandboxInfo.instanceId || null,
1662
- os: sandboxInfo.os || "linux",
1663
- ami: sandboxInfo.ami || null,
1664
- instanceType: sandboxInfo.instanceType || null,
1665
- timestamp: sandboxInfo.timestamp || null,
1666
- };
1667
- } catch {
1668
- // ignore errors
1669
- }
1670
- }
1671
- return null;
1672
- }
1673
-
1674
- // Returns sandboxId to use if AMI/instance type match current requirements
1675
- getRecentSandboxId() {
1676
- const sandboxInfo = this.getLastSandboxId();
1677
-
1678
- if (!sandboxInfo || !sandboxInfo.sandboxId) {
1679
- return null;
1680
- }
1681
-
1682
- // Check if AMI and instance type match current requirements
1683
- const currentAmi = this.sandboxAmi || null;
1684
- const currentInstance = this.sandboxInstance || null;
1685
- const storedAmi = sandboxInfo.ami || null;
1686
- const storedInstance = sandboxInfo.instanceType || null;
1687
-
1688
- if (currentAmi === storedAmi && currentInstance === storedInstance) {
1689
- return sandboxInfo.sandboxId;
1690
- } else {
1691
- this.emitter.emit(
1692
- events.log.log,
1693
- theme.dim(
1694
- "Recent sandbox found but AMI/instance type doesn't match current requirements",
1695
- ),
1696
- );
1697
- return null;
1698
- }
1699
- }
1700
-
1701
- saveLastSandboxId(sandboxId, osType = "linux") {
1702
- const lastSandboxFile = this.getLastSandboxFilePath();
1703
- const testdriverDir = path.dirname(lastSandboxFile);
1704
-
1705
- try {
1706
- // Ensure .testdriver directory exists
1707
- if (!fs.existsSync(testdriverDir)) {
1708
- fs.mkdirSync(testdriverDir, { recursive: true });
1709
- }
1710
-
1711
- const sandboxInfo = {
1712
- sandboxId: sandboxId,
1713
- os: osType,
1714
- ami: this.sandboxAmi || null,
1715
- instanceType: this.sandboxInstance || null,
1716
- timestamp: new Date().toISOString(),
1717
- };
1718
- fs.writeFileSync(lastSandboxFile, JSON.stringify(sandboxInfo, null, 2), {
1719
- encoding: "utf-8",
1720
- });
1721
- } catch {
1722
- // ignore errors
1723
- }
1724
- }
1725
-
1726
- clearRecentSandboxId() {
1727
- const lastSandboxFile = this.getLastSandboxFilePath();
1728
- try {
1729
- if (fs.existsSync(lastSandboxFile)) {
1730
- fs.unlinkSync(lastSandboxFile);
1731
- }
1732
- } catch {
1733
- // ignore errors
1734
- }
1735
- }
1736
-
1737
1638
  async buildEnv(options = {}) {
1738
1639
  // If instance already exists, do not build environment again
1739
1640
  if (this.instance) {
@@ -1762,10 +1663,8 @@ ${regression}
1762
1663
 
1763
1664
  if (heal) this.healMode = heal;
1764
1665
 
1765
- // If createNew flag is set, clear the recent sandbox file to force creating a new sandbox
1666
+ // If createNew flag is set, clear sandboxId to prevent reconnection attempts
1766
1667
  if (createNew) {
1767
- this.clearRecentSandboxId();
1768
- // Also clear this.sandboxId to prevent reconnection attempts
1769
1668
  this.sandboxId = null;
1770
1669
  if (!this.config.CI && !this.newSandbox) {
1771
1670
  this.emitter.emit(events.log.log, theme.dim("Creating a new sandbox"));
@@ -1780,8 +1679,6 @@ ${regression}
1780
1679
  // order is important!
1781
1680
  await this.connectToSandboxService();
1782
1681
 
1783
- const recentId = createNew ? null : this.getRecentSandboxId();
1784
-
1785
1682
  // Set sandbox ID for reconnection (only if not creating new and recent ID exists)
1786
1683
  if (this.ip) {
1787
1684
  let instance = await this.sandbox.send({
@@ -1794,13 +1691,13 @@ ${regression}
1794
1691
  // Store connection params for reconnection
1795
1692
  // For direct IP connections, store as a direct type so reconnection
1796
1693
  // sends a 'direct' message instead of 'connect' with an IP as sandboxId
1797
- this.sandbox._lastConnectParams = {
1694
+ this.sandbox.setConnectionParams({
1798
1695
  type: 'direct',
1799
1696
  ip: this.ip,
1800
1697
  sandboxId: instance?.instance?.instanceId || instance?.instance?.sandboxId || null,
1801
1698
  persist: true,
1802
1699
  keepAlive: this.keepAlive,
1803
- };
1700
+ });
1804
1701
 
1805
1702
  // Mark instance socket as connected so console logs are forwarded
1806
1703
  this.sandbox.instanceSocketConnected = true;
@@ -1811,33 +1708,6 @@ ${regression}
1811
1708
  await this.runLifecycle("provision");
1812
1709
 
1813
1710
  return;
1814
- } else if (!createNew && recentId) {
1815
- // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1816
- this.emitter.emit(
1817
- events.log.narration,
1818
- theme.dim(`using recent sandbox: ${recentId}`),
1819
- );
1820
- this.sandboxId = recentId;
1821
-
1822
- try {
1823
- let instance = await this.connectToSandboxDirect(
1824
- this.sandboxId,
1825
- true, // always persist by default
1826
- this.keepAlive, // pass keepAlive TTL
1827
- );
1828
-
1829
- this.instance = instance;
1830
-
1831
- await this.renderSandbox(instance, headless);
1832
- return;
1833
- } catch (error) {
1834
- // If connection fails, fall through to creating a new sandbox
1835
- this.emitter.emit(
1836
- events.log.narration,
1837
- theme.dim(`failed to connect to recent sandbox, creating new one...`),
1838
- );
1839
- console.error("Failed to reconnect to sandbox:", error);
1840
- }
1841
1711
  } else if (!createNew && this.sandboxId && !this.config.CI) {
1842
1712
  // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1843
1713
  // Attempt to connect to known instance
@@ -1892,9 +1762,6 @@ ${regression}
1892
1762
  this.sandboxId =
1893
1763
  newSandbox?.sandbox?.sandboxId || newSandbox?.sandbox?.instanceId;
1894
1764
 
1895
- // Use the configured sandbox OS type
1896
- this.saveLastSandboxId(this.sandboxId, this.sandboxOs);
1897
-
1898
1765
  let instance = await this.connectToSandboxDirect(
1899
1766
  this.sandboxId,
1900
1767
  true, // always persist by default
@@ -2332,12 +2199,6 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2332
2199
  }
2333
2200
 
2334
2201
  // Success - got a sandbox
2335
- if (response.sandbox && response.sandbox.sandboxId) {
2336
- this.saveLastSandboxId(response.sandbox.sandboxId, this.sandboxOs);
2337
- } else if (response.sandbox && response.sandbox.instanceId) {
2338
- this.saveLastSandboxId(response.sandbox.instanceId, this.sandboxOs);
2339
- }
2340
-
2341
2202
  return response;
2342
2203
  }
2343
2204
  }
@@ -8,6 +8,8 @@ const logger = require("./logger");
8
8
  let server = null;
9
9
  let wss = null;
10
10
  let clients = new Set();
11
+ let refCount = 0; // Number of active consumers (for concurrent test safety)
12
+ let debuggerUrl = null; // Stored URL of running debugger
11
13
 
12
14
  function createDebuggerServer(config = {}) {
13
15
  const port = config.TD_DEBUGGER_PORT || 0; // 0 means find available port
@@ -76,7 +78,12 @@ function createDebuggerServer(config = {}) {
76
78
 
77
79
  // Start server on available port
78
80
  server.listen(port, "localhost", () => {
79
- const actualPort = server.address().port;
81
+ const address = server.address();
82
+ if (!address) {
83
+ reject(new Error("Server started but address is not available"));
84
+ return;
85
+ }
86
+ const actualPort = address.port;
80
87
  resolve({ port: actualPort, server, wss });
81
88
  });
82
89
 
@@ -124,7 +131,43 @@ async function startDebugger(config = {}, emitter) {
124
131
  }
125
132
  }
126
133
 
127
- function stopDebugger() {
134
+ /**
135
+ * Acquire a reference to the debugger server.
136
+ * Starts the server on first call; subsequent calls reuse the existing server.
137
+ * Each call increments a reference count — call releaseDebugger() when done.
138
+ *
139
+ * @param {Object} config - Debugger configuration
140
+ * @param {EventEmitter} emitter - Event emitter for broadcasting
141
+ * @returns {Promise<{port: number, url: string}>} Debugger connection info
142
+ */
143
+ async function acquireDebugger(config = {}, emitter) {
144
+ refCount++;
145
+ if (server && debuggerUrl) {
146
+ // Server already running — reuse it
147
+ return { url: debuggerUrl };
148
+ }
149
+ // First consumer — start the server
150
+ const result = await startDebugger(config, emitter);
151
+ debuggerUrl = result.url;
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Release a reference to the debugger server.
157
+ * Only actually stops the server when the last consumer releases.
158
+ */
159
+ function releaseDebugger() {
160
+ if (refCount > 0) refCount--;
161
+ if (refCount > 0) return; // Other tests still using it
162
+ forceStopDebugger();
163
+ }
164
+
165
+ /**
166
+ * Forcefully stop the debugger server regardless of reference count.
167
+ * Used for process exit cleanup.
168
+ */
169
+ function forceStopDebugger() {
170
+ refCount = 0;
128
171
  if (wss) {
129
172
  wss.close();
130
173
  wss = null;
@@ -136,12 +179,23 @@ function stopDebugger() {
136
179
  }
137
180
 
138
181
  clients.clear();
182
+ debuggerUrl = null;
183
+ module.exports.debuggerUrl = null;
184
+ module.exports.config = null;
139
185
  logger.log("Debugger server stopped");
140
186
  }
141
187
 
188
+ // Keep stopDebugger as alias for forceStopDebugger for backward compatibility
189
+ function stopDebugger() {
190
+ forceStopDebugger();
191
+ }
192
+
142
193
  module.exports = {
143
194
  startDebugger,
144
195
  stopDebugger,
196
+ acquireDebugger,
197
+ releaseDebugger,
198
+ forceStopDebugger,
145
199
  broadcastEvent,
146
200
  createDebuggerServer,
147
201
  debuggerUrl: null,
@@ -47,6 +47,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
47
47
  this.reconnecting = false; // Prevent duplicate reconnection attempts
48
48
  this.pendingTimeouts = new Map(); // Track per-message timeouts
49
49
  this.pendingRetryQueue = []; // Queue of requests to retry after reconnection
50
+ this._lastConnectParams = null; // Connection params for reconnection (per-instance, not shared)
50
51
  }
51
52
 
52
53
  /**
@@ -98,7 +99,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
98
99
  // Add sandboxId to every message if we have a connected sandbox
99
100
  // This allows the API to reconnect if the connection was rerouted
100
101
  // Don't inject IP addresses as sandboxId — only valid instance/sandbox IDs
101
- if (this._lastConnectParams?.sandboxId && !message.sandboxId) {
102
+ if (this._lastConnectParams?.sandboxId && !message.sandboxId) {
102
103
  const id = this._lastConnectParams.sandboxId;
103
104
  // Only inject if it looks like a valid ID (not an IP address)
104
105
  if (id && !/^\d+\.\d+\.\d+\.\d+$/.test(id)) {
@@ -127,11 +128,6 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
127
128
  );
128
129
  }
129
130
  }, timeout);
130
- // Don't let pending message timeouts prevent Node process from exiting
131
- // (unref is not available in browser/non-Node environments)
132
- if (timeoutId.unref) {
133
- timeoutId.unref();
134
- }
135
131
 
136
132
  // Track timeout so close() can clear it
137
133
  this.pendingTimeouts.set(requestId, timeoutId);
@@ -198,6 +194,22 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
198
194
  }
199
195
  }
200
196
 
197
+ /**
198
+ * Set connection params for reconnection logic and sandboxId injection.
199
+ * Use this instead of directly assigning this._lastConnectParams from
200
+ * external code. Keeps the shape consistent and avoids stale state
201
+ * leaking across concurrent test runs.
202
+ * @param {Object|null} params
203
+ * @param {string} [params.type] - 'direct' for IP-based connections
204
+ * @param {string} [params.ip] - IP address for direct connections
205
+ * @param {string} [params.sandboxId] - Sandbox/instance ID
206
+ * @param {boolean} [params.persist] - Whether to persist the sandbox
207
+ * @param {number|null} [params.keepAlive] - Keep-alive TTL in ms
208
+ */
209
+ setConnectionParams(params) {
210
+ this._lastConnectParams = params ? { ...params } : null;
211
+ }
212
+
201
213
  async connect(sandboxId, persist = false, keepAlive = null) {
202
214
  let reply = await this.send({
203
215
  type: "connect",
@@ -209,14 +221,14 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
209
221
  if (reply.success) {
210
222
  // Only store connection params after successful connection
211
223
  // This prevents malformed sandboxId from being attached to subsequent messages
212
- this._lastConnectParams = { sandboxId, persist, keepAlive };
224
+ this.setConnectionParams({ sandboxId, persist, keepAlive });
213
225
  this.instanceSocketConnected = true;
214
226
  emitter.emit(events.sandbox.connected);
215
227
  // Return the full reply (includes url and sandbox)
216
228
  return reply;
217
229
  } else {
218
230
  // Clear any previous connection params on failure
219
- this._lastConnectParams = null;
231
+ this.setConnectionParams(null);
220
232
  // Throw error to trigger fallback to creating new sandbox
221
233
  throw new Error(reply.errorMessage || "Failed to connect to sandbox");
222
234
  }
@@ -348,11 +360,6 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
348
360
  this.reconnecting = false;
349
361
  }
350
362
  }, delay);
351
- // Don't let the reconnect timer prevent Node process from exiting
352
- // (unref is not available in browser/non-Node environments)
353
- if (this.reconnectTimer.unref) {
354
- this.reconnectTimer.unref();
355
- }
356
363
  }
357
364
 
358
365
  /**
@@ -461,12 +468,17 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
461
468
 
462
469
  if (!this.ps[message.requestId]) {
463
470
  // This can happen during reconnection (ps was cleared) or after timeout
464
- // (promise was deleted). Only log at debug level since it's expected.
471
+ // (promise was deleted). Expected during polling loops (e.g. Chrome
472
+ // debugger readiness checks) where short-timeout exec calls regularly
473
+ // expire before the sandbox responds. Only log in debug/verbose mode.
465
474
  if (!this.reconnecting) {
466
- console.warn(
467
- "No pending promise found for requestId:",
468
- message.requestId,
469
- );
475
+ const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
476
+ if (debugMode) {
477
+ console.warn(
478
+ "No pending promise found for requestId:",
479
+ message.requestId,
480
+ );
481
+ }
470
482
  }
471
483
  return;
472
484
  }
@@ -528,6 +540,7 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
528
540
  this.instanceSocketConnected = false;
529
541
  this.authenticated = false;
530
542
  this.instance = null;
543
+ this._lastConnectParams = null;
531
544
 
532
545
  // Silently clear pending promises and retry queue without rejecting
533
546
  // (rejecting causes unhandled promise rejections during cleanup)
@@ -25,7 +25,7 @@ const testdriver = new TestDriver(apiKey, options)
25
25
  Configuration options for the client
26
26
 
27
27
  <Expandable title="properties">
28
- <ParamField path="os" type="string" default="windows">
28
+ <ParamField path="os" type="string" default="linux">
29
29
  Operating system for the sandbox: `'windows'` or `'linux'`
30
30
  </ParamField>
31
31
 
@@ -33,7 +33,7 @@ const testdriver = new TestDriver(apiKey, options)
33
33
  Screen resolution for the sandbox (e.g., `'1920x1080'`, `'1366x768'`)
34
34
  </ParamField>
35
35
 
36
- <ParamField path="apiRoot" type="string" default="https://testdriver-api.onrender.com">
36
+ <ParamField path="apiRoot" type="string">
37
37
  API endpoint URL (typically only changed for self-hosted deployments)
38
38
  </ParamField>
39
39
 
@@ -49,6 +49,111 @@ const testdriver = new TestDriver(apiKey, options)
49
49
  Automatically capture screenshots before and after each command. Screenshots are saved to `.testdriver/screenshots/<test>/` with descriptive filenames that include the line number and action name. Format: `<seq>-<action>-<phase>-L<line>-<description>.png`
50
50
  </ParamField>
51
51
 
52
+ <ParamField path="newSandbox" type="boolean" default="true">
53
+ Force creation of a new sandbox instead of reusing an existing one
54
+ </ParamField>
55
+
56
+ <ParamField path="reconnect" type="boolean" default="false">
57
+ Reconnect to the last used sandbox instead of creating a new one. When `true`, provision methods (`chrome`, `vscode`, `installer`, etc.) will be skipped since the application is already running. Throws error if no previous sandbox exists.
58
+ </ParamField>
59
+
60
+ <ParamField path="keepAlive" type="number" default="60000">
61
+ 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.
62
+ </ParamField>
63
+
64
+ <ParamField path="preview" type="string" default="browser">
65
+ Preview mode for live test visualization:
66
+ - `"browser"` — Opens debugger in default browser (default)
67
+ - `"ide"` — Opens preview in IDE panel (VSCode, Cursor - requires TestDriver extension)
68
+ - `"none"` — Headless mode, no visual preview
69
+ </ParamField>
70
+
71
+ <ParamField path="headless" type="boolean" default="false">
72
+ **Deprecated**: Use `preview: "none"` instead. Run in headless mode without opening the debugger.
73
+ </ParamField>
74
+
75
+ <ParamField path="debugOnFailure" type="boolean" default="false">
76
+ Keep the sandbox alive when a test fails so you can reconnect and debug interactively. The sandbox ID is printed to the console.
77
+ </ParamField>
78
+
79
+ <ParamField path="ip" type="string">
80
+ Direct IP address to connect to a running sandbox instance (for self-hosted deployments)
81
+ </ParamField>
82
+
83
+ <ParamField path="sandboxAmi" type="string">
84
+ Custom AMI ID for the sandbox instance (AWS deployments, e.g., `'ami-1234'`)
85
+ </ParamField>
86
+
87
+ <ParamField path="sandboxInstance" type="string">
88
+ EC2 instance type for the sandbox (AWS deployments, e.g., `'i3.metal'`)
89
+ </ParamField>
90
+
91
+ <ParamField path="cache" type="boolean | object" default="true">
92
+ Enable or disable element caching, or provide advanced threshold configuration.
93
+
94
+ <Expandable title="advanced config">
95
+ <ParamField path="enabled" type="boolean" default="true">
96
+ Enable or disable caching
97
+ </ParamField>
98
+
99
+ <ParamField path="thresholds" type="object">
100
+ Fine-tune cache matching
101
+
102
+ <Expandable title="properties">
103
+ <ParamField path="find" type="object">
104
+ Thresholds for `find()` operations
105
+
106
+ <Expandable title="properties">
107
+ <ParamField path="screen" type="number" default="0.05">
108
+ Pixel diff threshold for screen comparison (0-1). `0.05` = 5% diff allowed.
109
+ </ParamField>
110
+
111
+ <ParamField path="element" type="number" default="0.8">
112
+ OpenCV template match threshold for element matching (0-1). `0.8` = 80% correlation.
113
+ </ParamField>
114
+ </Expandable>
115
+ </ParamField>
116
+
117
+ <ParamField path="assert" type="number" default="0.05">
118
+ Pixel diff threshold for `assert()` operations (0-1). `0.05` = 5% diff allowed.
119
+ </ParamField>
120
+ </Expandable>
121
+ </ParamField>
122
+ </Expandable>
123
+ </ParamField>
124
+
125
+ <ParamField path="cacheKey" type="string">
126
+ Cache key for element finding operations. If provided, enables caching tied to this key.
127
+ </ParamField>
128
+
129
+ <ParamField path="dashcam" type="boolean" default="true">
130
+ Enable or disable Dashcam video recording
131
+ </ParamField>
132
+
133
+ <ParamField path="redraw" type="boolean | object" default="true">
134
+ Enable or disable screen-change (redraw) detection, or provide advanced configuration.
135
+
136
+ <Expandable title="advanced config">
137
+ <ParamField path="enabled" type="boolean" default="true">
138
+ Enable or disable redraw detection
139
+ </ParamField>
140
+
141
+ <ParamField path="thresholds" type="object">
142
+ Threshold configuration
143
+
144
+ <Expandable title="properties">
145
+ <ParamField path="screen" type="number | false" default="0.05">
146
+ Pixel diff threshold (0-1). Set to `false` to disable screen redraw detection.
147
+ </ParamField>
148
+
149
+ <ParamField path="network" type="boolean" default="false">
150
+ Enable or disable network activity monitoring
151
+ </ParamField>
152
+ </Expandable>
153
+ </ParamField>
154
+ </Expandable>
155
+ </ParamField>
156
+
52
157
  <ParamField path="environment" type="object">
53
158
  Additional environment variables to pass to the sandbox
54
159
  </ParamField>