testdriverai 7.2.74 → 7.2.76
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/ai/agents/testdriver.md +18 -13
- package/interfaces/cli/commands/init.js +10 -5
- package/mcp-server/dist/server.mjs +203 -5
- package/package.json +1 -1
- package/sdk.d.ts +20 -1
- package/sdk.js +148 -16
package/ai/agents/testdriver.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testdriver
|
|
3
3
|
description: An expert at creating and refining automated tests using TestDriver.ai
|
|
4
|
-
tools:
|
|
5
|
-
- testdriver/*
|
|
4
|
+
tools: ["*"]
|
|
6
5
|
mcp-servers:
|
|
7
6
|
testdriver:
|
|
8
7
|
command: npx
|
|
@@ -12,6 +11,7 @@ mcp-servers:
|
|
|
12
11
|
- testdriverai-mcp
|
|
13
12
|
env:
|
|
14
13
|
TD_API_KEY: ${TD_API_KEY}
|
|
14
|
+
tools: ["testdriverai"]
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
# TestDriver Expert
|
|
@@ -44,7 +44,7 @@ Use this agent when the user asks to:
|
|
|
44
44
|
4. **⚠️ WRITE CODE IMMEDIATELY**: After EVERY successful action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the end.
|
|
45
45
|
5. **Verify Actions**: Use `check` after actions to verify they succeeded (for YOUR understanding only).
|
|
46
46
|
6. **Add Assertions**: Use `assert` for test conditions that should be in the final test file.
|
|
47
|
-
7. **⚠️ RUN THE TEST YOURSELF**: Use `npx vitest run <testFile
|
|
47
|
+
7. **⚠️ RUN THE TEST YOURSELF**: Use `npx vitest run <testFile> --reporter=dot` to run the test - do NOT tell the user to run it. Iterate until it passes.
|
|
48
48
|
|
|
49
49
|
## Prerequisites
|
|
50
50
|
|
|
@@ -294,11 +294,15 @@ assert({ assertion: "the dashboard is visible" })
|
|
|
294
294
|
**⚠️ YOU must run the test - do NOT tell the user to run it:**
|
|
295
295
|
|
|
296
296
|
```bash
|
|
297
|
-
npx vitest run tests/login.test.mjs
|
|
297
|
+
npx vitest run tests/login.test.mjs --reporter=dot
|
|
298
298
|
```
|
|
299
299
|
|
|
300
|
+
**Always use `--reporter=dot`** for cleaner, more concise output that's easier to parse.
|
|
301
|
+
|
|
300
302
|
Analyze the output, fix any issues, and iterate until the test passes.
|
|
301
303
|
|
|
304
|
+
**⚠️ ALWAYS share the test report link with the user.** After each test run, look for the "View Report" URL in the test output (e.g., `https://app.testdriver.ai/projects/.../reports/...`) and share it with the user so they can review the recording and results.
|
|
305
|
+
|
|
302
306
|
### MCP Tools Reference
|
|
303
307
|
|
|
304
308
|
| Tool | Description |
|
|
@@ -463,16 +467,17 @@ const result = await testdriver.assert("dashboard is visible");
|
|
|
463
467
|
## Tips for Agents
|
|
464
468
|
|
|
465
469
|
1. **⚠️ WRITE CODE IMMEDIATELY** - After EVERY successful MCP action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the session ends.
|
|
466
|
-
2. **⚠️ RUN TESTS YOURSELF** - Do NOT tell the user to run tests. YOU must run the tests using `npx vitest run <testFile
|
|
470
|
+
2. **⚠️ RUN TESTS YOURSELF** - Do NOT tell the user to run tests. YOU must run the tests using `npx vitest run <testFile> --reporter=dot`. Always use `--reporter=dot` for cleaner output. Analyze the output and iterate until the test passes. **Always share the test report link** (e.g., `https://app.testdriver.ai/projects/.../reports/...`) with the user after each run.
|
|
467
471
|
3. **⚠️ ADD SCREENSHOTS LIBERALLY** - Include `await testdriver.screenshot()` throughout your tests: after provision, before/after clicks, after typing, and before assertions. This creates a visual trail that makes debugging failures much easier.
|
|
468
|
-
4.
|
|
469
|
-
5. **
|
|
470
|
-
6. **
|
|
471
|
-
7. **
|
|
472
|
-
8. **Use `check`
|
|
473
|
-
9. **
|
|
474
|
-
10. **
|
|
475
|
-
11. **
|
|
472
|
+
4. **⚠️ NEVER USE `.wait()`** - Do NOT use any `.wait()` method. Instead, use `find()` with a `timeout` option to poll for elements, or use `assert()` / `check()` to verify state. Explicit waits are flaky and slow.
|
|
473
|
+
5. **Use MCP tools for development** - Build tests interactively with visual feedback
|
|
474
|
+
6. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
|
|
475
|
+
7. **Look at test samples** in `node_modules/testdriverai/test` for working examples
|
|
476
|
+
8. **Use `check` to understand screen state** - This is how you verify what the sandbox shows during MCP development.
|
|
477
|
+
9. **Use `check` after actions, `assert` for test files** - `check` gives detailed AI analysis (no code), `assert` gives boolean pass/fail (generates code)
|
|
478
|
+
10. **Be specific with element descriptions** - "blue Sign In button in the header" > "button"
|
|
479
|
+
11. **Start simple** - get one step working before adding more
|
|
480
|
+
12. **Always `await` async methods** - TestDriver will warn if you forget, but for TypeScript projects, add `@typescript-eslint/no-floating-promises` to your ESLint config to catch missing `await` at compile time:
|
|
476
481
|
|
|
477
482
|
```json
|
|
478
483
|
// eslint.config.js (for TypeScript projects)
|
|
@@ -356,11 +356,8 @@ test('should login and add item to cart', async (context) => {
|
|
|
356
356
|
if (!fs.existsSync(configFile)) {
|
|
357
357
|
const configContent = `import { defineConfig } from 'vitest/config';
|
|
358
358
|
import TestDriver from 'testdriverai/vitest';
|
|
359
|
-
import { config } from 'dotenv';
|
|
360
|
-
|
|
361
|
-
// Load environment variables from .env file
|
|
362
|
-
config();
|
|
363
359
|
|
|
360
|
+
// Note: dotenv is loaded automatically by the TestDriver SDK
|
|
364
361
|
export default defineConfig({
|
|
365
362
|
test: {
|
|
366
363
|
testTimeout: 300000,
|
|
@@ -488,12 +485,20 @@ jobs:
|
|
|
488
485
|
|
|
489
486
|
if (!fs.existsSync(mcpConfigFile)) {
|
|
490
487
|
const mcpConfig = {
|
|
488
|
+
inputs: [
|
|
489
|
+
{
|
|
490
|
+
type: "promptString",
|
|
491
|
+
id: "testdriver-api-key",
|
|
492
|
+
description: "TestDriver API Key From https://console.testdriver.ai/team",
|
|
493
|
+
password: true,
|
|
494
|
+
},
|
|
495
|
+
],
|
|
491
496
|
servers: {
|
|
492
497
|
testdriver: {
|
|
493
498
|
command: "npx",
|
|
494
499
|
args: ["-p", "testdriverai@beta", "testdriverai-mcp"],
|
|
495
500
|
env: {
|
|
496
|
-
TD_API_KEY: "${
|
|
501
|
+
TD_API_KEY: "${input:testdriver-api-key}",
|
|
497
502
|
},
|
|
498
503
|
},
|
|
499
504
|
},
|
|
@@ -41,6 +41,7 @@ if (isSentryEnabled()) {
|
|
|
41
41
|
release: `testdriverai-mcp@${version}`,
|
|
42
42
|
sampleRate: 1.0,
|
|
43
43
|
tracesSampleRate: 1.0,
|
|
44
|
+
sendDefaultPii: true,
|
|
44
45
|
integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
|
|
45
46
|
initialScope: {
|
|
46
47
|
tags: {
|
|
@@ -265,11 +266,16 @@ function createToolResult(success, textContent, structuredData, generatedCode) {
|
|
|
265
266
|
},
|
|
266
267
|
};
|
|
267
268
|
}
|
|
268
|
-
// Create MCP server
|
|
269
|
-
const server =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
// Create MCP server wrapped with Sentry for automatic tracing
|
|
270
|
+
const server = isSentryEnabled()
|
|
271
|
+
? Sentry.wrapMcpServerWithSentry(new McpServer({
|
|
272
|
+
name: "testdriver",
|
|
273
|
+
version: version,
|
|
274
|
+
}))
|
|
275
|
+
: new McpServer({
|
|
276
|
+
name: "testdriver",
|
|
277
|
+
version: version,
|
|
278
|
+
});
|
|
273
279
|
// Element reference storage (for click/hover after find)
|
|
274
280
|
// Stores actual Element instances - no raw coordinates as input
|
|
275
281
|
const elementRefs = new Map();
|
|
@@ -1341,6 +1347,198 @@ server.registerTool("exec", {
|
|
|
1341
1347
|
throw error;
|
|
1342
1348
|
}
|
|
1343
1349
|
});
|
|
1350
|
+
// List Local Screenshots - lists screenshots saved to .testdriver directory
|
|
1351
|
+
server.registerTool("list_local_screenshots", {
|
|
1352
|
+
description: `List screenshots saved in the .testdriver directory.
|
|
1353
|
+
|
|
1354
|
+
This tool helps you find screenshots that have been saved during test runs or via the screenshot tool.
|
|
1355
|
+
Screenshots are organized in subdirectories like 'mcp-screenshots' and 'screenshots'.
|
|
1356
|
+
|
|
1357
|
+
Returns a list of screenshot paths that can be viewed with the 'view_local_screenshot' tool.`,
|
|
1358
|
+
inputSchema: z.object({
|
|
1359
|
+
directory: z.string().optional().describe("Subdirectory to list (e.g., 'mcp-screenshots', 'screenshots'). If not provided, lists all subdirectories."),
|
|
1360
|
+
}),
|
|
1361
|
+
}, async (params) => {
|
|
1362
|
+
const startTime = Date.now();
|
|
1363
|
+
logger.info("list_local_screenshots: Starting", { directory: params.directory });
|
|
1364
|
+
try {
|
|
1365
|
+
// Find .testdriver directory - check current working directory and common locations
|
|
1366
|
+
const possiblePaths = [
|
|
1367
|
+
path.join(process.cwd(), ".testdriver"),
|
|
1368
|
+
path.join(os.homedir(), ".testdriver"),
|
|
1369
|
+
];
|
|
1370
|
+
let testdriverDir = null;
|
|
1371
|
+
for (const p of possiblePaths) {
|
|
1372
|
+
if (fs.existsSync(p)) {
|
|
1373
|
+
testdriverDir = p;
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (!testdriverDir) {
|
|
1378
|
+
logger.warn("list_local_screenshots: .testdriver directory not found");
|
|
1379
|
+
return createToolResult(false, "No .testdriver directory found. Screenshots are saved here during test runs.", { error: "Directory not found" });
|
|
1380
|
+
}
|
|
1381
|
+
const screenshots = [];
|
|
1382
|
+
// Function to recursively find PNG files
|
|
1383
|
+
const findPngFiles = (dir) => {
|
|
1384
|
+
if (!fs.existsSync(dir))
|
|
1385
|
+
return;
|
|
1386
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1387
|
+
for (const entry of entries) {
|
|
1388
|
+
const fullPath = path.join(dir, entry.name);
|
|
1389
|
+
if (entry.isDirectory()) {
|
|
1390
|
+
// If a specific directory was requested, only search that one
|
|
1391
|
+
if (!params.directory || entry.name === params.directory || dir !== testdriverDir) {
|
|
1392
|
+
findPngFiles(fullPath);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".png")) {
|
|
1396
|
+
const stats = fs.statSync(fullPath);
|
|
1397
|
+
screenshots.push({
|
|
1398
|
+
path: fullPath,
|
|
1399
|
+
name: entry.name,
|
|
1400
|
+
modified: stats.mtime,
|
|
1401
|
+
size: stats.size,
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
findPngFiles(testdriverDir);
|
|
1407
|
+
// Sort by modification time (newest first)
|
|
1408
|
+
screenshots.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1409
|
+
const duration = Date.now() - startTime;
|
|
1410
|
+
logger.info("list_local_screenshots: Completed", { count: screenshots.length, duration });
|
|
1411
|
+
if (screenshots.length === 0) {
|
|
1412
|
+
return createToolResult(true, "No screenshots found in .testdriver directory.", {
|
|
1413
|
+
action: "list_local_screenshots",
|
|
1414
|
+
count: 0,
|
|
1415
|
+
directory: testdriverDir,
|
|
1416
|
+
duration
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
// Format the list for display
|
|
1420
|
+
const screenshotList = screenshots.slice(0, 50).map((s, i) => {
|
|
1421
|
+
const relativePath = path.relative(testdriverDir, s.path);
|
|
1422
|
+
const sizeKB = Math.round(s.size / 1024);
|
|
1423
|
+
const timeAgo = formatTimeAgo(s.modified);
|
|
1424
|
+
return `${i + 1}. ${relativePath} (${sizeKB}KB, ${timeAgo})`;
|
|
1425
|
+
}).join("\n");
|
|
1426
|
+
const message = screenshots.length > 50
|
|
1427
|
+
? `Found ${screenshots.length} screenshots (showing 50 most recent):\n\n${screenshotList}`
|
|
1428
|
+
: `Found ${screenshots.length} screenshot(s):\n\n${screenshotList}`;
|
|
1429
|
+
return createToolResult(true, message, {
|
|
1430
|
+
action: "list_local_screenshots",
|
|
1431
|
+
count: screenshots.length,
|
|
1432
|
+
directory: testdriverDir,
|
|
1433
|
+
screenshots: screenshots.slice(0, 50).map(s => ({
|
|
1434
|
+
path: s.path,
|
|
1435
|
+
relativePath: path.relative(testdriverDir, s.path),
|
|
1436
|
+
name: s.name,
|
|
1437
|
+
modified: s.modified.toISOString(),
|
|
1438
|
+
sizeBytes: s.size,
|
|
1439
|
+
})),
|
|
1440
|
+
duration
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
catch (error) {
|
|
1444
|
+
logger.error("list_local_screenshots: Failed", { error: String(error) });
|
|
1445
|
+
captureException(error, { tags: { tool: "list_local_screenshots" } });
|
|
1446
|
+
throw error;
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
// Helper to format time ago
|
|
1450
|
+
function formatTimeAgo(date) {
|
|
1451
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
1452
|
+
if (seconds < 60)
|
|
1453
|
+
return `${seconds}s ago`;
|
|
1454
|
+
const minutes = Math.floor(seconds / 60);
|
|
1455
|
+
if (minutes < 60)
|
|
1456
|
+
return `${minutes}m ago`;
|
|
1457
|
+
const hours = Math.floor(minutes / 60);
|
|
1458
|
+
if (hours < 24)
|
|
1459
|
+
return `${hours}h ago`;
|
|
1460
|
+
const days = Math.floor(hours / 24);
|
|
1461
|
+
return `${days}d ago`;
|
|
1462
|
+
}
|
|
1463
|
+
// View Local Screenshot - view a screenshot from .testdriver directory
|
|
1464
|
+
// Returns the image so AI clients that support images can see it
|
|
1465
|
+
// Also displays to the user via MCP App
|
|
1466
|
+
registerAppTool(server, "view_local_screenshot", {
|
|
1467
|
+
title: "View Local Screenshot",
|
|
1468
|
+
description: `View a screenshot from the .testdriver directory.
|
|
1469
|
+
|
|
1470
|
+
Use 'list_local_screenshots' first to see available screenshots, then use this tool to view one.
|
|
1471
|
+
|
|
1472
|
+
This tool returns the image content so AI clients that support images can see it directly.
|
|
1473
|
+
The image is also displayed to the user via the MCP App UI.
|
|
1474
|
+
|
|
1475
|
+
Useful for:
|
|
1476
|
+
- Reviewing screenshots from previous test runs
|
|
1477
|
+
- Debugging test failures by examining saved screenshots
|
|
1478
|
+
- Comparing current screen state to saved screenshots`,
|
|
1479
|
+
inputSchema: z.object({
|
|
1480
|
+
path: z.string().describe("Full path to the screenshot file (from list_local_screenshots)"),
|
|
1481
|
+
}),
|
|
1482
|
+
_meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
|
|
1483
|
+
}, async (params) => {
|
|
1484
|
+
const startTime = Date.now();
|
|
1485
|
+
logger.info("view_local_screenshot: Starting", { path: params.path });
|
|
1486
|
+
try {
|
|
1487
|
+
// Validate the path exists and is a PNG
|
|
1488
|
+
if (!fs.existsSync(params.path)) {
|
|
1489
|
+
logger.warn("view_local_screenshot: File not found", { path: params.path });
|
|
1490
|
+
return createToolResult(false, `Screenshot not found: ${params.path}`, { error: "File not found" });
|
|
1491
|
+
}
|
|
1492
|
+
if (!params.path.toLowerCase().endsWith(".png")) {
|
|
1493
|
+
logger.warn("view_local_screenshot: Not a PNG file", { path: params.path });
|
|
1494
|
+
return createToolResult(false, "Only PNG files are supported", { error: "Invalid file type" });
|
|
1495
|
+
}
|
|
1496
|
+
// Security check - only allow files from .testdriver directory
|
|
1497
|
+
const normalizedPath = path.resolve(params.path);
|
|
1498
|
+
if (!normalizedPath.includes(".testdriver")) {
|
|
1499
|
+
logger.warn("view_local_screenshot: Path not in .testdriver", { path: normalizedPath });
|
|
1500
|
+
return createToolResult(false, "Can only view screenshots from .testdriver directory", { error: "Security: path not allowed" });
|
|
1501
|
+
}
|
|
1502
|
+
// Read the file
|
|
1503
|
+
const imageBuffer = fs.readFileSync(params.path);
|
|
1504
|
+
const imageBase64 = imageBuffer.toString("base64");
|
|
1505
|
+
// Store image for MCP App UI display
|
|
1506
|
+
const screenshotResourceUri = storeImage(imageBase64, "screenshot");
|
|
1507
|
+
const stats = fs.statSync(params.path);
|
|
1508
|
+
const sizeKB = Math.round(stats.size / 1024);
|
|
1509
|
+
const fileName = path.basename(params.path);
|
|
1510
|
+
const duration = Date.now() - startTime;
|
|
1511
|
+
logger.info("view_local_screenshot: Completed", { path: params.path, sizeKB, duration });
|
|
1512
|
+
// Return the image content for AI clients that support images
|
|
1513
|
+
// The content array includes both text and image for maximum compatibility
|
|
1514
|
+
const content = [
|
|
1515
|
+
{ type: "text", text: `Screenshot: ${fileName} (${sizeKB}KB)` },
|
|
1516
|
+
{
|
|
1517
|
+
type: "image",
|
|
1518
|
+
data: imageBase64,
|
|
1519
|
+
mimeType: "image/png"
|
|
1520
|
+
},
|
|
1521
|
+
];
|
|
1522
|
+
return {
|
|
1523
|
+
content,
|
|
1524
|
+
structuredContent: {
|
|
1525
|
+
action: "view_local_screenshot",
|
|
1526
|
+
success: true,
|
|
1527
|
+
path: params.path,
|
|
1528
|
+
fileName,
|
|
1529
|
+
sizeBytes: stats.size,
|
|
1530
|
+
modified: stats.mtime.toISOString(),
|
|
1531
|
+
screenshotResourceUri,
|
|
1532
|
+
duration
|
|
1533
|
+
},
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
logger.error("view_local_screenshot: Failed", { error: String(error), path: params.path });
|
|
1538
|
+
captureException(error, { tags: { tool: "view_local_screenshot" }, extra: { path: params.path } });
|
|
1539
|
+
throw error;
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1344
1542
|
// Screenshot - captures full screen to show user the current state
|
|
1345
1543
|
// NOTE: This is for SHOWING the user the screen, not for AI understanding.
|
|
1346
1544
|
// Use 'check' tool for AI to understand screen state.
|
package/package.json
CHANGED
package/sdk.d.ts
CHANGED
|
@@ -867,7 +867,26 @@ export interface DashcamAPI {
|
|
|
867
867
|
}
|
|
868
868
|
|
|
869
869
|
export default class TestDriverSDK {
|
|
870
|
-
|
|
870
|
+
/**
|
|
871
|
+
* Create a new TestDriverSDK instance
|
|
872
|
+
* Automatically loads environment variables from .env file via dotenv.
|
|
873
|
+
*
|
|
874
|
+
* @param apiKey - API key (optional, defaults to TD_API_KEY environment variable)
|
|
875
|
+
* @param options - SDK configuration options
|
|
876
|
+
*
|
|
877
|
+
* @example
|
|
878
|
+
* // API key loaded automatically from TD_API_KEY in .env
|
|
879
|
+
* const client = new TestDriver();
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* // Pass options only (API key from .env)
|
|
883
|
+
* const client = new TestDriver({ os: 'windows' });
|
|
884
|
+
*
|
|
885
|
+
* @example
|
|
886
|
+
* // Or pass API key explicitly
|
|
887
|
+
* const client = new TestDriver('your-api-key');
|
|
888
|
+
*/
|
|
889
|
+
constructor(apiKey?: string | TestDriverOptions, options?: TestDriverOptions);
|
|
871
890
|
|
|
872
891
|
/**
|
|
873
892
|
* Whether the SDK is currently connected to a sandbox
|
package/sdk.js
CHANGED
|
@@ -3,7 +3,9 @@ const path = require("path");
|
|
|
3
3
|
const os = require("os");
|
|
4
4
|
const crypto = require("crypto");
|
|
5
5
|
const { formatter } = require("./sdk-log-formatter");
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
// Load .env file into process.env by default
|
|
8
|
+
require("dotenv").config();
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Get the file path of the caller (the file that called TestDriver)
|
|
@@ -1233,20 +1235,24 @@ function createChainablePromise(promise) {
|
|
|
1233
1235
|
* TestDriver SDK
|
|
1234
1236
|
*
|
|
1235
1237
|
* This SDK provides programmatic access to TestDriver's AI-powered testing capabilities.
|
|
1238
|
+
* Automatically loads environment variables from .env file via dotenv.
|
|
1236
1239
|
*
|
|
1237
1240
|
* @example
|
|
1238
1241
|
* const TestDriver = require('testdriverai');
|
|
1239
1242
|
*
|
|
1240
|
-
*
|
|
1243
|
+
* // API key loaded automatically from TD_API_KEY in .env
|
|
1244
|
+
* const client = new TestDriver();
|
|
1241
1245
|
* await client.connect();
|
|
1242
1246
|
*
|
|
1247
|
+
* // Pass options only (API key from .env)
|
|
1248
|
+
* const client = new TestDriver({ os: 'windows' });
|
|
1249
|
+
*
|
|
1250
|
+
* // Or pass API key explicitly
|
|
1251
|
+
* const client = new TestDriver('your-api-key');
|
|
1252
|
+
*
|
|
1243
1253
|
* // New API
|
|
1244
1254
|
* const element = await client.find('Submit button');
|
|
1245
1255
|
* await element.click();
|
|
1246
|
-
*
|
|
1247
|
-
* // Legacy API (deprecated)
|
|
1248
|
-
* await client.hoverText('Submit');
|
|
1249
|
-
* await client.click();
|
|
1250
1256
|
*/
|
|
1251
1257
|
|
|
1252
1258
|
/**
|
|
@@ -1264,9 +1270,18 @@ const { createMarkdownLogger } = require("./interfaces/logger.js");
|
|
|
1264
1270
|
|
|
1265
1271
|
class TestDriverSDK {
|
|
1266
1272
|
constructor(apiKey, options = {}) {
|
|
1273
|
+
// Support calling with just options: new TestDriver({ os: 'windows' })
|
|
1274
|
+
if (typeof apiKey === 'object' && apiKey !== null) {
|
|
1275
|
+
options = apiKey;
|
|
1276
|
+
apiKey = null;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Use provided API key or fall back to environment variable
|
|
1280
|
+
const resolvedApiKey = apiKey || process.env.TD_API_KEY;
|
|
1281
|
+
|
|
1267
1282
|
// Set up environment with API key
|
|
1268
1283
|
const environment = {
|
|
1269
|
-
TD_API_KEY:
|
|
1284
|
+
TD_API_KEY: resolvedApiKey,
|
|
1270
1285
|
TD_API_ROOT: options.apiRoot || "https://testdriver-api.onrender.com",
|
|
1271
1286
|
TD_RESOLUTION: options.resolution || "1366x768",
|
|
1272
1287
|
TD_ANALYTICS: options.analytics !== false,
|
|
@@ -1507,7 +1522,64 @@ class TestDriverSDK {
|
|
|
1507
1522
|
await this._dashcam.addWebLog("**", "Web Logs");
|
|
1508
1523
|
}
|
|
1509
1524
|
|
|
1525
|
+
// Set up Chrome profile with preferences
|
|
1510
1526
|
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
1527
|
+
const userDataDir =
|
|
1528
|
+
this.os === "windows"
|
|
1529
|
+
? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
|
|
1530
|
+
: "/tmp/testdriver-chrome-profile";
|
|
1531
|
+
|
|
1532
|
+
// Create user data directory and Default profile directory
|
|
1533
|
+
const defaultProfileDir =
|
|
1534
|
+
this.os === "windows"
|
|
1535
|
+
? `${userDataDir}\\Default`
|
|
1536
|
+
: `${userDataDir}/Default`;
|
|
1537
|
+
|
|
1538
|
+
const createDirCmd =
|
|
1539
|
+
this.os === "windows"
|
|
1540
|
+
? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
|
|
1541
|
+
: `mkdir -p "${defaultProfileDir}"`;
|
|
1542
|
+
|
|
1543
|
+
await this.exec(shell, createDirCmd, 60000, true);
|
|
1544
|
+
|
|
1545
|
+
// Write Chrome preferences
|
|
1546
|
+
const chromePrefs = {
|
|
1547
|
+
credentials_enable_service: false,
|
|
1548
|
+
profile: {
|
|
1549
|
+
password_manager_enabled: false,
|
|
1550
|
+
default_content_setting_values: {},
|
|
1551
|
+
},
|
|
1552
|
+
signin: {
|
|
1553
|
+
allowed: false,
|
|
1554
|
+
},
|
|
1555
|
+
sync: {
|
|
1556
|
+
requested: false,
|
|
1557
|
+
first_setup_complete: true,
|
|
1558
|
+
sync_all_os_types: false,
|
|
1559
|
+
},
|
|
1560
|
+
autofill: {
|
|
1561
|
+
enabled: false,
|
|
1562
|
+
},
|
|
1563
|
+
local_state: {
|
|
1564
|
+
browser: {
|
|
1565
|
+
has_seen_welcome_page: true,
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
const prefsPath =
|
|
1571
|
+
this.os === "windows"
|
|
1572
|
+
? `${defaultProfileDir}\\Preferences`
|
|
1573
|
+
: `${defaultProfileDir}/Preferences`;
|
|
1574
|
+
|
|
1575
|
+
const prefsJson = JSON.stringify(chromePrefs, null, 2);
|
|
1576
|
+
const writePrefCmd =
|
|
1577
|
+
this.os === "windows"
|
|
1578
|
+
? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
|
|
1579
|
+
`[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
|
|
1580
|
+
: `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
|
|
1581
|
+
|
|
1582
|
+
await this.exec(shell, writePrefCmd, 60000, true);
|
|
1511
1583
|
|
|
1512
1584
|
// Build Chrome launch command
|
|
1513
1585
|
const chromeArgs = [];
|
|
@@ -1519,6 +1591,7 @@ class TestDriverSDK {
|
|
|
1519
1591
|
"--no-first-run",
|
|
1520
1592
|
"--no-experiments",
|
|
1521
1593
|
"--disable-infobars",
|
|
1594
|
+
`--user-data-dir=${userDataDir}`,
|
|
1522
1595
|
);
|
|
1523
1596
|
|
|
1524
1597
|
// Add remote debugging port for captcha solving support
|
|
@@ -1731,6 +1804,64 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1731
1804
|
await this._dashcam.addWebLog("**", "Web Logs");
|
|
1732
1805
|
}
|
|
1733
1806
|
|
|
1807
|
+
// Set up Chrome profile with preferences
|
|
1808
|
+
const userDataDir =
|
|
1809
|
+
this.os === "windows"
|
|
1810
|
+
? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
|
|
1811
|
+
: "/tmp/testdriver-chrome-profile";
|
|
1812
|
+
|
|
1813
|
+
// Create user data directory and Default profile directory
|
|
1814
|
+
const defaultProfileDir =
|
|
1815
|
+
this.os === "windows"
|
|
1816
|
+
? `${userDataDir}\\Default`
|
|
1817
|
+
: `${userDataDir}/Default`;
|
|
1818
|
+
|
|
1819
|
+
const createDirCmd =
|
|
1820
|
+
this.os === "windows"
|
|
1821
|
+
? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
|
|
1822
|
+
: `mkdir -p "${defaultProfileDir}"`;
|
|
1823
|
+
|
|
1824
|
+
await this.exec(shell, createDirCmd, 60000, true);
|
|
1825
|
+
|
|
1826
|
+
// Write Chrome preferences
|
|
1827
|
+
const chromePrefs = {
|
|
1828
|
+
credentials_enable_service: false,
|
|
1829
|
+
profile: {
|
|
1830
|
+
password_manager_enabled: false,
|
|
1831
|
+
default_content_setting_values: {},
|
|
1832
|
+
},
|
|
1833
|
+
signin: {
|
|
1834
|
+
allowed: false,
|
|
1835
|
+
},
|
|
1836
|
+
sync: {
|
|
1837
|
+
requested: false,
|
|
1838
|
+
first_setup_complete: true,
|
|
1839
|
+
sync_all_os_types: false,
|
|
1840
|
+
},
|
|
1841
|
+
autofill: {
|
|
1842
|
+
enabled: false,
|
|
1843
|
+
},
|
|
1844
|
+
local_state: {
|
|
1845
|
+
browser: {
|
|
1846
|
+
has_seen_welcome_page: true,
|
|
1847
|
+
},
|
|
1848
|
+
},
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
const prefsPath =
|
|
1852
|
+
this.os === "windows"
|
|
1853
|
+
? `${defaultProfileDir}\\Preferences`
|
|
1854
|
+
: `${defaultProfileDir}/Preferences`;
|
|
1855
|
+
|
|
1856
|
+
const prefsJson = JSON.stringify(chromePrefs, null, 2);
|
|
1857
|
+
const writePrefCmd =
|
|
1858
|
+
this.os === "windows"
|
|
1859
|
+
? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
|
|
1860
|
+
`[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
|
|
1861
|
+
: `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
|
|
1862
|
+
|
|
1863
|
+
await this.exec(shell, writePrefCmd, 60000, true);
|
|
1864
|
+
|
|
1734
1865
|
// Build Chrome launch command
|
|
1735
1866
|
const chromeArgs = [];
|
|
1736
1867
|
if (maximized) chromeArgs.push("--start-maximized");
|
|
@@ -1741,6 +1872,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1741
1872
|
"--no-experiments",
|
|
1742
1873
|
"--disable-infobars",
|
|
1743
1874
|
"--disable-features=ChromeLabs",
|
|
1875
|
+
`--user-data-dir=${userDataDir}`,
|
|
1744
1876
|
);
|
|
1745
1877
|
|
|
1746
1878
|
// Add remote debugging port for captcha solving support
|
|
@@ -3435,28 +3567,28 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3435
3567
|
*
|
|
3436
3568
|
* @example
|
|
3437
3569
|
* // Simple execution
|
|
3438
|
-
* const result = await client.
|
|
3570
|
+
* const result = await client.act('Click the submit button');
|
|
3439
3571
|
* console.log(result.success); // true
|
|
3440
3572
|
*
|
|
3441
3573
|
* @example
|
|
3442
3574
|
* // With custom retry limit
|
|
3443
|
-
* const result = await client.
|
|
3575
|
+
* const result = await client.act('Fill out the contact form', { tries: 10 });
|
|
3444
3576
|
* console.log(`Completed in ${result.tries} tries`);
|
|
3445
3577
|
*
|
|
3446
3578
|
* @example
|
|
3447
3579
|
* // Handle failures
|
|
3448
3580
|
* try {
|
|
3449
|
-
* await client.
|
|
3581
|
+
* await client.act('Complete the checkout process', { tries: 3 });
|
|
3450
3582
|
* } catch (error) {
|
|
3451
3583
|
* console.log(`Failed after ${error.tries} tries: ${error.message}`);
|
|
3452
3584
|
* }
|
|
3453
3585
|
*/
|
|
3454
|
-
async
|
|
3586
|
+
async act(task, options = {}) {
|
|
3455
3587
|
this._ensureConnected();
|
|
3456
3588
|
|
|
3457
3589
|
const { tries = 7 } = options;
|
|
3458
3590
|
|
|
3459
|
-
this.analytics.track("sdk.
|
|
3591
|
+
this.analytics.track("sdk.act", { task, tries });
|
|
3460
3592
|
|
|
3461
3593
|
const { events } = require("./agent/events.js");
|
|
3462
3594
|
const startTime = Date.now();
|
|
@@ -3465,7 +3597,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3465
3597
|
const originalCheckLimit = this.agent.checkLimit;
|
|
3466
3598
|
this.agent.checkLimit = tries;
|
|
3467
3599
|
|
|
3468
|
-
// Reset check count for this
|
|
3600
|
+
// Reset check count for this act() call
|
|
3469
3601
|
const originalCheckCount = this.agent.checkCount;
|
|
3470
3602
|
this.agent.checkCount = 0;
|
|
3471
3603
|
|
|
@@ -3532,7 +3664,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3532
3664
|
}
|
|
3533
3665
|
|
|
3534
3666
|
/**
|
|
3535
|
-
* @deprecated Use
|
|
3667
|
+
* @deprecated Use act() instead
|
|
3536
3668
|
* Execute a natural language task using AI
|
|
3537
3669
|
*
|
|
3538
3670
|
* @param {string} task - Natural language description of what to do
|
|
@@ -3540,8 +3672,8 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3540
3672
|
* @param {number} [options.tries=7] - Maximum number of check/retry attempts
|
|
3541
3673
|
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3542
3674
|
*/
|
|
3543
|
-
async
|
|
3544
|
-
return await this.
|
|
3675
|
+
async ai(task, options) {
|
|
3676
|
+
return await this.act(task, options);
|
|
3545
3677
|
}
|
|
3546
3678
|
}
|
|
3547
3679
|
|