testdriverai 7.5.26 → 7.6.0-test.1

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.
@@ -9,6 +9,7 @@ import { setTestRunInfo } from "./shared-test-state.mjs";
9
9
 
10
10
  // Use createRequire to import CommonJS modules without esbuild processing
11
11
  const require = createRequire(import.meta.url);
12
+ const channelConfig = require("../channel.json");
12
13
 
13
14
  // Import Sentry for error reporting
14
15
  const Sentry = require("@sentry/node");
@@ -763,7 +764,7 @@ export default function testDriverPlugin(options = {}) {
763
764
  pluginState.apiRoot =
764
765
  options.apiRoot ||
765
766
  process.env.TD_API_ROOT ||
766
- "https://api.testdriver.ai";
767
+ channelConfig.channels[channelConfig.active];
767
768
  pluginState.ciProvider = detectCI();
768
769
  pluginState.gitInfo = getGitInfo();
769
770
 
@@ -822,7 +823,7 @@ class TestDriverReporter {
822
823
  pluginState.apiRoot =
823
824
  this.options.apiRoot ||
824
825
  process.env.TD_API_ROOT ||
825
- "https://api.testdriver.ai";
826
+ channelConfig.channels[channelConfig.active];
826
827
  logger.debug("API key from options:", !!this.options.apiKey);
827
828
  logger.debug("API key from env (at onInit):", !!process.env.TD_API_KEY);
828
829
  logger.debug("API root from options:", this.options.apiRoot);
@@ -1258,6 +1259,26 @@ function getConsoleUrl(apiRoot) {
1258
1259
  return `http://localhost:3001`;
1259
1260
  }
1260
1261
 
1262
+ // Render PR previews: map API service to Web service
1263
+ // canary-api-pr-123.onrender.com -> canary-web-pr-123.onrender.com
1264
+ // testdriver-api-i4m4-pr-123.onrender.com -> web-i4m4-pr-123.onrender.com
1265
+ const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
1266
+ if (renderPrMatch) {
1267
+ const [, prefix, suffix, prSuffix] = renderPrMatch;
1268
+ // Map API naming to Web naming:
1269
+ // canary-api -> canary-web
1270
+ // testdriver-api-i4m4 -> web-i4m4
1271
+ let webPrefix;
1272
+ if (prefix === 'testdriver' && suffix) {
1273
+ // testdriver-api-i4m4 -> web-i4m4
1274
+ webPrefix = 'web' + suffix;
1275
+ } else {
1276
+ // canary-api -> canary-web
1277
+ webPrefix = prefix + '-web';
1278
+ }
1279
+ return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
1280
+ }
1281
+
1261
1282
  // Other tunnels or unknown hosts: return as-is
1262
1283
  return apiRoot;
1263
1284
  }
@@ -80,8 +80,9 @@ class Dashcam {
80
80
  * @private
81
81
  */
82
82
  _getApiRoot() {
83
+ const channelConfig = require("../../channel.json");
83
84
  return (
84
- this.client.config?.TD_API_ROOT || "https://api.testdriver.ai"
85
+ this.client.config?.TD_API_ROOT || channelConfig.channels[channelConfig.active]
85
86
  );
86
87
  }
87
88
 
@@ -91,7 +92,7 @@ class Dashcam {
91
92
  * @param {string} apiRoot - The API root URL
92
93
  * @returns {string} The corresponding console URL
93
94
  */
94
- static getConsoleUrl(apiRoot = "https://api.testdriver.ai") {
95
+ static getConsoleUrl(apiRoot = (() => { const c = require("../../channel.json"); return c.channels[c.active]; })()) {
95
96
  // Allow explicit override via env (e.g. VITE_DOMAIN from .env)
96
97
  if (process.env.VITE_DOMAIN) return process.env.VITE_DOMAIN;
97
98
 
@@ -110,6 +111,26 @@ class Dashcam {
110
111
  return "http://localhost:3001";
111
112
  }
112
113
 
114
+ // Render PR previews: map API service to Web service
115
+ // canary-api-pr-123.onrender.com -> canary-web-pr-123.onrender.com
116
+ // testdriver-api-i4m4-pr-123.onrender.com -> web-i4m4-pr-123.onrender.com
117
+ const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
118
+ if (renderPrMatch) {
119
+ const [, prefix, suffix, prSuffix] = renderPrMatch;
120
+ // Map API naming to Web naming:
121
+ // canary-api -> canary-web
122
+ // testdriver-api-i4m4 -> web-i4m4
123
+ let webPrefix;
124
+ if (prefix === 'testdriver' && suffix) {
125
+ // testdriver-api-i4m4 -> web-i4m4
126
+ webPrefix = 'web' + suffix;
127
+ } else {
128
+ // canary-api -> canary-web
129
+ webPrefix = prefix + '-web';
130
+ }
131
+ return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
132
+ }
133
+
113
134
  // Cloudflare tunnels, custom domains, etc.: the web console is served
114
135
  // from the same origin as the API, so return apiRoot as-is.
115
136
  return apiRoot;
@@ -337,40 +337,80 @@ jobs:
337
337
  progress("⊘ GitHub workflow already exists");
338
338
  }
339
339
 
340
- // 6. Create VSCode MCP config
341
- const vscodeDir = path.join(targetDir, ".vscode");
342
- if (!fs.existsSync(vscodeDir)) {
343
- fs.mkdirSync(vscodeDir, { recursive: true });
344
- }
340
+ // 6. Setup MCP configuration
341
+ // When triggered from VS Code extension, create .vscode/mcp.json silently
342
+ // When triggered from CLI, use interactive add-mcp for user to select their MCP client
343
+ const isVscodeInit = process.env.TD_INIT_SOURCE === "vscode";
344
+
345
+ if (isVscodeInit) {
346
+ // VS Code extension: create .vscode/mcp.json directly
347
+ const vscodeDir = path.join(targetDir, ".vscode");
348
+ if (!fs.existsSync(vscodeDir)) {
349
+ fs.mkdirSync(vscodeDir, { recursive: true });
350
+ }
345
351
 
346
- const mcpConfigFile = path.join(vscodeDir, "mcp.json");
347
- if (!fs.existsSync(mcpConfigFile)) {
348
- const mcpConfig = {
349
- inputs: [
350
- {
351
- type: "promptString",
352
- id: "testdriver-api-key",
353
- description: "TestDriver API Key From https://console.testdriver.ai/team",
354
- password: true,
355
- },
356
- ],
357
- servers: {
358
- testdriver: {
359
- command: "npx",
360
- args: ["-p", "testdriverai", "testdriverai-mcp"],
361
- env: {
362
- TD_API_KEY: "${input:testdriver-api-key}",
352
+ const mcpConfigFile = path.join(vscodeDir, "mcp.json");
353
+ if (!fs.existsSync(mcpConfigFile)) {
354
+ const mcpConfig = {
355
+ inputs: [
356
+ {
357
+ type: "promptString",
358
+ id: "testdriver-api-key",
359
+ description: "TestDriver API Key From https://console.testdriver.ai/team",
360
+ password: true,
361
+ },
362
+ ],
363
+ servers: {
364
+ testdriver: {
365
+ command: "npx",
366
+ args: ["-p", "testdriverai", "testdriverai-mcp"],
367
+ env: {
368
+ TD_API_KEY: "${input:testdriver-api-key}",
369
+ },
363
370
  },
364
371
  },
365
- },
366
- };
367
- fs.writeFileSync(mcpConfigFile, JSON.stringify(mcpConfig, null, 2) + "\n");
368
- progress("✓ Created MCP config: .vscode/mcp.json");
372
+ };
373
+ fs.writeFileSync(mcpConfigFile, JSON.stringify(mcpConfig, null, 2) + "\n");
374
+ progress("✓ Created MCP config: .vscode/mcp.json");
375
+ } else {
376
+ progress("⊘ MCP config already exists");
377
+ }
369
378
  } else {
370
- progress("⊘ MCP config already exists");
379
+ // CLI: use add-mcp for interactive MCP client selection
380
+ progress("🔧 Setting up MCP integration...");
381
+ try {
382
+ const addMcpResult = require("child_process").spawnSync(
383
+ "npx",
384
+ [
385
+ "add-mcp",
386
+ "testdriver",
387
+ "--command",
388
+ "npx -p testdriverai testdriverai-mcp",
389
+ "--env",
390
+ "TD_API_KEY",
391
+ ],
392
+ {
393
+ cwd: targetDir,
394
+ stdio: "inherit", // Pass through stdin/stdout for interactive prompts
395
+ shell: process.platform === "win32",
396
+ }
397
+ );
398
+
399
+ if (addMcpResult.status === 0) {
400
+ progress("✓ MCP configured via add-mcp");
401
+ } else if (addMcpResult.status !== null) {
402
+ progress("⚠ MCP setup skipped or failed - you can run 'npx add-mcp testdriver' later");
403
+ }
404
+ } catch (err) {
405
+ progress("⚠ Could not run add-mcp - you can run 'npx add-mcp testdriver' later");
406
+ }
371
407
  }
372
408
 
373
409
  // 7. Create VSCode extensions recommendations
410
+ const vscodeDir = path.join(targetDir, ".vscode");
411
+ if (!fs.existsSync(vscodeDir)) {
412
+ fs.mkdirSync(vscodeDir, { recursive: true });
413
+ }
374
414
  const extensionsFile = path.join(vscodeDir, "extensions.json");
375
415
  if (!fs.existsSync(extensionsFile)) {
376
416
  const extensionsConfig = {
@@ -22,6 +22,7 @@ import TestDriverSDK from "../../sdk.js";
22
22
 
23
23
  // Use createRequire to import CommonJS modules
24
24
  const require = createRequire(import.meta.url);
25
+ const channelConfig = require("../../channel.json");
25
26
 
26
27
  /**
27
28
  * Minimum required Vitest major version
@@ -255,7 +256,7 @@ async function uploadLogsToReplay(client, dashcamUrl) {
255
256
  }
256
257
 
257
258
  // Use the SDK's configured API root (matches what the SDK uses for all other API calls)
258
- const apiRoot = client.config?.TD_API_ROOT || process.env.TD_API_ROOT || "https://api.testdriver.ai";
259
+ const apiRoot = client.config?.TD_API_ROOT || process.env.TD_API_ROOT || channelConfig.channels[channelConfig.active];
259
260
 
260
261
  console.log(`[TestDriver] Uploading logs for replay ${replayId} to ${apiRoot}...`);
261
262
 
@@ -11,9 +11,19 @@ MCP server that enables AI agents to iteratively build TestDriver tests with vis
11
11
 
12
12
  ## Installation
13
13
 
14
- ### Via npx (Recommended)
14
+ ### Quick Install (Recommended)
15
15
 
16
- No installation needed! Just configure your MCP client to use npx:
16
+ Use `add-mcp` to automatically configure TestDriver for your MCP client:
17
+
18
+ ```bash
19
+ npx add-mcp testdriver
20
+ ```
21
+
22
+ This will prompt you to select your MCP client (VS Code, Cursor, Claude Desktop, etc.) and configure it automatically.
23
+
24
+ ### Manual Configuration
25
+
26
+ If you prefer to configure manually, add the following to your MCP config file:
17
27
 
18
28
  ```json
19
29
  {
@@ -141,7 +141,7 @@ export function generateActionCode(action, args, result) {
141
141
  return `const assertResult = await testdriver.assert("${escapeString(assertion)}");\nexpect(assertResult).toBeTruthy();`;
142
142
  }
143
143
  case "exec": {
144
- const language = args.language || "js";
144
+ const language = args.language || "sh";
145
145
  const code = args.code;
146
146
  const timeout = args.timeout;
147
147
  if (code.includes("\n")) {
@@ -23,9 +23,11 @@ import { sessionManager } from "./session.js";
23
23
  // Sentry
24
24
  // =============================================================================
25
25
  // Read version from main package.json (../../package.json from mcp-server/dist/)
26
- const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
27
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
26
+ const sdkRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
27
+ const packageJson = JSON.parse(fs.readFileSync(path.join(sdkRoot, "package.json"), "utf-8"));
28
28
  const version = packageJson.version || "1.0.0";
29
+ const channelConfig = JSON.parse(fs.readFileSync(path.join(sdkRoot, "channel.json"), "utf-8"));
30
+ const releaseChannel = channelConfig.active || "dev";
29
31
  const isSentryEnabled = () => {
30
32
  if (process.env.TD_TELEMETRY === "false") {
31
33
  return false;
@@ -37,7 +39,7 @@ if (isSentryEnabled()) {
37
39
  Sentry.init({
38
40
  dsn: process.env.SENTRY_DSN ||
39
41
  "https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
40
- environment: "mcp",
42
+ environment: releaseChannel,
41
43
  release: version,
42
44
  sampleRate: 1.0,
43
45
  tracesSampleRate: 1.0,
@@ -687,6 +689,7 @@ registerAppTool(server, "find", {
687
689
  const duration = Date.now() - startTime;
688
690
  // Store cropped image for resource serving (instead of inline data URL)
689
691
  let croppedImageResourceUri;
692
+ let screenshotResourceUri;
690
693
  const croppedImage = rawResponse.croppedImage;
691
694
  if (croppedImage) {
692
695
  const imageData = croppedImage.startsWith('data:')
@@ -696,6 +699,20 @@ registerAppTool(server, "find", {
696
699
  // Remove croppedImage from response to avoid context bloat
697
700
  delete rawResponse.croppedImage;
698
701
  }
702
+ else if (!found) {
703
+ // Element not found and no cropped image - capture a fresh screenshot
704
+ // so the user can see what's currently visible on screen
705
+ try {
706
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
707
+ if (screenshotBase64) {
708
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
709
+ logger.debug("find: Captured screenshot for not-found state");
710
+ }
711
+ }
712
+ catch (e) {
713
+ logger.warn("find: Failed to capture screenshot for not-found state", { error: String(e) });
714
+ }
715
+ }
699
716
  // Remove extractedText and pixelDiffImage from response to reduce context bloat
700
717
  delete rawResponse.extractedText;
701
718
  delete rawResponse.pixelDiffImage;
@@ -717,6 +734,7 @@ registerAppTool(server, "find", {
717
734
  element: elementInfo,
718
735
  ref: elementRef,
719
736
  croppedImageResourceUri,
737
+ screenshotResourceUri,
720
738
  duration,
721
739
  }, generatedCode);
722
740
  }
@@ -786,6 +804,7 @@ registerAppTool(server, "findall", {
786
804
  const duration = Date.now() - startTime;
787
805
  // Store cropped image for resource serving (instead of inline data URL)
788
806
  let croppedImageResourceUri;
807
+ let screenshotResourceUri;
789
808
  const croppedImage = rawResponse.croppedImage;
790
809
  if (croppedImage) {
791
810
  const imageData = croppedImage.startsWith('data:')
@@ -795,6 +814,20 @@ registerAppTool(server, "findall", {
795
814
  // Remove croppedImage from response to avoid context bloat
796
815
  delete rawResponse.croppedImage;
797
816
  }
817
+ else if (count === 0) {
818
+ // No elements found and no cropped image - capture a fresh screenshot
819
+ // so the user can see what's currently visible on screen
820
+ try {
821
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
822
+ if (screenshotBase64) {
823
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
824
+ logger.debug("findall: Captured screenshot for not-found state");
825
+ }
826
+ }
827
+ catch (e) {
828
+ logger.warn("findall: Failed to capture screenshot for not-found state", { error: String(e) });
829
+ }
830
+ }
798
831
  // Remove extractedText and pixelDiffImage from response to reduce context bloat
799
832
  delete rawResponse.extractedText;
800
833
  delete rawResponse.pixelDiffImage;
@@ -810,6 +843,7 @@ registerAppTool(server, "findall", {
810
843
  refs,
811
844
  elements: elementInfos,
812
845
  croppedImageResourceUri,
846
+ screenshotResourceUri,
813
847
  duration,
814
848
  }, generatedCode);
815
849
  }
@@ -1029,6 +1063,7 @@ registerAppTool(server, "find_and_click", {
1029
1063
  const duration = Date.now() - startTime;
1030
1064
  // Store cropped image (screenshot) for resource serving
1031
1065
  let croppedImageResourceUri;
1066
+ let screenshotResourceUri;
1032
1067
  const croppedImage = rawResponse.croppedImage;
1033
1068
  if (croppedImage) {
1034
1069
  const imageData = croppedImage.startsWith('data:')
@@ -1037,6 +1072,20 @@ registerAppTool(server, "find_and_click", {
1037
1072
  croppedImageResourceUri = storeImage(imageData, "screenshot");
1038
1073
  delete rawResponse.croppedImage;
1039
1074
  }
1075
+ else {
1076
+ // No cropped image - capture a fresh screenshot so the user can see
1077
+ // what's currently visible on screen when element was not found
1078
+ try {
1079
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
1080
+ if (screenshotBase64) {
1081
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
1082
+ logger.debug("find_and_click: Captured screenshot for not-found state");
1083
+ }
1084
+ }
1085
+ catch (e) {
1086
+ logger.warn("find_and_click: Failed to capture screenshot for not-found state", { error: String(e) });
1087
+ }
1088
+ }
1040
1089
  // Remove extractedText and pixelDiffImage from response to reduce context bloat
1041
1090
  delete rawResponse.extractedText;
1042
1091
  delete rawResponse.pixelDiffImage;
@@ -1045,6 +1094,7 @@ registerAppTool(server, "find_and_click", {
1045
1094
  action: "find_and_click",
1046
1095
  error: "Element not found",
1047
1096
  croppedImageResourceUri,
1097
+ screenshotResourceUri,
1048
1098
  duration
1049
1099
  });
1050
1100
  }
@@ -1372,9 +1422,9 @@ You can optionally provide a reference image URI to compare against a previous s
1372
1422
  });
1373
1423
  // Exec
1374
1424
  server.registerTool("exec", {
1375
- description: "Execute code in the sandbox (JavaScript, shell, or PowerShell)",
1425
+ description: "Execute shell or PowerShell commands in the sandbox",
1376
1426
  inputSchema: z.object({
1377
- language: z.enum(["js", "sh", "pwsh"]).default("js"),
1427
+ language: z.enum(["sh", "pwsh"]).default("sh"),
1378
1428
  code: z.string().describe("Code to execute"),
1379
1429
  timeout: z.number().default(30000).describe("Timeout in ms"),
1380
1430
  }),
@@ -161,7 +161,7 @@ export function generateActionCode(
161
161
  }
162
162
 
163
163
  case "exec": {
164
- const language = (args.language as string) || "js";
164
+ const language = (args.language as string) || "sh";
165
165
  const code = args.code as string;
166
166
  const timeout = args.timeout as number | undefined;
167
167
 
@@ -30,9 +30,11 @@ import { sessionManager, type SessionState } from "./session.js";
30
30
  // =============================================================================
31
31
 
32
32
  // Read version from main package.json (../../package.json from mcp-server/dist/)
33
- const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
34
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
33
+ const sdkRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
34
+ const packageJson = JSON.parse(fs.readFileSync(path.join(sdkRoot, "package.json"), "utf-8"));
35
35
  const version = packageJson.version || "1.0.0";
36
+ const channelConfig = JSON.parse(fs.readFileSync(path.join(sdkRoot, "channel.json"), "utf-8"));
37
+ const releaseChannel = channelConfig.active || "dev";
36
38
 
37
39
  const isSentryEnabled = () => {
38
40
  if (process.env.TD_TELEMETRY === "false") {
@@ -47,7 +49,7 @@ if (isSentryEnabled()) {
47
49
  dsn:
48
50
  process.env.SENTRY_DSN ||
49
51
  "https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
50
- environment: "mcp",
52
+ environment: releaseChannel,
51
53
  release: version,
52
54
  sampleRate: 1.0,
53
55
  tracesSampleRate: 1.0,
@@ -867,6 +869,7 @@ registerAppTool(
867
869
 
868
870
  // Store cropped image for resource serving (instead of inline data URL)
869
871
  let croppedImageResourceUri: string | undefined;
872
+ let screenshotResourceUri: string | undefined;
870
873
  const croppedImage = rawResponse.croppedImage;
871
874
  if (croppedImage) {
872
875
  const imageData = croppedImage.startsWith('data:')
@@ -875,6 +878,18 @@ registerAppTool(
875
878
  croppedImageResourceUri = storeImage(imageData, "cropped");
876
879
  // Remove croppedImage from response to avoid context bloat
877
880
  delete rawResponse.croppedImage;
881
+ } else if (!found) {
882
+ // Element not found and no cropped image - capture a fresh screenshot
883
+ // so the user can see what's currently visible on screen
884
+ try {
885
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
886
+ if (screenshotBase64) {
887
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
888
+ logger.debug("find: Captured screenshot for not-found state");
889
+ }
890
+ } catch (e) {
891
+ logger.warn("find: Failed to capture screenshot for not-found state", { error: String(e) });
892
+ }
878
893
  }
879
894
 
880
895
  // Remove extractedText and pixelDiffImage from response to reduce context bloat
@@ -904,6 +919,7 @@ registerAppTool(
904
919
  element: elementInfo,
905
920
  ref: elementRef,
906
921
  croppedImageResourceUri,
922
+ screenshotResourceUri,
907
923
  duration,
908
924
  },
909
925
  generatedCode
@@ -988,6 +1004,7 @@ registerAppTool(
988
1004
 
989
1005
  // Store cropped image for resource serving (instead of inline data URL)
990
1006
  let croppedImageResourceUri: string | undefined;
1007
+ let screenshotResourceUri: string | undefined;
991
1008
  const croppedImage = rawResponse.croppedImage;
992
1009
  if (croppedImage) {
993
1010
  const imageData = croppedImage.startsWith('data:')
@@ -996,6 +1013,18 @@ registerAppTool(
996
1013
  croppedImageResourceUri = storeImage(imageData, "cropped");
997
1014
  // Remove croppedImage from response to avoid context bloat
998
1015
  delete rawResponse.croppedImage;
1016
+ } else if (count === 0) {
1017
+ // No elements found and no cropped image - capture a fresh screenshot
1018
+ // so the user can see what's currently visible on screen
1019
+ try {
1020
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
1021
+ if (screenshotBase64) {
1022
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
1023
+ logger.debug("findall: Captured screenshot for not-found state");
1024
+ }
1025
+ } catch (e) {
1026
+ logger.warn("findall: Failed to capture screenshot for not-found state", { error: String(e) });
1027
+ }
999
1028
  }
1000
1029
 
1001
1030
  // Remove extractedText and pixelDiffImage from response to reduce context bloat
@@ -1019,6 +1048,7 @@ registerAppTool(
1019
1048
  refs,
1020
1049
  elements: elementInfos,
1021
1050
  croppedImageResourceUri,
1051
+ screenshotResourceUri,
1022
1052
  duration,
1023
1053
  },
1024
1054
  generatedCode
@@ -1317,6 +1347,7 @@ registerAppTool(
1317
1347
 
1318
1348
  // Store cropped image (screenshot) for resource serving
1319
1349
  let croppedImageResourceUri: string | undefined;
1350
+ let screenshotResourceUri: string | undefined;
1320
1351
  const croppedImage = rawResponse.croppedImage;
1321
1352
  if (croppedImage) {
1322
1353
  const imageData = croppedImage.startsWith('data:')
@@ -1324,6 +1355,18 @@ registerAppTool(
1324
1355
  : croppedImage;
1325
1356
  croppedImageResourceUri = storeImage(imageData, "screenshot");
1326
1357
  delete rawResponse.croppedImage;
1358
+ } else {
1359
+ // No cropped image - capture a fresh screenshot so the user can see
1360
+ // what's currently visible on screen when element was not found
1361
+ try {
1362
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
1363
+ if (screenshotBase64) {
1364
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
1365
+ logger.debug("find_and_click: Captured screenshot for not-found state");
1366
+ }
1367
+ } catch (e) {
1368
+ logger.warn("find_and_click: Failed to capture screenshot for not-found state", { error: String(e) });
1369
+ }
1327
1370
  }
1328
1371
 
1329
1372
  // Remove extractedText and pixelDiffImage from response to reduce context bloat
@@ -1338,6 +1381,7 @@ registerAppTool(
1338
1381
  action: "find_and_click",
1339
1382
  error: "Element not found",
1340
1383
  croppedImageResourceUri,
1384
+ screenshotResourceUri,
1341
1385
  duration
1342
1386
  }
1343
1387
  );
@@ -1760,9 +1804,9 @@ You can optionally provide a reference image URI to compare against a previous s
1760
1804
  server.registerTool(
1761
1805
  "exec",
1762
1806
  {
1763
- description: "Execute code in the sandbox (JavaScript, shell, or PowerShell)",
1807
+ description: "Execute shell or PowerShell commands in the sandbox",
1764
1808
  inputSchema: z.object({
1765
- language: z.enum(["js", "sh", "pwsh"]).default("js"),
1809
+ language: z.enum(["sh", "pwsh"]).default("sh"),
1766
1810
  code: z.string().describe("Code to execute"),
1767
1811
  timeout: z.number().default(30000).describe("Timeout in ms"),
1768
1812
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.5.26",
3
+ "version": "7.6.0-test.1",
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:windows": "TEST_PLATFORM=windows vitest run",
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 = "js" | "pwsh" | "sh";
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://v6.testdriver.ai') */
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;