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
package/sdk.js
CHANGED
|
@@ -1399,6 +1399,10 @@ const TestDriverAgent = require("./agent/index.js");
|
|
|
1399
1399
|
const { events } = require("./agent/events.js");
|
|
1400
1400
|
const { createMarkdownLogger } = require("./interfaces/logger.js");
|
|
1401
1401
|
|
|
1402
|
+
// Track screenshot directories already cleaned in this process to avoid
|
|
1403
|
+
// concurrent tests in the same file from nuking each other's screenshots.
|
|
1404
|
+
const _cleanedScreenshotDirs = new Set();
|
|
1405
|
+
|
|
1402
1406
|
class TestDriverSDK {
|
|
1403
1407
|
constructor(apiKey, options = {}) {
|
|
1404
1408
|
// Support calling with just options: new TestDriver({ os: 'windows' })
|
|
@@ -1432,6 +1436,12 @@ class TestDriverSDK {
|
|
|
1432
1436
|
...options.environment,
|
|
1433
1437
|
};
|
|
1434
1438
|
|
|
1439
|
+
// Auto-detect CI environment (GitHub Actions, etc.) and pass through
|
|
1440
|
+
// This ensures the API creates fresh sandboxes instead of reusing hot-pool instances
|
|
1441
|
+
if (!environment.CI && process.env.CI) {
|
|
1442
|
+
environment.CI = process.env.CI;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1435
1445
|
// Create the underlying agent with minimal CLI args
|
|
1436
1446
|
this.agent = new TestDriverAgent(environment, {
|
|
1437
1447
|
command: "sdk",
|
|
@@ -1710,22 +1720,29 @@ class TestDriverSDK {
|
|
|
1710
1720
|
* @param {number} [timeoutMs=60000] - Maximum time to wait in ms
|
|
1711
1721
|
* @returns {Promise<void>}
|
|
1712
1722
|
*/
|
|
1713
|
-
async _waitForChromeDebuggerReady(timeoutMs =
|
|
1723
|
+
async _waitForChromeDebuggerReady(timeoutMs = 60000) {
|
|
1714
1724
|
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
1715
1725
|
const portCheckCmd = this.os === "windows"
|
|
1716
1726
|
? `$tcp = New-Object System.Net.Sockets.TcpClient; $tcp.Connect('127.0.0.1', 9222); $tcp.Close(); echo 'open'`
|
|
1717
1727
|
: `curl -s -o /dev/null --connect-timeout 2 http://localhost:9222 2>/dev/null && echo 'open' || echo 'closed'`;
|
|
1718
|
-
const pageCheckCmd = this.os === "windows"
|
|
1719
|
-
? `(Invoke-RestMethod -Uri 'http://localhost:9222/json' -TimeoutSec 2) | Where-Object { $_.type -eq 'page' } | Select-Object -First 1 | ConvertTo-Json`
|
|
1720
|
-
: `curl -s http://localhost:9222/json 2>/dev/null | grep '"type": "page"'`;
|
|
1721
1728
|
|
|
1722
1729
|
const deadline = Date.now() + timeoutMs;
|
|
1723
1730
|
|
|
1731
|
+
// Use commands.exec directly to bypass auto-screenshots wrapper.
|
|
1732
|
+
// The polling loop fires many rapid exec calls with short timeouts;
|
|
1733
|
+
// going through the wrapper adds 2-3 extra sandbox messages
|
|
1734
|
+
// (screenshot before/after/error) per iteration, overwhelming the
|
|
1735
|
+
// WebSocket and generating cascading "No pending promise" warnings
|
|
1736
|
+
// when timed-out responses arrive after the promise has been cleaned up.
|
|
1737
|
+
const execDirect = this.commands?.exec
|
|
1738
|
+
? (...args) => this.commands.exec(...args)
|
|
1739
|
+
: (...args) => this.exec(...args); // fallback if commands not ready
|
|
1740
|
+
|
|
1724
1741
|
// Wait for port 9222 to be listening
|
|
1725
1742
|
let portReady = false;
|
|
1726
1743
|
while (Date.now() < deadline) {
|
|
1727
1744
|
try {
|
|
1728
|
-
const result = await
|
|
1745
|
+
const result = await execDirect(shell, portCheckCmd, 10000, true);
|
|
1729
1746
|
if (result && result.includes("open")) {
|
|
1730
1747
|
portReady = true;
|
|
1731
1748
|
break;
|
|
@@ -1733,7 +1750,7 @@ class TestDriverSDK {
|
|
|
1733
1750
|
} catch (_) {
|
|
1734
1751
|
// Port not ready yet
|
|
1735
1752
|
}
|
|
1736
|
-
await new Promise((r) => setTimeout(r,
|
|
1753
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1737
1754
|
}
|
|
1738
1755
|
if (!portReady) {
|
|
1739
1756
|
throw new Error(
|
|
@@ -1741,25 +1758,6 @@ class TestDriverSDK {
|
|
|
1741
1758
|
);
|
|
1742
1759
|
}
|
|
1743
1760
|
|
|
1744
|
-
// Wait for a page target to appear via CDP
|
|
1745
|
-
let pageReady = false;
|
|
1746
|
-
while (Date.now() < deadline) {
|
|
1747
|
-
try {
|
|
1748
|
-
const result = await this.exec(shell, pageCheckCmd, 5000, true);
|
|
1749
|
-
if (result && result.trim().length > 0) {
|
|
1750
|
-
pageReady = true;
|
|
1751
|
-
break;
|
|
1752
|
-
}
|
|
1753
|
-
} catch (_) {
|
|
1754
|
-
// No page target yet
|
|
1755
|
-
}
|
|
1756
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1757
|
-
}
|
|
1758
|
-
if (!pageReady) {
|
|
1759
|
-
throw new Error(
|
|
1760
|
-
`Chrome page target did not become available within ${timeoutMs}ms`,
|
|
1761
|
-
);
|
|
1762
|
-
}
|
|
1763
1761
|
}
|
|
1764
1762
|
|
|
1765
1763
|
_createProvisionAPI() {
|
|
@@ -2706,7 +2704,9 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2706
2704
|
);
|
|
2707
2705
|
}
|
|
2708
2706
|
|
|
2709
|
-
// Clean up screenshots folder for this test file before running
|
|
2707
|
+
// Clean up screenshots folder for this test file before running.
|
|
2708
|
+
// Only clean once per directory per process to avoid concurrent tests
|
|
2709
|
+
// in the same file (--sequence.concurrent) from nuking each other's screenshots.
|
|
2710
2710
|
if (this.testFile) {
|
|
2711
2711
|
const testFileName = path.basename(
|
|
2712
2712
|
this.testFile,
|
|
@@ -2718,8 +2718,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2718
2718
|
"screenshots",
|
|
2719
2719
|
testFileName,
|
|
2720
2720
|
);
|
|
2721
|
-
if (
|
|
2722
|
-
|
|
2721
|
+
if (!_cleanedScreenshotDirs.has(screenshotsDir)) {
|
|
2722
|
+
_cleanedScreenshotDirs.add(screenshotsDir);
|
|
2723
|
+
if (fs.existsSync(screenshotsDir)) {
|
|
2724
|
+
fs.rmSync(screenshotsDir, { recursive: true, force: true });
|
|
2725
|
+
}
|
|
2723
2726
|
}
|
|
2724
2727
|
}
|
|
2725
2728
|
|
|
@@ -2746,33 +2749,6 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2746
2749
|
: this.newSandbox,
|
|
2747
2750
|
};
|
|
2748
2751
|
|
|
2749
|
-
// Handle reconnect option - use last sandbox file
|
|
2750
|
-
// Check both connectOptions and constructor options
|
|
2751
|
-
const shouldReconnect =
|
|
2752
|
-
connectOptions.reconnect !== undefined
|
|
2753
|
-
? connectOptions.reconnect
|
|
2754
|
-
: this.reconnect;
|
|
2755
|
-
|
|
2756
|
-
// Skip reconnect if IP is supplied - directly connect to the provided IP
|
|
2757
|
-
const hasIp = Boolean(connectOptions.ip || this.ip);
|
|
2758
|
-
|
|
2759
|
-
if (shouldReconnect && !hasIp) {
|
|
2760
|
-
const lastSandbox = this.agent.getLastSandboxId();
|
|
2761
|
-
if (!lastSandbox || !lastSandbox.sandboxId) {
|
|
2762
|
-
throw new Error(
|
|
2763
|
-
"Cannot reconnect: No previous sandbox found. Run a test first to create a sandbox, or remove the reconnect option.",
|
|
2764
|
-
);
|
|
2765
|
-
}
|
|
2766
|
-
this.agent.sandboxId = lastSandbox.sandboxId;
|
|
2767
|
-
buildEnvOptions.new = false;
|
|
2768
|
-
|
|
2769
|
-
// Use OS from last sandbox if not explicitly specified
|
|
2770
|
-
if (!connectOptions.os && lastSandbox.os) {
|
|
2771
|
-
this.agent.sandboxOs = lastSandbox.os;
|
|
2772
|
-
this.os = lastSandbox.os;
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
2752
|
// Set agent properties for buildEnv to use
|
|
2777
2753
|
if (connectOptions.sandboxId) {
|
|
2778
2754
|
this.agent.sandboxId = connectOptions.sandboxId;
|
|
@@ -2883,10 +2859,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2883
2859
|
}
|
|
2884
2860
|
}
|
|
2885
2861
|
|
|
2886
|
-
//
|
|
2862
|
+
// Release our reference to the shared debugger server.
|
|
2863
|
+
// The server only actually stops when the last concurrent test disconnects.
|
|
2887
2864
|
try {
|
|
2888
|
-
const {
|
|
2889
|
-
|
|
2865
|
+
const { releaseDebugger } = require("./agent/lib/debugger-server.js");
|
|
2866
|
+
releaseDebugger();
|
|
2890
2867
|
} catch (err) {
|
|
2891
2868
|
// Ignore if debugger wasn't started
|
|
2892
2869
|
}
|
|
@@ -2916,14 +2893,6 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2916
2893
|
return this.session?.get() || null;
|
|
2917
2894
|
}
|
|
2918
2895
|
|
|
2919
|
-
/**
|
|
2920
|
-
* Get the last sandbox info from the stored file
|
|
2921
|
-
* @returns {Object|null} Last sandbox info including sandboxId, os, ami, instanceType, timestamp, or null if not found
|
|
2922
|
-
*/
|
|
2923
|
-
getLastSandboxId() {
|
|
2924
|
-
return this.agent.getLastSandboxId();
|
|
2925
|
-
}
|
|
2926
|
-
|
|
2927
2896
|
// ====================================
|
|
2928
2897
|
// Element Finding API
|
|
2929
2898
|
// ====================================
|
|
@@ -3924,16 +3893,13 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3924
3893
|
* @private
|
|
3925
3894
|
*/
|
|
3926
3895
|
async _initializeDebugger() {
|
|
3927
|
-
//
|
|
3928
|
-
|
|
3896
|
+
// Use reference-counted debugger server so concurrent tests share one
|
|
3897
|
+
// server and it only shuts down when the last test disconnects.
|
|
3898
|
+
const { acquireDebugger } = require("./agent/lib/debugger-server.js");
|
|
3929
3899
|
|
|
3930
|
-
// Only initialize once
|
|
3931
3900
|
if (!this.agent.debuggerUrl) {
|
|
3932
|
-
const
|
|
3933
|
-
|
|
3934
|
-
this.emitter,
|
|
3935
|
-
);
|
|
3936
|
-
this.agent.debuggerUrl = debuggerProcess.url || null;
|
|
3901
|
+
const result = await acquireDebugger(this.config, this.emitter);
|
|
3902
|
+
this.agent.debuggerUrl = result.url || null;
|
|
3937
3903
|
}
|
|
3938
3904
|
}
|
|
3939
3905
|
|
package/vitest.config.mjs
CHANGED
|
@@ -5,17 +5,17 @@ import { defineConfig } from "vitest/config";
|
|
|
5
5
|
// Note: dotenv is loaded automatically by the TestDriver SDK
|
|
6
6
|
const setupFiles = [
|
|
7
7
|
"testdriverai/vitest/setup",
|
|
8
|
-
"testdriverai/vitest/setup-aws"
|
|
9
|
-
'testdriverai/vitest/setup-disable-defender'
|
|
8
|
+
"testdriverai/vitest/setup-aws"
|
|
10
9
|
];
|
|
11
10
|
|
|
12
11
|
export default defineConfig({
|
|
13
12
|
test: {
|
|
14
|
-
retry:
|
|
13
|
+
retry: 0,
|
|
15
14
|
testTimeout: 900000,
|
|
16
15
|
hookTimeout: 900000,
|
|
17
16
|
disableConsoleIntercept: true,
|
|
18
17
|
maxConcurrency: 100,
|
|
18
|
+
maxWorkers: 16,
|
|
19
19
|
reporters: [
|
|
20
20
|
"default",
|
|
21
21
|
TestDriver(),
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post-spawn hook to disable Windows Defender
|
|
3
|
-
*
|
|
4
|
-
* Usage in vitest.config.mjs:
|
|
5
|
-
* ```js
|
|
6
|
-
* setupFiles: [
|
|
7
|
-
* 'testdriverai/vitest/setup',
|
|
8
|
-
* 'testdriverai/vitest/setup-aws',
|
|
9
|
-
* 'testdriverai/vitest/setup-disable-defender'
|
|
10
|
-
* ]
|
|
11
|
-
* ```
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { execSync } from 'child_process';
|
|
15
|
-
import { dirname, join } from 'path';
|
|
16
|
-
import { fileURLToPath } from 'url';
|
|
17
|
-
import { beforeEach } from 'vitest';
|
|
18
|
-
|
|
19
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
-
const __dirname = dirname(__filename);
|
|
21
|
-
|
|
22
|
-
beforeEach(async (context) => {
|
|
23
|
-
// Only run if we have an instance IP (self-hosted mode)
|
|
24
|
-
if (!context.ip) return;
|
|
25
|
-
|
|
26
|
-
// Get instance ID from global state set by setup-aws
|
|
27
|
-
const instanceInfo = globalThis.__testdriverAWS?.instances?.get(context.task.id);
|
|
28
|
-
if (!instanceInfo?.instanceId) {
|
|
29
|
-
console.warn('[TestDriver] No instance ID found, skipping Defender disable');
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const { instanceId, awsRegion } = instanceInfo;
|
|
34
|
-
const scriptPath = join(__dirname, '../../setup/aws/disable-defender.sh');
|
|
35
|
-
|
|
36
|
-
console.log(`[TestDriver] Disabling Windows Defender on ${instanceId}...`);
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
execSync(`bash ${scriptPath}`, {
|
|
40
|
-
encoding: 'utf-8',
|
|
41
|
-
env: {
|
|
42
|
-
...process.env,
|
|
43
|
-
AWS_REGION: awsRegion,
|
|
44
|
-
INSTANCE_ID: instanceId,
|
|
45
|
-
},
|
|
46
|
-
stdio: 'inherit',
|
|
47
|
-
});
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.warn('[TestDriver] Failed to disable Defender:', error.message);
|
|
50
|
-
// Don't throw - this is optional optimization
|
|
51
|
-
}
|
|
52
|
-
});
|