testdriverai 7.5.25 → 7.6.0-test.0
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/CHANGELOG.md +27 -0
- package/README.md +1 -0
- package/agent/index.js +3 -3
- package/agent/lib/config.js +3 -1
- package/agent/lib/sandbox.js +6 -4
- package/ai/agents/testdriver.md +0 -3
- package/ai/skills/testdriver-aws-setup/SKILL.md +1 -1
- package/ai/skills/testdriver-exec/SKILL.md +23 -40
- package/ai/skills/testdriver-test-writer/SKILL.md +0 -3
- package/ai/skills/testdriver-testdriver/SKILL.md +0 -3
- package/ai/skills/testdriver-wait/SKILL.md +50 -0
- package/ai/skills/testdriver-waiting-for-elements/SKILL.md +3 -1
- package/channel.json +9 -0
- package/debugger/index.html +20 -3
- package/docs/docs.json +1 -1
- package/docs/v6/commands/exec.mdx +15 -21
- package/docs/v7/_drafts/agents.mdx +4 -13
- package/docs/v7/_drafts/commands/exec.mdx +15 -21
- package/docs/v7/aws-setup.mdx +1 -1
- package/docs/v7/exec.mdx +36 -64
- package/docs/v7/quickstart.mdx +1 -1
- package/examples/config.mjs +1 -1
- package/examples/no-provision.test.mjs +18 -11
- package/interfaces/cli/commands/init.js +2 -1
- package/interfaces/vitest-plugin.mjs +23 -2
- package/lib/core/Dashcam.js +23 -2
- package/lib/init-project.js +67 -27
- package/lib/vitest/hooks.mjs +2 -1
- package/mcp-server/README.md +12 -2
- package/mcp-server/dist/codegen.js +1 -1
- package/mcp-server/dist/server.mjs +50 -2
- package/mcp-server/src/codegen.ts +1 -1
- package/mcp-server/src/server.ts +44 -2
- package/package.json +6 -2
- package/sdk.d.ts +2 -2
- package/sdk.js +49 -2
- package/vitest.config.mjs +56 -12
package/mcp-server/src/server.ts
CHANGED
|
@@ -867,6 +867,7 @@ registerAppTool(
|
|
|
867
867
|
|
|
868
868
|
// Store cropped image for resource serving (instead of inline data URL)
|
|
869
869
|
let croppedImageResourceUri: string | undefined;
|
|
870
|
+
let screenshotResourceUri: string | undefined;
|
|
870
871
|
const croppedImage = rawResponse.croppedImage;
|
|
871
872
|
if (croppedImage) {
|
|
872
873
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -875,6 +876,18 @@ registerAppTool(
|
|
|
875
876
|
croppedImageResourceUri = storeImage(imageData, "cropped");
|
|
876
877
|
// Remove croppedImage from response to avoid context bloat
|
|
877
878
|
delete rawResponse.croppedImage;
|
|
879
|
+
} else if (!found) {
|
|
880
|
+
// Element not found and no cropped image - capture a fresh screenshot
|
|
881
|
+
// so the user can see what's currently visible on screen
|
|
882
|
+
try {
|
|
883
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
884
|
+
if (screenshotBase64) {
|
|
885
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
886
|
+
logger.debug("find: Captured screenshot for not-found state");
|
|
887
|
+
}
|
|
888
|
+
} catch (e) {
|
|
889
|
+
logger.warn("find: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
890
|
+
}
|
|
878
891
|
}
|
|
879
892
|
|
|
880
893
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
@@ -904,6 +917,7 @@ registerAppTool(
|
|
|
904
917
|
element: elementInfo,
|
|
905
918
|
ref: elementRef,
|
|
906
919
|
croppedImageResourceUri,
|
|
920
|
+
screenshotResourceUri,
|
|
907
921
|
duration,
|
|
908
922
|
},
|
|
909
923
|
generatedCode
|
|
@@ -988,6 +1002,7 @@ registerAppTool(
|
|
|
988
1002
|
|
|
989
1003
|
// Store cropped image for resource serving (instead of inline data URL)
|
|
990
1004
|
let croppedImageResourceUri: string | undefined;
|
|
1005
|
+
let screenshotResourceUri: string | undefined;
|
|
991
1006
|
const croppedImage = rawResponse.croppedImage;
|
|
992
1007
|
if (croppedImage) {
|
|
993
1008
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -996,6 +1011,18 @@ registerAppTool(
|
|
|
996
1011
|
croppedImageResourceUri = storeImage(imageData, "cropped");
|
|
997
1012
|
// Remove croppedImage from response to avoid context bloat
|
|
998
1013
|
delete rawResponse.croppedImage;
|
|
1014
|
+
} else if (count === 0) {
|
|
1015
|
+
// No elements found and no cropped image - capture a fresh screenshot
|
|
1016
|
+
// so the user can see what's currently visible on screen
|
|
1017
|
+
try {
|
|
1018
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
1019
|
+
if (screenshotBase64) {
|
|
1020
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
1021
|
+
logger.debug("findall: Captured screenshot for not-found state");
|
|
1022
|
+
}
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
logger.warn("findall: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
1025
|
+
}
|
|
999
1026
|
}
|
|
1000
1027
|
|
|
1001
1028
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
@@ -1019,6 +1046,7 @@ registerAppTool(
|
|
|
1019
1046
|
refs,
|
|
1020
1047
|
elements: elementInfos,
|
|
1021
1048
|
croppedImageResourceUri,
|
|
1049
|
+
screenshotResourceUri,
|
|
1022
1050
|
duration,
|
|
1023
1051
|
},
|
|
1024
1052
|
generatedCode
|
|
@@ -1317,6 +1345,7 @@ registerAppTool(
|
|
|
1317
1345
|
|
|
1318
1346
|
// Store cropped image (screenshot) for resource serving
|
|
1319
1347
|
let croppedImageResourceUri: string | undefined;
|
|
1348
|
+
let screenshotResourceUri: string | undefined;
|
|
1320
1349
|
const croppedImage = rawResponse.croppedImage;
|
|
1321
1350
|
if (croppedImage) {
|
|
1322
1351
|
const imageData = croppedImage.startsWith('data:')
|
|
@@ -1324,6 +1353,18 @@ registerAppTool(
|
|
|
1324
1353
|
: croppedImage;
|
|
1325
1354
|
croppedImageResourceUri = storeImage(imageData, "screenshot");
|
|
1326
1355
|
delete rawResponse.croppedImage;
|
|
1356
|
+
} else {
|
|
1357
|
+
// No cropped image - capture a fresh screenshot so the user can see
|
|
1358
|
+
// what's currently visible on screen when element was not found
|
|
1359
|
+
try {
|
|
1360
|
+
const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
|
|
1361
|
+
if (screenshotBase64) {
|
|
1362
|
+
screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
|
|
1363
|
+
logger.debug("find_and_click: Captured screenshot for not-found state");
|
|
1364
|
+
}
|
|
1365
|
+
} catch (e) {
|
|
1366
|
+
logger.warn("find_and_click: Failed to capture screenshot for not-found state", { error: String(e) });
|
|
1367
|
+
}
|
|
1327
1368
|
}
|
|
1328
1369
|
|
|
1329
1370
|
// Remove extractedText and pixelDiffImage from response to reduce context bloat
|
|
@@ -1338,6 +1379,7 @@ registerAppTool(
|
|
|
1338
1379
|
action: "find_and_click",
|
|
1339
1380
|
error: "Element not found",
|
|
1340
1381
|
croppedImageResourceUri,
|
|
1382
|
+
screenshotResourceUri,
|
|
1341
1383
|
duration
|
|
1342
1384
|
}
|
|
1343
1385
|
);
|
|
@@ -1760,9 +1802,9 @@ You can optionally provide a reference image URI to compare against a previous s
|
|
|
1760
1802
|
server.registerTool(
|
|
1761
1803
|
"exec",
|
|
1762
1804
|
{
|
|
1763
|
-
description: "Execute
|
|
1805
|
+
description: "Execute shell or PowerShell commands in the sandbox",
|
|
1764
1806
|
inputSchema: z.object({
|
|
1765
|
-
language: z.enum(["
|
|
1807
|
+
language: z.enum(["sh", "pwsh"]).default("sh"),
|
|
1766
1808
|
code: z.string().describe("Code to execute"),
|
|
1767
1809
|
timeout: z.number().default(30000).describe("Timeout in ms"),
|
|
1768
1810
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0-test.0",
|
|
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",
|
|
@@ -47,7 +47,11 @@
|
|
|
47
47
|
"bundle": "node build.mjs",
|
|
48
48
|
"test": "mocha test/*",
|
|
49
49
|
"test:sdk": "vitest run",
|
|
50
|
-
"test:sdk:
|
|
50
|
+
"test:sdk:dev": "vitest run --project dev",
|
|
51
|
+
"test:sdk:staging": "vitest run --project staging",
|
|
52
|
+
"test:sdk:canary": "vitest run --project canary",
|
|
53
|
+
"test:sdk:stable": "vitest run --project stable",
|
|
54
|
+
"test:sdk:windows": "TD_OS=windows vitest run",
|
|
51
55
|
"test:sdk:mac": "TEST_PLATFORM=mac vitest run",
|
|
52
56
|
"test:sdk:linux": "TEST_PLATFORM=linux vitest run",
|
|
53
57
|
"test:sdk:watch": "vitest",
|
package/sdk.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type ClickAction =
|
|
|
13
13
|
export type ScrollDirection = "up" | "down" | "left" | "right";
|
|
14
14
|
export type ScrollMethod = "keyboard" | "mouse";
|
|
15
15
|
export type TextMatchMethod = "ai" | "turbo";
|
|
16
|
-
export type ExecLanguage = "
|
|
16
|
+
export type ExecLanguage = "sh" | "pwsh";
|
|
17
17
|
/**
|
|
18
18
|
* Preview mode for live test visualization
|
|
19
19
|
* - "browser": Opens debugger in default browser (default)
|
|
@@ -218,7 +218,7 @@ export type KeyboardKey =
|
|
|
218
218
|
| "optionright";
|
|
219
219
|
|
|
220
220
|
export interface TestDriverOptions {
|
|
221
|
-
/** API endpoint URL (default: 'https://
|
|
221
|
+
/** API endpoint URL (default depends on release channel: latest → 'https://api.testdriver.ai') */
|
|
222
222
|
apiRoot?: string;
|
|
223
223
|
/** Sandbox resolution (default: '1366x768') */
|
|
224
224
|
resolution?: string;
|
package/sdk.js
CHANGED
|
@@ -1402,7 +1402,7 @@ function normalizeRedrawOptions(opts) {
|
|
|
1402
1402
|
* @typedef {'up' | 'down' | 'left' | 'right'} ScrollDirection
|
|
1403
1403
|
* @typedef {'keyboard' | 'mouse'} ScrollMethod
|
|
1404
1404
|
* @typedef {'ai' | 'turbo'} TextMatchMethod
|
|
1405
|
-
* @typedef {'
|
|
1405
|
+
* @typedef {'sh' | 'pwsh'} ExecLanguage
|
|
1406
1406
|
* @typedef {'\\t' | '\n' | '\r' | ' ' | '!' | '"' | '#' | '$' | '%' | '&' | "'" | '(' | ')' | '*' | '+' | ',' | '-' | '.' | '/' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | ':' | ';' | '<' | '=' | '>' | '?' | '@' | '[' | '\\' | ']' | '^' | '_' | '`' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | '{' | '|' | '}' | '~' | 'accept' | 'add' | 'alt' | 'altleft' | 'altright' | 'apps' | 'backspace' | 'browserback' | 'browserfavorites' | 'browserforward' | 'browserhome' | 'browserrefresh' | 'browsersearch' | 'browserstop' | 'capslock' | 'clear' | 'convert' | 'ctrl' | 'ctrlleft' | 'ctrlright' | 'decimal' | 'del' | 'delete' | 'divide' | 'down' | 'end' | 'enter' | 'esc' | 'escape' | 'execute' | 'f1' | 'f10' | 'f11' | 'f12' | 'f13' | 'f14' | 'f15' | 'f16' | 'f17' | 'f18' | 'f19' | 'f2' | 'f20' | 'f21' | 'f22' | 'f23' | 'f24' | 'f3' | 'f4' | 'f5' | 'f6' | 'f7' | 'f8' | 'f9' | 'final' | 'fn' | 'hanguel' | 'hangul' | 'hanja' | 'help' | 'home' | 'insert' | 'junja' | 'kana' | 'kanji' | 'launchapp1' | 'launchapp2' | 'launchmail' | 'launchmediaselect' | 'left' | 'modechange' | 'multiply' | 'nexttrack' | 'nonconvert' | 'num0' | 'num1' | 'num2' | 'num3' | 'num4' | 'num5' | 'num6' | 'num7' | 'num8' | 'num9' | 'numlock' | 'pagedown' | 'pageup' | 'pause' | 'pgdn' | 'pgup' | 'playpause' | 'prevtrack' | 'print' | 'printscreen' | 'prntscrn' | 'prtsc' | 'prtscr' | 'return' | 'right' | 'scrolllock' | 'select' | 'separator' | 'shift' | 'shiftleft' | 'shiftright' | 'sleep' | 'space' | 'stop' | 'subtract' | 'tab' | 'up' | 'volumedown' | 'volumemute' | 'volumeup' | 'win' | 'winleft' | 'winright' | 'yen' | 'command' | 'option' | 'optionleft' | 'optionright'} KeyboardKey
|
|
1407
1407
|
*/
|
|
1408
1408
|
|
|
@@ -1871,6 +1871,8 @@ class TestDriverSDK {
|
|
|
1871
1871
|
"--no-first-run",
|
|
1872
1872
|
"--no-experiments",
|
|
1873
1873
|
"--disable-infobars",
|
|
1874
|
+
"--disable-features=StartupBrowserCreator",
|
|
1875
|
+
"--disable-features=ChromeWhatsNewUI",
|
|
1874
1876
|
`--user-data-dir=${userDataDir}`,
|
|
1875
1877
|
);
|
|
1876
1878
|
|
|
@@ -2862,6 +2864,9 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2862
2864
|
sandboxId: this.instance?.instanceId,
|
|
2863
2865
|
});
|
|
2864
2866
|
|
|
2867
|
+
// Log environment info (non-blocking, skip on stable)
|
|
2868
|
+
this._logEnvironmentInfo();
|
|
2869
|
+
|
|
2865
2870
|
return this.instance;
|
|
2866
2871
|
}
|
|
2867
2872
|
|
|
@@ -2890,8 +2895,10 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2890
2895
|
|
|
2891
2896
|
// Always close the sandbox WebSocket connection to clean up resources
|
|
2892
2897
|
// This ensures we don't leave orphaned connections even if connect() failed
|
|
2898
|
+
// Must be awaited so presence.leave() completes before we return —
|
|
2899
|
+
// otherwise the concurrency counter on the API stays stale.
|
|
2893
2900
|
if (this.sandbox && typeof this.sandbox.close === "function") {
|
|
2894
|
-
this.sandbox.close();
|
|
2901
|
+
await this.sandbox.close();
|
|
2895
2902
|
}
|
|
2896
2903
|
|
|
2897
2904
|
// Remove all event listeners on the emitter to release references
|
|
@@ -3799,6 +3806,46 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3799
3806
|
* Set up logging for the SDK
|
|
3800
3807
|
* @private
|
|
3801
3808
|
*/
|
|
3809
|
+
/**
|
|
3810
|
+
* Log environment info (version, API URL, git commit) after connect.
|
|
3811
|
+
* Fires asynchronously so it never blocks the test.
|
|
3812
|
+
* Suppressed when the API reports the "stable" channel.
|
|
3813
|
+
* @private
|
|
3814
|
+
*/
|
|
3815
|
+
_logEnvironmentInfo() {
|
|
3816
|
+
const apiRoot = this.config?.TD_API_ROOT || 'unknown';
|
|
3817
|
+
const sdkVersion = require('./package.json').version;
|
|
3818
|
+
const http = apiRoot.startsWith('https') ? require('https') : require('http');
|
|
3819
|
+
|
|
3820
|
+
const url = apiRoot + '/api/entrance/version';
|
|
3821
|
+
const req = http.get(url, { timeout: 5000 }, (res) => {
|
|
3822
|
+
let data = '';
|
|
3823
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
3824
|
+
res.on('end', () => {
|
|
3825
|
+
try {
|
|
3826
|
+
const info = JSON.parse(data);
|
|
3827
|
+
if (info.channel === 'stable') return; // don't show on stable
|
|
3828
|
+
const commit = info.commit || 'unknown';
|
|
3829
|
+
const shortCommit = commit.substring(0, 7);
|
|
3830
|
+
const commitUrl = commit !== 'unknown'
|
|
3831
|
+
? `https://github.com/testdriverai/mono/commit/${commit}`
|
|
3832
|
+
: null;
|
|
3833
|
+
const lines = [
|
|
3834
|
+
'',
|
|
3835
|
+
` TestDriver SDK v${sdkVersion}`,
|
|
3836
|
+
` API: ${apiRoot} (${info.channel || 'unknown'} v${info.version || '?'})`,
|
|
3837
|
+
commitUrl
|
|
3838
|
+
? ` Commit: ${shortCommit} → ${commitUrl}`
|
|
3839
|
+
: ` Commit: ${shortCommit}`,
|
|
3840
|
+
'',
|
|
3841
|
+
];
|
|
3842
|
+
console.log(lines.join('\n'));
|
|
3843
|
+
} catch (_) { /* ignore parse errors */ }
|
|
3844
|
+
});
|
|
3845
|
+
});
|
|
3846
|
+
req.on('error', () => { /* ignore network errors */ });
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3802
3849
|
_setupLogging() {
|
|
3803
3850
|
// Track the last fatal error message to throw on exit
|
|
3804
3851
|
let lastFatalError = null;
|
package/vitest.config.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import TestDriver from "testdriverai/vitest";
|
|
2
2
|
import { defineConfig } from "vitest/config";
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { resolve } from "path";
|
|
3
5
|
|
|
4
6
|
// Always include AWS setup - it will be a no-op unless TD_OS=windows
|
|
5
7
|
// Note: dotenv is loaded automatically by the TestDriver SDK
|
|
@@ -8,19 +10,61 @@ const setupFiles = [
|
|
|
8
10
|
"testdriverai/vitest/setup-aws"
|
|
9
11
|
];
|
|
10
12
|
|
|
13
|
+
const sharedTestConfig = {
|
|
14
|
+
retry: 0,
|
|
15
|
+
testTimeout: 900000,
|
|
16
|
+
hookTimeout: 900000,
|
|
17
|
+
maxConcurrency: 100,
|
|
18
|
+
disableConsoleIntercept: false,
|
|
19
|
+
silent: false,
|
|
20
|
+
reporters: [
|
|
21
|
+
"verbose",
|
|
22
|
+
TestDriver()
|
|
23
|
+
],
|
|
24
|
+
setupFiles,
|
|
25
|
+
include: ["examples/**/*.test.mjs"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ── Parse a simple KEY=VALUE .env file ──────────────────────────────
|
|
29
|
+
function parseEnvFile(filePath) {
|
|
30
|
+
if (!existsSync(filePath)) return {};
|
|
31
|
+
const env = {};
|
|
32
|
+
for (const line of readFileSync(filePath, "utf-8").split("\n")) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
35
|
+
const idx = trimmed.indexOf("=");
|
|
36
|
+
if (idx === -1) continue;
|
|
37
|
+
env[trimmed.slice(0, idx)] = trimmed.slice(idx + 1);
|
|
38
|
+
}
|
|
39
|
+
return env;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Load base .env + per-environment overlay ────────────────────────
|
|
43
|
+
const monoRoot = resolve(import.meta.dirname, "..");
|
|
44
|
+
const baseEnv = parseEnvFile(resolve(monoRoot, ".env"));
|
|
45
|
+
|
|
46
|
+
const environments = ["dev", "test", "canary", "stable"];
|
|
47
|
+
|
|
48
|
+
function envForProject(envName) {
|
|
49
|
+
const overlay = parseEnvFile(resolve(monoRoot, "envs", `${envName}.env`));
|
|
50
|
+
return { ...baseEnv, ...overlay };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── If TD_ENV is set (e.g. from CLI), only run that environment ─────
|
|
54
|
+
// Usage: TD_ENV=dev vitest run
|
|
55
|
+
// TD_ENV=canary vitest run examples/assert.test.mjs
|
|
56
|
+
// vitest run --project dev
|
|
57
|
+
// vitest run --project canary --project stable
|
|
11
58
|
export default defineConfig({
|
|
12
59
|
test: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
TestDriver()
|
|
23
|
-
],
|
|
24
|
-
setupFiles,
|
|
60
|
+
...sharedTestConfig,
|
|
61
|
+
env: envForProject(process.env.TD_ENV || "dev"),
|
|
62
|
+
projects: environments.map((envName) => ({
|
|
63
|
+
extends: true,
|
|
64
|
+
test: {
|
|
65
|
+
name: envName,
|
|
66
|
+
env: envForProject(envName),
|
|
67
|
+
},
|
|
68
|
+
})),
|
|
25
69
|
},
|
|
26
70
|
});
|