testdriverai 7.2.75 → 7.2.77
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/agent/lib/redraw.js +15 -4
- package/ai/agents/testdriver.md +18 -12
- package/interfaces/cli/commands/init.js +9 -1
- package/mcp-server/dist/server.mjs +203 -5
- package/package.json +1 -1
- package/sdk.d.ts +5 -1
- package/sdk.js +129 -14
package/agent/lib/redraw.js
CHANGED
|
@@ -8,6 +8,7 @@ const DEFAULT_REDRAW_OPTIONS = {
|
|
|
8
8
|
enabled: true, // Master switch to enable/disable redraw detection
|
|
9
9
|
screenRedraw: true, // Enable screen redraw detection
|
|
10
10
|
networkMonitor: true, // Enable network activity monitoring
|
|
11
|
+
noChangeTimeoutMs: 1500, // Exit early if no screen change detected after this time
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
// Factory function that creates redraw functionality with the provided system instance
|
|
@@ -235,7 +236,7 @@ const createRedraw = (
|
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
async function checkCondition(resolve, startTime, timeoutMs, options) {
|
|
238
|
-
const { enabled, screenRedraw, networkMonitor } = options;
|
|
239
|
+
const { enabled, screenRedraw, networkMonitor, noChangeTimeoutMs = 1500 } = options;
|
|
239
240
|
|
|
240
241
|
// If redraw is disabled, resolve immediately
|
|
241
242
|
if (!enabled) {
|
|
@@ -248,6 +249,9 @@ const createRedraw = (
|
|
|
248
249
|
let diffFromInitial = 0;
|
|
249
250
|
let diffFromLast = 0;
|
|
250
251
|
let isTimeout = timeElapsed > timeoutMs;
|
|
252
|
+
|
|
253
|
+
// Early exit: if no screen change detected after noChangeTimeoutMs, assume action had no visual effect
|
|
254
|
+
const noChangeTimeout = screenRedraw && !hasChangedFromInitial && timeElapsed > noChangeTimeoutMs;
|
|
251
255
|
|
|
252
256
|
// Screen stability detection:
|
|
253
257
|
// 1. Check if screen has changed from initial (detect transition)
|
|
@@ -276,8 +280,14 @@ const createRedraw = (
|
|
|
276
280
|
lastScreenImage = nowImage;
|
|
277
281
|
}
|
|
278
282
|
|
|
279
|
-
// Screen is settled when:
|
|
280
|
-
|
|
283
|
+
// Screen is settled when:
|
|
284
|
+
// 1. It has changed from initial AND consecutive frames are now stable, OR
|
|
285
|
+
// 2. No change was detected after noChangeTimeoutMs (action had no visual effect)
|
|
286
|
+
const screenSettled = (hasChangedFromInitial && consecutiveFramesStable) || noChangeTimeout;
|
|
287
|
+
|
|
288
|
+
if (noChangeTimeout && !hasChangedFromInitial) {
|
|
289
|
+
emitter.emit(events.log.debug, `[redraw] No screen change detected after ${noChangeTimeoutMs}ms, settling early`);
|
|
290
|
+
}
|
|
281
291
|
|
|
282
292
|
// If screen redraw is disabled, consider it as "settled"
|
|
283
293
|
const effectiveScreenSettled = screenRedraw ? screenSettled : true;
|
|
@@ -334,12 +344,13 @@ const createRedraw = (
|
|
|
334
344
|
networkSettled: effectiveNetworkSettled,
|
|
335
345
|
isTimeout,
|
|
336
346
|
timeElapsed,
|
|
347
|
+
noChangeTimeout,
|
|
337
348
|
});
|
|
338
349
|
resolve("true");
|
|
339
350
|
} else {
|
|
340
351
|
setTimeout(() => {
|
|
341
352
|
checkCondition(resolve, startTime, timeoutMs, options);
|
|
342
|
-
},
|
|
353
|
+
}, 250);
|
|
343
354
|
}
|
|
344
355
|
}
|
|
345
356
|
|
package/ai/agents/testdriver.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testdriver
|
|
3
3
|
description: An expert at creating and refining automated tests using TestDriver.ai
|
|
4
|
-
tools:
|
|
4
|
+
tools: ["*"]
|
|
5
5
|
mcp-servers:
|
|
6
6
|
testdriver:
|
|
7
7
|
command: npx
|
|
@@ -11,6 +11,7 @@ mcp-servers:
|
|
|
11
11
|
- testdriverai-mcp
|
|
12
12
|
env:
|
|
13
13
|
TD_API_KEY: ${TD_API_KEY}
|
|
14
|
+
tools: ["testdriverai"]
|
|
14
15
|
---
|
|
15
16
|
|
|
16
17
|
# TestDriver Expert
|
|
@@ -43,7 +44,7 @@ Use this agent when the user asks to:
|
|
|
43
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.
|
|
44
45
|
5. **Verify Actions**: Use `check` after actions to verify they succeeded (for YOUR understanding only).
|
|
45
46
|
6. **Add Assertions**: Use `assert` for test conditions that should be in the final test file.
|
|
46
|
-
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.
|
|
47
48
|
|
|
48
49
|
## Prerequisites
|
|
49
50
|
|
|
@@ -293,11 +294,15 @@ assert({ assertion: "the dashboard is visible" })
|
|
|
293
294
|
**⚠️ YOU must run the test - do NOT tell the user to run it:**
|
|
294
295
|
|
|
295
296
|
```bash
|
|
296
|
-
npx vitest run tests/login.test.mjs
|
|
297
|
+
npx vitest run tests/login.test.mjs --reporter=dot
|
|
297
298
|
```
|
|
298
299
|
|
|
300
|
+
**Always use `--reporter=dot`** for cleaner, more concise output that's easier to parse.
|
|
301
|
+
|
|
299
302
|
Analyze the output, fix any issues, and iterate until the test passes.
|
|
300
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
|
+
|
|
301
306
|
### MCP Tools Reference
|
|
302
307
|
|
|
303
308
|
| Tool | Description |
|
|
@@ -462,16 +467,17 @@ const result = await testdriver.assert("dashboard is visible");
|
|
|
462
467
|
## Tips for Agents
|
|
463
468
|
|
|
464
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.
|
|
465
|
-
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.
|
|
466
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.
|
|
467
|
-
4.
|
|
468
|
-
5. **
|
|
469
|
-
6. **
|
|
470
|
-
7. **
|
|
471
|
-
8. **Use `check`
|
|
472
|
-
9. **
|
|
473
|
-
10. **
|
|
474
|
-
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:
|
|
475
481
|
|
|
476
482
|
```json
|
|
477
483
|
// eslint.config.js (for TypeScript projects)
|
|
@@ -485,12 +485,20 @@ jobs:
|
|
|
485
485
|
|
|
486
486
|
if (!fs.existsSync(mcpConfigFile)) {
|
|
487
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
|
+
],
|
|
488
496
|
servers: {
|
|
489
497
|
testdriver: {
|
|
490
498
|
command: "npx",
|
|
491
499
|
args: ["-p", "testdriverai@beta", "testdriverai-mcp"],
|
|
492
500
|
env: {
|
|
493
|
-
TD_API_KEY: "${
|
|
501
|
+
TD_API_KEY: "${input:testdriver-api-key}",
|
|
494
502
|
},
|
|
495
503
|
},
|
|
496
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
|
@@ -879,10 +879,14 @@ export default class TestDriverSDK {
|
|
|
879
879
|
* const client = new TestDriver();
|
|
880
880
|
*
|
|
881
881
|
* @example
|
|
882
|
+
* // Pass options only (API key from .env)
|
|
883
|
+
* const client = new TestDriver({ os: 'windows' });
|
|
884
|
+
*
|
|
885
|
+
* @example
|
|
882
886
|
* // Or pass API key explicitly
|
|
883
887
|
* const client = new TestDriver('your-api-key');
|
|
884
888
|
*/
|
|
885
|
-
constructor(apiKey?: string, options?: TestDriverOptions);
|
|
889
|
+
constructor(apiKey?: string | TestDriverOptions, options?: TestDriverOptions);
|
|
886
890
|
|
|
887
891
|
/**
|
|
888
892
|
* Whether the SDK is currently connected to a sandbox
|
package/sdk.js
CHANGED
|
@@ -3,7 +3,6 @@ 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
|
-
const logger = require("./agent/lib/logger");
|
|
7
6
|
|
|
8
7
|
// Load .env file into process.env by default
|
|
9
8
|
require("dotenv").config();
|
|
@@ -1245,16 +1244,15 @@ function createChainablePromise(promise) {
|
|
|
1245
1244
|
* const client = new TestDriver();
|
|
1246
1245
|
* await client.connect();
|
|
1247
1246
|
*
|
|
1247
|
+
* // Pass options only (API key from .env)
|
|
1248
|
+
* const client = new TestDriver({ os: 'windows' });
|
|
1249
|
+
*
|
|
1248
1250
|
* // Or pass API key explicitly
|
|
1249
1251
|
* const client = new TestDriver('your-api-key');
|
|
1250
1252
|
*
|
|
1251
1253
|
* // New API
|
|
1252
1254
|
* const element = await client.find('Submit button');
|
|
1253
1255
|
* await element.click();
|
|
1254
|
-
*
|
|
1255
|
-
* // Legacy API (deprecated)
|
|
1256
|
-
* await client.hoverText('Submit');
|
|
1257
|
-
* await client.click();
|
|
1258
1256
|
*/
|
|
1259
1257
|
|
|
1260
1258
|
/**
|
|
@@ -1524,7 +1522,64 @@ class TestDriverSDK {
|
|
|
1524
1522
|
await this._dashcam.addWebLog("**", "Web Logs");
|
|
1525
1523
|
}
|
|
1526
1524
|
|
|
1525
|
+
// Set up Chrome profile with preferences
|
|
1527
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);
|
|
1528
1583
|
|
|
1529
1584
|
// Build Chrome launch command
|
|
1530
1585
|
const chromeArgs = [];
|
|
@@ -1536,6 +1591,7 @@ class TestDriverSDK {
|
|
|
1536
1591
|
"--no-first-run",
|
|
1537
1592
|
"--no-experiments",
|
|
1538
1593
|
"--disable-infobars",
|
|
1594
|
+
`--user-data-dir=${userDataDir}`,
|
|
1539
1595
|
);
|
|
1540
1596
|
|
|
1541
1597
|
// Add remote debugging port for captcha solving support
|
|
@@ -1748,6 +1804,64 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1748
1804
|
await this._dashcam.addWebLog("**", "Web Logs");
|
|
1749
1805
|
}
|
|
1750
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
|
+
|
|
1751
1865
|
// Build Chrome launch command
|
|
1752
1866
|
const chromeArgs = [];
|
|
1753
1867
|
if (maximized) chromeArgs.push("--start-maximized");
|
|
@@ -1758,6 +1872,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1758
1872
|
"--no-experiments",
|
|
1759
1873
|
"--disable-infobars",
|
|
1760
1874
|
"--disable-features=ChromeLabs",
|
|
1875
|
+
`--user-data-dir=${userDataDir}`,
|
|
1761
1876
|
);
|
|
1762
1877
|
|
|
1763
1878
|
// Add remote debugging port for captcha solving support
|
|
@@ -3452,28 +3567,28 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3452
3567
|
*
|
|
3453
3568
|
* @example
|
|
3454
3569
|
* // Simple execution
|
|
3455
|
-
* const result = await client.
|
|
3570
|
+
* const result = await client.act('Click the submit button');
|
|
3456
3571
|
* console.log(result.success); // true
|
|
3457
3572
|
*
|
|
3458
3573
|
* @example
|
|
3459
3574
|
* // With custom retry limit
|
|
3460
|
-
* const result = await client.
|
|
3575
|
+
* const result = await client.act('Fill out the contact form', { tries: 10 });
|
|
3461
3576
|
* console.log(`Completed in ${result.tries} tries`);
|
|
3462
3577
|
*
|
|
3463
3578
|
* @example
|
|
3464
3579
|
* // Handle failures
|
|
3465
3580
|
* try {
|
|
3466
|
-
* await client.
|
|
3581
|
+
* await client.act('Complete the checkout process', { tries: 3 });
|
|
3467
3582
|
* } catch (error) {
|
|
3468
3583
|
* console.log(`Failed after ${error.tries} tries: ${error.message}`);
|
|
3469
3584
|
* }
|
|
3470
3585
|
*/
|
|
3471
|
-
async
|
|
3586
|
+
async act(task, options = {}) {
|
|
3472
3587
|
this._ensureConnected();
|
|
3473
3588
|
|
|
3474
3589
|
const { tries = 7 } = options;
|
|
3475
3590
|
|
|
3476
|
-
this.analytics.track("sdk.
|
|
3591
|
+
this.analytics.track("sdk.act", { task, tries });
|
|
3477
3592
|
|
|
3478
3593
|
const { events } = require("./agent/events.js");
|
|
3479
3594
|
const startTime = Date.now();
|
|
@@ -3482,7 +3597,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3482
3597
|
const originalCheckLimit = this.agent.checkLimit;
|
|
3483
3598
|
this.agent.checkLimit = tries;
|
|
3484
3599
|
|
|
3485
|
-
// Reset check count for this
|
|
3600
|
+
// Reset check count for this act() call
|
|
3486
3601
|
const originalCheckCount = this.agent.checkCount;
|
|
3487
3602
|
this.agent.checkCount = 0;
|
|
3488
3603
|
|
|
@@ -3549,7 +3664,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3549
3664
|
}
|
|
3550
3665
|
|
|
3551
3666
|
/**
|
|
3552
|
-
* @deprecated Use
|
|
3667
|
+
* @deprecated Use act() instead
|
|
3553
3668
|
* Execute a natural language task using AI
|
|
3554
3669
|
*
|
|
3555
3670
|
* @param {string} task - Natural language description of what to do
|
|
@@ -3557,8 +3672,8 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3557
3672
|
* @param {number} [options.tries=7] - Maximum number of check/retry attempts
|
|
3558
3673
|
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3559
3674
|
*/
|
|
3560
|
-
async
|
|
3561
|
-
return await this.
|
|
3675
|
+
async ai(task, options) {
|
|
3676
|
+
return await this.act(task, options);
|
|
3562
3677
|
}
|
|
3563
3678
|
}
|
|
3564
3679
|
|