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.
- package/.github/copilot-instructions.md +58 -9
- package/.github/workflows/windows-self-hosted.yaml +3 -14
- package/CHANGELOG.md +13 -0
- package/agent/index.js +3 -142
- package/agent/lib/debugger-server.js +56 -2
- package/agent/lib/sandbox.js +31 -18
- package/docs/v7/client.mdx +107 -2
- package/docs/v7/customizing-devices.mdx +170 -4
- package/examples/chrome-extension.test.mjs +27 -27
- package/interfaces/vitest-plugin.mjs +4 -1
- package/lib/sentry.js +32 -5
- package/lib/vitest/hooks.mjs +4 -35
- package/lib/vitest/setup-aws.mjs +11 -3
- package/lib/vitest/setup-self-hosted.mjs +15 -0
- package/package.json +1 -2
- package/sdk.js +40 -74
- package/vitest.config.mjs +3 -3
- package/lib/vitest/setup-disable-defender.mjs +0 -52
|
@@ -507,15 +507,64 @@ it("should incrementally build test", async (context) => {
|
|
|
507
507
|
|
|
508
508
|
```javascript
|
|
509
509
|
const testdriver = TestDriver(context, {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
reconnect: false,
|
|
513
|
-
keepAlive: 30000,
|
|
514
|
-
os: "linux",
|
|
515
|
-
resolution: "1366x768",
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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,
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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).
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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)
|
package/docs/v7/client.mdx
CHANGED
|
@@ -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="
|
|
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"
|
|
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>
|