testdriverai 7.2.73 → 7.2.75
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/{test-writer.md → testdriver.md} +111 -85
- package/interfaces/cli/commands/init.js +70 -95
- package/interfaces/cli/commands/setup.js +6 -6
- package/mcp-server/dist/mcp-app.html +2 -2
- package/mcp-server/dist/provision-types.d.ts +1 -1
- package/mcp-server/dist/provision-types.js +2 -2
- package/mcp-server/dist/server.mjs +25 -49
- package/package.json +1 -1
- package/sdk.d.ts +16 -1
- package/sdk.js +19 -2
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
+
name: testdriver
|
|
2
3
|
description: An expert at creating and refining automated tests using TestDriver.ai
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
tools:
|
|
5
|
+
mcp-servers:
|
|
6
|
+
testdriver:
|
|
7
|
+
command: npx
|
|
8
|
+
args:
|
|
9
|
+
- -p
|
|
10
|
+
- testdriverai@beta
|
|
11
|
+
- testdriverai-mcp
|
|
12
|
+
env:
|
|
13
|
+
TD_API_KEY: ${TD_API_KEY}
|
|
11
14
|
---
|
|
12
15
|
|
|
13
16
|
# TestDriver Expert
|
|
@@ -35,11 +38,12 @@ Use this agent when the user asks to:
|
|
|
35
38
|
### Workflow
|
|
36
39
|
|
|
37
40
|
1. **Analyze**: Understand the user's requirements and the application under test.
|
|
38
|
-
2. **Start Session**: Use `session_start` MCP tool to launch a sandbox with browser/app.
|
|
39
|
-
3. **Interact**: Use MCP tools (`find`, `click`, `type`, etc.) - each returns a screenshot
|
|
40
|
-
4.
|
|
41
|
-
5. **
|
|
42
|
-
6. **
|
|
41
|
+
2. **Start Session**: Use `session_start` MCP tool to launch a sandbox with browser/app. Specify `testFile` to track where code should be written.
|
|
42
|
+
3. **Interact**: Use MCP tools (`find`, `click`, `type`, etc.) - each returns a screenshot AND generated code.
|
|
43
|
+
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
|
+
5. **Verify Actions**: Use `check` after actions to verify they succeeded (for YOUR understanding only).
|
|
45
|
+
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>` to run the test - do NOT tell the user to run it. Iterate until it passes.
|
|
43
47
|
|
|
44
48
|
## Prerequisites
|
|
45
49
|
|
|
@@ -108,12 +112,16 @@ describe("My Test Suite", () => {
|
|
|
108
112
|
await testdriver.provision.chrome({
|
|
109
113
|
url: "https://example.com",
|
|
110
114
|
});
|
|
115
|
+
await testdriver.screenshot(); // Capture initial page state
|
|
111
116
|
|
|
112
117
|
// Find elements and interact
|
|
113
118
|
const button = await testdriver.find("Sign In button");
|
|
119
|
+
await testdriver.screenshot(); // Capture before click
|
|
114
120
|
await button.click();
|
|
121
|
+
await testdriver.screenshot(); // Capture after click
|
|
115
122
|
|
|
116
123
|
// Assert using natural language
|
|
124
|
+
await testdriver.screenshot(); // Capture before assertion
|
|
117
125
|
const result = await testdriver.assert("the dashboard is visible");
|
|
118
126
|
expect(result).toBeTruthy();
|
|
119
127
|
});
|
|
@@ -177,9 +185,9 @@ await element.mouseUp(); // release mouse
|
|
|
177
185
|
element.found(); // check if found (boolean)
|
|
178
186
|
```
|
|
179
187
|
|
|
180
|
-
### Screenshots
|
|
188
|
+
### Screenshots for Debugging
|
|
181
189
|
|
|
182
|
-
Use `screenshot()`
|
|
190
|
+
**Use `screenshot()` liberally throughout your tests** to capture the screen state at key moments. This makes debugging much easier when tests fail - you can see exactly what the screen looked like at each step.
|
|
183
191
|
|
|
184
192
|
```javascript
|
|
185
193
|
// Capture a screenshot - saved to .testdriver/screenshots/<test-file>/
|
|
@@ -190,6 +198,14 @@ console.log("Screenshot saved to:", screenshotPath);
|
|
|
190
198
|
await testdriver.screenshot(1, false, true);
|
|
191
199
|
```
|
|
192
200
|
|
|
201
|
+
**When to add screenshots:**
|
|
202
|
+
- After provisioning (initial page load)
|
|
203
|
+
- Before and after clicking important elements
|
|
204
|
+
- After typing text into fields
|
|
205
|
+
- Before assertions (to see what the AI is evaluating)
|
|
206
|
+
- After any action that changes the page state
|
|
207
|
+
- When debugging a flaky or failing test
|
|
208
|
+
|
|
193
209
|
**Screenshot file organization:**
|
|
194
210
|
|
|
195
211
|
```
|
|
@@ -210,107 +226,106 @@ await testdriver.screenshot(1, false, true);
|
|
|
210
226
|
### Key Advantages
|
|
211
227
|
|
|
212
228
|
- **No need to restart** - continue from current state
|
|
213
|
-
- **
|
|
214
|
-
- **Code generation** - convert recorded commands to test files
|
|
229
|
+
- **Generated code with every action** - each tool returns the code to add to your test
|
|
215
230
|
- **Use `check` to verify** - understand screen state without explicit screenshots
|
|
216
231
|
|
|
232
|
+
### ⚠️ CRITICAL: Write Code Immediately & Run Tests Yourself
|
|
233
|
+
|
|
234
|
+
**Every MCP tool response includes "ACTION REQUIRED: Append this code..." - you MUST write that code to the test file IMMEDIATELY before proceeding to the next action.**
|
|
235
|
+
|
|
236
|
+
**When ready to validate, RUN THE TEST YOURSELF using `npx vitest run`. Do NOT tell the user to run it.**
|
|
237
|
+
|
|
217
238
|
### Step 1: Start a Session
|
|
218
239
|
|
|
219
240
|
```
|
|
220
|
-
session_start({ type: "chrome", url: "https://your-app.com/login" })
|
|
241
|
+
session_start({ type: "chrome", url: "https://your-app.com/login", testFile: "tests/login.test.mjs" })
|
|
221
242
|
→ Screenshot shows login page
|
|
243
|
+
→ Response includes: "ACTION REQUIRED: Append this code..."
|
|
244
|
+
→ ⚠️ IMMEDIATELY write to tests/login.test.mjs:
|
|
245
|
+
await testdriver.provision.chrome({ url: "https://your-app.com/login" });
|
|
246
|
+
await testdriver.screenshot(); // Capture initial page state
|
|
222
247
|
```
|
|
223
248
|
|
|
224
249
|
This provisions a sandbox with Chrome and navigates to your URL. You'll see a screenshot of the initial page.
|
|
225
250
|
|
|
226
251
|
### Step 2: Interact with the App
|
|
227
252
|
|
|
228
|
-
Find elements and interact with them
|
|
253
|
+
Find elements and interact with them. **Write code to file after EACH action, including screenshots for debugging:**
|
|
229
254
|
|
|
230
255
|
```
|
|
231
|
-
|
|
232
|
-
→ Returns: screenshot with element highlighted
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
256
|
+
find_and_click({ description: "email input field" })
|
|
257
|
+
→ Returns: screenshot with element highlighted
|
|
258
|
+
→ ⚠️ IMMEDIATELY append to test file:
|
|
259
|
+
await testdriver.find("email input field").click();
|
|
260
|
+
await testdriver.screenshot(); // Capture after click
|
|
236
261
|
|
|
237
262
|
type({ text: "user@example.com" })
|
|
238
263
|
→ Returns: screenshot showing typed text
|
|
264
|
+
→ ⚠️ IMMEDIATELY append to test file:
|
|
265
|
+
await testdriver.type("user@example.com");
|
|
266
|
+
await testdriver.screenshot(); // Capture after typing
|
|
239
267
|
```
|
|
240
268
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
```
|
|
244
|
-
find_and_click({ description: "Sign In button" })
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### Step 3: Verify Actions Succeeded
|
|
269
|
+
### Step 3: Verify Actions Succeeded (For Your Understanding)
|
|
248
270
|
|
|
249
|
-
After
|
|
271
|
+
After actions, use `check` to verify they worked. This is for YOUR understanding - does NOT generate code:
|
|
250
272
|
|
|
251
273
|
```
|
|
252
274
|
check({ task: "Was the email entered into the field?" })
|
|
253
275
|
→ Returns: AI analysis comparing previous screenshot to current state
|
|
254
276
|
```
|
|
255
277
|
|
|
256
|
-
### Step 4: Add Assertions
|
|
278
|
+
### Step 4: Add Assertions (Generates Code)
|
|
257
279
|
|
|
258
|
-
Use `assert` for pass/fail conditions
|
|
280
|
+
Use `assert` for pass/fail conditions. This DOES generate code for the test file:
|
|
259
281
|
|
|
260
282
|
```
|
|
261
283
|
assert({ assertion: "the dashboard is visible" })
|
|
262
284
|
→ Returns: pass/fail with screenshot
|
|
285
|
+
→ ⚠️ IMMEDIATELY append to test file:
|
|
286
|
+
await testdriver.screenshot(); // Capture before assertion
|
|
287
|
+
const assertResult = await testdriver.assert("the dashboard is visible");
|
|
288
|
+
expect(assertResult).toBeTruthy();
|
|
263
289
|
```
|
|
264
290
|
|
|
265
|
-
### Step 5:
|
|
291
|
+
### Step 5: Run the Test Yourself
|
|
266
292
|
|
|
267
|
-
|
|
293
|
+
**⚠️ YOU must run the test - do NOT tell the user to run it:**
|
|
268
294
|
|
|
295
|
+
```bash
|
|
296
|
+
npx vitest run tests/login.test.mjs
|
|
269
297
|
```
|
|
270
|
-
commit({
|
|
271
|
-
testFile: "tests/login.test.mjs",
|
|
272
|
-
testName: "Login Flow",
|
|
273
|
-
testDescription: "User can log in with email and password"
|
|
274
|
-
})
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### Step 6: Verify the Test
|
|
278
298
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
```
|
|
282
|
-
verify({ testFile: "tests/login.test.mjs" })
|
|
283
|
-
```
|
|
299
|
+
Analyze the output, fix any issues, and iterate until the test passes.
|
|
284
300
|
|
|
285
301
|
### MCP Tools Reference
|
|
286
302
|
|
|
287
303
|
| Tool | Description |
|
|
288
304
|
|------|-------------|
|
|
289
|
-
| `session_start` | Start sandbox with browser/app,
|
|
290
|
-
| `session_status` | Check session health
|
|
305
|
+
| `session_start` | Start sandbox with browser/app, returns screenshot + provision code |
|
|
306
|
+
| `session_status` | Check session health and time remaining |
|
|
291
307
|
| `session_extend` | Add more time before session expires |
|
|
292
308
|
| `find` | Locate element by description, returns ref for later use |
|
|
293
|
-
| `click` | Click on element ref
|
|
309
|
+
| `click` | Click on element ref |
|
|
294
310
|
| `find_and_click` | Find and click in one action |
|
|
295
311
|
| `type` | Type text into focused field |
|
|
296
312
|
| `press_keys` | Press keyboard shortcuts (e.g., `["ctrl", "a"]`) |
|
|
297
313
|
| `scroll` | Scroll page (up/down/left/right) |
|
|
298
|
-
| `check` | AI analysis of
|
|
299
|
-
| `assert` | AI-powered boolean assertion
|
|
314
|
+
| `check` | AI analysis of screen state - for YOUR understanding only, does NOT generate code |
|
|
315
|
+
| `assert` | AI-powered boolean assertion - GENERATES CODE for test files |
|
|
300
316
|
| `exec` | Execute JavaScript, shell, or PowerShell in sandbox |
|
|
301
317
|
| `screenshot` | Capture screenshot - **only use when user explicitly asks** |
|
|
302
|
-
| `commit` | Write recorded commands to test file |
|
|
303
|
-
| `verify` | Run test file from scratch |
|
|
304
|
-
| `get_command_log` | View recorded commands before committing |
|
|
305
318
|
|
|
306
319
|
### Tips for MCP Workflow
|
|
307
320
|
|
|
308
|
-
1.
|
|
309
|
-
2. **Use `
|
|
310
|
-
3.
|
|
311
|
-
4. **
|
|
312
|
-
5. **
|
|
313
|
-
6. **
|
|
321
|
+
1. **⚠️ Write code IMMEDIATELY** - After EVERY action, append generated code to test file RIGHT AWAY
|
|
322
|
+
2. **⚠️ Run tests YOURSELF** - Use `npx vitest run` - do NOT tell user to run tests
|
|
323
|
+
3. **⚠️ Add screenshots liberally** - Include `await testdriver.screenshot()` after every significant action for debugging
|
|
324
|
+
4. **Work incrementally** - Don't try to build the entire test at once
|
|
325
|
+
5. **Use `check` after actions** - Verify your actions succeeded before moving on (for YOUR understanding)
|
|
326
|
+
6. **Use `assert` for test verifications** - These generate code that goes in the test file
|
|
327
|
+
7. **Be specific with element descriptions** - "the blue Sign In button in the header" is better than "button"
|
|
328
|
+
8. **Extend session proactively** - Sessions expire after 5 minutes; use `session_extend` if needed
|
|
314
329
|
|
|
315
330
|
## Recommended Development Workflow
|
|
316
331
|
|
|
@@ -325,17 +340,21 @@ verify({ testFile: "tests/login.test.mjs" })
|
|
|
325
340
|
it("should incrementally build test", async (context) => {
|
|
326
341
|
const testdriver = TestDriver(context);
|
|
327
342
|
await testdriver.provision.chrome({ url: "https://example.com" });
|
|
343
|
+
await testdriver.screenshot(); // Capture initial state
|
|
328
344
|
|
|
329
345
|
// Step 1: Find and inspect
|
|
330
346
|
const element = await testdriver.find("Some button");
|
|
331
347
|
console.log("Element found:", element.found());
|
|
332
348
|
console.log("Coordinates:", element.x, element.y);
|
|
333
349
|
console.log("Confidence:", element.confidence);
|
|
350
|
+
await testdriver.screenshot(); // Capture after find
|
|
334
351
|
|
|
335
352
|
// Step 2: Interact
|
|
336
353
|
await element.click();
|
|
354
|
+
await testdriver.screenshot(); // Capture after click
|
|
337
355
|
|
|
338
356
|
// Step 3: Assert and log
|
|
357
|
+
await testdriver.screenshot(); // Capture before assertion
|
|
339
358
|
const result = await testdriver.assert("Something happened");
|
|
340
359
|
console.log("Assertion result:", result);
|
|
341
360
|
expect(result).toBeTruthy();
|
|
@@ -417,33 +436,42 @@ const date = await testdriver.exec("pwsh", "Get-Date", 5000);
|
|
|
417
436
|
|
|
418
437
|
### Capturing Screenshots
|
|
419
438
|
|
|
439
|
+
**Add screenshots liberally throughout your tests** for debugging. When a test fails, you'll have a visual trail showing exactly what happened at each step.
|
|
440
|
+
|
|
420
441
|
```javascript
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
const filepath = "screenshot.png";
|
|
424
|
-
fs.writeFileSync(filepath, Buffer.from(screenshot, "base64"));
|
|
425
|
-
console.log("Screenshot saved to:", filepath);
|
|
442
|
+
// Basic screenshot - automatically saved to .testdriver/screenshots/<test-file>/
|
|
443
|
+
await testdriver.screenshot();
|
|
426
444
|
|
|
427
445
|
// Capture with mouse cursor visible
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
433
|
-
|
|
446
|
+
await testdriver.screenshot(1, false, true);
|
|
447
|
+
|
|
448
|
+
// Recommended pattern: screenshot after every significant action
|
|
449
|
+
await testdriver.provision.chrome({ url: "https://example.com" });
|
|
450
|
+
await testdriver.screenshot(); // After page load
|
|
451
|
+
|
|
452
|
+
await testdriver.find("Login button").click();
|
|
453
|
+
await testdriver.screenshot(); // After click
|
|
454
|
+
|
|
455
|
+
await testdriver.type("user@example.com");
|
|
456
|
+
await testdriver.screenshot(); // After typing
|
|
457
|
+
|
|
458
|
+
await testdriver.screenshot(); // Before assertion
|
|
459
|
+
const result = await testdriver.assert("dashboard is visible");
|
|
434
460
|
```
|
|
435
461
|
|
|
436
462
|
## Tips for Agents
|
|
437
463
|
|
|
438
|
-
1.
|
|
439
|
-
2. **
|
|
440
|
-
3.
|
|
441
|
-
4. **Use
|
|
442
|
-
5. **
|
|
443
|
-
6. **
|
|
444
|
-
7. **
|
|
445
|
-
8. **
|
|
446
|
-
9. **
|
|
464
|
+
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>`. Analyze the output and iterate until the test passes.
|
|
466
|
+
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. **Use MCP tools for development** - Build tests interactively with visual feedback
|
|
468
|
+
5. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
|
|
469
|
+
6. **Look at test samples** in `node_modules/testdriverai/test` for working examples
|
|
470
|
+
7. **Use `check` to understand screen state** - This is how you verify what the sandbox shows during MCP development.
|
|
471
|
+
8. **Use `check` after actions, `assert` for test files** - `check` gives detailed AI analysis (no code), `assert` gives boolean pass/fail (generates code)
|
|
472
|
+
9. **Be specific with element descriptions** - "blue Sign In button in the header" > "button"
|
|
473
|
+
10. **Start simple** - get one step working before adding more
|
|
474
|
+
11. **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:
|
|
447
475
|
|
|
448
476
|
```json
|
|
449
477
|
// eslint.config.js (for TypeScript projects)
|
|
@@ -453,5 +481,3 @@ console.log("Screenshot with mouse saved to: screenshot-with-mouse.png");
|
|
|
453
481
|
}
|
|
454
482
|
}
|
|
455
483
|
```
|
|
456
|
-
|
|
457
|
-
10. **Use `verify` to validate tests** - After committing, run `verify` to ensure the generated test works from scratch.
|
|
@@ -21,6 +21,7 @@ class InitCommand extends BaseCommand {
|
|
|
21
21
|
await this.createGitHubWorkflow();
|
|
22
22
|
await this.createGitignore();
|
|
23
23
|
await this.createVscodeMcpConfig();
|
|
24
|
+
await this.createVscodeExtensions();
|
|
24
25
|
await this.installDependencies();
|
|
25
26
|
await this.copySkills();
|
|
26
27
|
await this.createAgents();
|
|
@@ -149,7 +150,10 @@ class InitCommand extends BaseCommand {
|
|
|
149
150
|
try {
|
|
150
151
|
execSync(`setx ${key} "${value}"`, { stdio: "ignore" });
|
|
151
152
|
console.log(
|
|
152
|
-
chalk.green(` ✓ Set ${key} as user environment variable
|
|
153
|
+
chalk.green(` ✓ Set ${key} as user environment variable`),
|
|
154
|
+
);
|
|
155
|
+
console.log(
|
|
156
|
+
chalk.gray(` Restart your terminal for changes to take effect\n`),
|
|
153
157
|
);
|
|
154
158
|
} catch (error) {
|
|
155
159
|
console.log(
|
|
@@ -184,7 +188,10 @@ class InitCommand extends BaseCommand {
|
|
|
184
188
|
);
|
|
185
189
|
fs.writeFileSync(profilePath, updated);
|
|
186
190
|
console.log(
|
|
187
|
-
chalk.green(` ✓ Updated ${key} in ${profilePath}
|
|
191
|
+
chalk.green(` ✓ Updated ${key} in ${profilePath}`),
|
|
192
|
+
);
|
|
193
|
+
console.log(
|
|
194
|
+
chalk.gray(` Run: source ${profilePath} (or open a new terminal)\n`),
|
|
188
195
|
);
|
|
189
196
|
return;
|
|
190
197
|
}
|
|
@@ -193,7 +200,10 @@ class InitCommand extends BaseCommand {
|
|
|
193
200
|
// Append to profile
|
|
194
201
|
fs.appendFileSync(profilePath, `\n${exportLine}\n`);
|
|
195
202
|
console.log(
|
|
196
|
-
chalk.green(` ✓ Added ${key} to ${profilePath}
|
|
203
|
+
chalk.green(` ✓ Added ${key} to ${profilePath}`),
|
|
204
|
+
);
|
|
205
|
+
console.log(
|
|
206
|
+
chalk.gray(` Run: source ${profilePath} (or open a new terminal)\n`),
|
|
197
207
|
);
|
|
198
208
|
}
|
|
199
209
|
|
|
@@ -346,11 +356,8 @@ test('should login and add item to cart', async (context) => {
|
|
|
346
356
|
if (!fs.existsSync(configFile)) {
|
|
347
357
|
const configContent = `import { defineConfig } from 'vitest/config';
|
|
348
358
|
import TestDriver from 'testdriverai/vitest';
|
|
349
|
-
import { config } from 'dotenv';
|
|
350
|
-
|
|
351
|
-
// Load environment variables from .env file
|
|
352
|
-
config();
|
|
353
359
|
|
|
360
|
+
// Note: dotenv is loaded automatically by the TestDriver SDK
|
|
354
361
|
export default defineConfig({
|
|
355
362
|
test: {
|
|
356
363
|
testTimeout: 300000,
|
|
@@ -478,10 +485,10 @@ jobs:
|
|
|
478
485
|
|
|
479
486
|
if (!fs.existsSync(mcpConfigFile)) {
|
|
480
487
|
const mcpConfig = {
|
|
481
|
-
|
|
488
|
+
servers: {
|
|
482
489
|
testdriver: {
|
|
483
490
|
command: "npx",
|
|
484
|
-
args: ["testdriverai-mcp"],
|
|
491
|
+
args: ["-p", "testdriverai@beta", "testdriverai-mcp"],
|
|
485
492
|
env: {
|
|
486
493
|
TD_API_KEY: "${TD_API_KEY}",
|
|
487
494
|
},
|
|
@@ -499,6 +506,36 @@ jobs:
|
|
|
499
506
|
}
|
|
500
507
|
}
|
|
501
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Create VSCode extensions recommendations
|
|
511
|
+
*/
|
|
512
|
+
async createVscodeExtensions() {
|
|
513
|
+
const vscodeDir = path.join(process.cwd(), ".vscode");
|
|
514
|
+
const extensionsFile = path.join(vscodeDir, "extensions.json");
|
|
515
|
+
|
|
516
|
+
// Create .vscode directory if it doesn't exist
|
|
517
|
+
if (!fs.existsSync(vscodeDir)) {
|
|
518
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
519
|
+
console.log(chalk.gray(` Created directory: ${vscodeDir}`));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (!fs.existsSync(extensionsFile)) {
|
|
523
|
+
const extensionsConfig = {
|
|
524
|
+
recommendations: [
|
|
525
|
+
"vitest.explorer",
|
|
526
|
+
],
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
fs.writeFileSync(
|
|
530
|
+
extensionsFile,
|
|
531
|
+
JSON.stringify(extensionsConfig, null, 2) + "\n",
|
|
532
|
+
);
|
|
533
|
+
console.log(chalk.green(` Created extensions config: ${extensionsFile}`));
|
|
534
|
+
} else {
|
|
535
|
+
console.log(chalk.gray(" Extensions config already exists, skipping..."));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
502
539
|
/**
|
|
503
540
|
* Copy TestDriver skills from the package to the project
|
|
504
541
|
*/
|
|
@@ -557,7 +594,7 @@ jobs:
|
|
|
557
594
|
}
|
|
558
595
|
|
|
559
596
|
/**
|
|
560
|
-
*
|
|
597
|
+
* Copy TestDriver agents to .github/agents
|
|
561
598
|
*/
|
|
562
599
|
async createAgents() {
|
|
563
600
|
const agentsDestDir = path.join(process.cwd(), ".github", "agents");
|
|
@@ -576,98 +613,36 @@ jobs:
|
|
|
576
613
|
}
|
|
577
614
|
}
|
|
578
615
|
|
|
616
|
+
if (!agentsSourceDir) {
|
|
617
|
+
console.log(chalk.yellow(" ⚠️ Agents directory not found, skipping agents copy..."));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
579
621
|
// Create .github/agents directory if it doesn't exist
|
|
580
622
|
if (!fs.existsSync(agentsDestDir)) {
|
|
581
623
|
fs.mkdirSync(agentsDestDir, { recursive: true });
|
|
582
624
|
console.log(chalk.gray(` Created directory: ${agentsDestDir}`));
|
|
583
625
|
}
|
|
584
626
|
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
// Parse the source frontmatter and body
|
|
598
|
-
const frontmatterMatch = sourceContent.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
599
|
-
|
|
600
|
-
if (frontmatterMatch) {
|
|
601
|
-
const frontmatterText = frontmatterMatch[1];
|
|
602
|
-
const body = frontmatterMatch[2];
|
|
603
|
-
|
|
604
|
-
// Extract description from frontmatter
|
|
605
|
-
const descMatch = frontmatterText.match(/description:\s*["']?(.*?)["']?$/m);
|
|
606
|
-
const description = descMatch ? descMatch[1] : `TestDriver ${agentName} agent`;
|
|
607
|
-
|
|
608
|
-
// Create GitHub Copilot agent format
|
|
609
|
-
const agentContent = `---
|
|
610
|
-
name: ${agentName}
|
|
611
|
-
description: ${description}
|
|
612
|
-
tools:
|
|
613
|
-
- testdriver/*
|
|
614
|
-
mcp-servers:
|
|
615
|
-
testdriver:
|
|
616
|
-
command: npx
|
|
617
|
-
args:
|
|
618
|
-
- testdriverai-mcp
|
|
619
|
-
env:
|
|
620
|
-
TD_API_KEY: \${TD_API_KEY}
|
|
621
|
-
---
|
|
622
|
-
${body}`;
|
|
623
|
-
|
|
624
|
-
fs.writeFileSync(destPath, agentContent);
|
|
625
|
-
console.log(chalk.green(` Created agent: ${destPath}`));
|
|
626
|
-
}
|
|
627
|
-
} else {
|
|
628
|
-
console.log(chalk.gray(` Agent ${agentName}.agent.md already exists, skipping...`));
|
|
629
|
-
}
|
|
627
|
+
// Copy agent files with .agent.md extension
|
|
628
|
+
const agentFiles = fs.readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
|
|
629
|
+
let copiedCount = 0;
|
|
630
|
+
|
|
631
|
+
for (const agentFile of agentFiles) {
|
|
632
|
+
const sourcePath = path.join(agentsSourceDir, agentFile);
|
|
633
|
+
const agentName = agentFile.replace(".md", "");
|
|
634
|
+
const destPath = path.join(agentsDestDir, `${agentName}.agent.md`);
|
|
635
|
+
|
|
636
|
+
if (!fs.existsSync(destPath)) {
|
|
637
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
638
|
+
copiedCount++;
|
|
630
639
|
}
|
|
631
|
-
}
|
|
632
|
-
// Create a default test-writer agent if no source found
|
|
633
|
-
const defaultAgentPath = path.join(agentsDestDir, "test-writer.agent.md");
|
|
634
|
-
|
|
635
|
-
if (!fs.existsSync(defaultAgentPath)) {
|
|
636
|
-
const defaultAgentContent = `---
|
|
637
|
-
name: test-writer
|
|
638
|
-
description: An expert at creating and refining automated tests using TestDriver.ai
|
|
639
|
-
tools:
|
|
640
|
-
- testdriver/*
|
|
641
|
-
mcp-servers:
|
|
642
|
-
testdriver:
|
|
643
|
-
command: npx
|
|
644
|
-
args:
|
|
645
|
-
- testdriverai-mcp
|
|
646
|
-
env:
|
|
647
|
-
TD_API_KEY: \${TD_API_KEY}
|
|
648
|
-
---
|
|
649
|
-
# TestDriver Expert
|
|
650
|
-
|
|
651
|
-
You are an expert at writing automated tests using the TestDriver library. Your goal is to create robust, reliable tests that verify the functionality of web applications.
|
|
652
|
-
|
|
653
|
-
## Workflow
|
|
654
|
-
|
|
655
|
-
1. **Start Session**: Use \`session_start\` to provision a sandbox with browser
|
|
656
|
-
2. **Interact**: Use \`find\`, \`click\`, \`type\` etc. - each returns a screenshot
|
|
657
|
-
3. **Verify**: Use \`check\` after actions and \`assert\` for test conditions
|
|
658
|
-
4. **Build Test**: Append generated code to your test file
|
|
659
|
-
5. **Validate**: Use \`verify\` to run the test from scratch
|
|
660
|
-
|
|
661
|
-
## Tips
|
|
662
|
-
|
|
663
|
-
- Be specific with element descriptions: "blue Sign In button in the header" > "button"
|
|
664
|
-
- Use \`check\` after actions to verify they succeeded
|
|
665
|
-
- Start simple - get one step working before adding more
|
|
666
|
-
`;
|
|
640
|
+
}
|
|
667
641
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
642
|
+
if (copiedCount > 0) {
|
|
643
|
+
console.log(chalk.green(` Copied ${copiedCount} agent(s) to ${agentsDestDir}`));
|
|
644
|
+
} else {
|
|
645
|
+
console.log(chalk.gray(" Agents already exist, skipping..."));
|
|
671
646
|
}
|
|
672
647
|
}
|
|
673
648
|
|
|
@@ -705,7 +680,7 @@ You are an expert at writing automated tests using the TestDriver library. Your
|
|
|
705
680
|
console.log(" 1. Run your tests:");
|
|
706
681
|
console.log(chalk.gray(" npx vitest run\n"));
|
|
707
682
|
console.log(" 2. Use AI agents to write tests:");
|
|
708
|
-
console.log(chalk.gray(" Open VSCode/Cursor and use @
|
|
683
|
+
console.log(chalk.gray(" Open VSCode/Cursor and use @testdriver agent\n"));
|
|
709
684
|
console.log(" 3. MCP server configured:");
|
|
710
685
|
console.log(chalk.gray(" TestDriver tools available via MCP in .vscode/mcp.json\n"));
|
|
711
686
|
console.log(
|
|
@@ -47,7 +47,7 @@ class SetupCommand extends Command {
|
|
|
47
47
|
|
|
48
48
|
this.installSkills(sourceSkills, path.join(CLAUDE_HOME, "skills"));
|
|
49
49
|
this.installAgents(sourceAgents, path.join(CLAUDE_HOME, "agents"));
|
|
50
|
-
this.
|
|
50
|
+
this.installClaudeMcp();
|
|
51
51
|
this.installCursorMcp();
|
|
52
52
|
await this.promptForApiKey();
|
|
53
53
|
|
|
@@ -125,7 +125,7 @@ class SetupCommand extends Command {
|
|
|
125
125
|
/**
|
|
126
126
|
* Add testdriver MCP server to ~/.claude.json
|
|
127
127
|
*/
|
|
128
|
-
|
|
128
|
+
installClaudeMcp() {
|
|
129
129
|
let config = {};
|
|
130
130
|
|
|
131
131
|
if (fs.existsSync(CLAUDE_MCP_FILE)) {
|
|
@@ -182,13 +182,13 @@ class SetupCommand extends Command {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
if (!config.
|
|
186
|
-
config.
|
|
185
|
+
if (!config.servers) {
|
|
186
|
+
config.servers = {};
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
const alreadyConfigured = config.
|
|
189
|
+
const alreadyConfigured = config.servers["testdriver-cloud"];
|
|
190
190
|
|
|
191
|
-
Object.assign(config.
|
|
191
|
+
Object.assign(config.servers, CURSOR_MCP_SERVER_CONFIG);
|
|
192
192
|
fs.writeFileSync(CURSOR_MCP_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
193
193
|
|
|
194
194
|
if (alreadyConfigured) {
|
|
@@ -90,8 +90,8 @@ Note: This type uses \`Record<K, string | undefined>\` rather than \`Partial<Rec
|
|
|
90
90
|
for compatibility with Zod schema generation. Both are functionally equivalent for validation.`);c.object({method:c.literal("ui/open-link"),params:c.object({url:c.string().describe("URL to open in the host's browser")})});var fI=c.object({isError:c.boolean().optional().describe("True if the host failed to open the URL (e.g., due to security policy).")}).passthrough(),pI=c.object({isError:c.boolean().optional().describe("True if the host rejected or failed to deliver the message.")}).passthrough();c.object({method:c.literal("ui/notifications/sandbox-proxy-ready"),params:c.object({})});var fo=c.object({connectDomains:c.array(c.string()).optional().describe("Origins for network requests (fetch/XHR/WebSocket)."),resourceDomains:c.array(c.string()).optional().describe("Origins for static resources (scripts, images, styles, fonts)."),frameDomains:c.array(c.string()).optional().describe("Origins for nested iframes (frame-src directive)."),baseUriDomains:c.array(c.string()).optional().describe("Allowed base URIs for the document (base-uri directive).")}),po=c.object({camera:c.object({}).optional().describe("Request camera access (Permission Policy `camera` feature)."),microphone:c.object({}).optional().describe("Request microphone access (Permission Policy `microphone` feature)."),geolocation:c.object({}).optional().describe("Request geolocation access (Permission Policy `geolocation` feature)."),clipboardWrite:c.object({}).optional().describe("Request clipboard write access (Permission Policy `clipboard-write` feature).")});c.object({method:c.literal("ui/notifications/size-changed"),params:c.object({width:c.number().optional().describe("New width in pixels."),height:c.number().optional().describe("New height in pixels.")})});var vI=c.object({method:c.literal("ui/notifications/tool-input"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Complete tool call arguments as key-value pairs.")).optional().describe("Complete tool call arguments as key-value pairs.")})}),hI=c.object({method:c.literal("ui/notifications/tool-input-partial"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Partial tool call arguments (incomplete, may change).")).optional().describe("Partial tool call arguments (incomplete, may change).")})}),gI=c.object({method:c.literal("ui/notifications/tool-cancelled"),params:c.object({reason:c.string().optional().describe('Optional reason for the cancellation (e.g., "user action", "timeout").')})}),$I=c.object({fonts:c.string().optional()}),_I=c.object({variables:mI.optional().describe("CSS variables for theming the app."),css:$I.optional().describe("CSS blocks that apps can inject.")}),bI=c.object({method:c.literal("ui/resource-teardown"),params:c.object({})});c.record(c.string(),c.unknown());var ts=c.object({text:c.object({}).optional().describe("Host supports text content blocks."),image:c.object({}).optional().describe("Host supports image content blocks."),audio:c.object({}).optional().describe("Host supports audio content blocks."),resource:c.object({}).optional().describe("Host supports resource content blocks."),resourceLink:c.object({}).optional().describe("Host supports resource link content blocks."),structuredContent:c.object({}).optional().describe("Host supports structured content.")}),yI=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),openLinks:c.object({}).optional().describe("Host supports opening external URLs."),serverTools:c.object({listChanged:c.boolean().optional().describe("Host supports tools/list_changed notifications.")}).optional().describe("Host can proxy tool calls to the MCP server."),serverResources:c.object({listChanged:c.boolean().optional().describe("Host supports resources/list_changed notifications.")}).optional().describe("Host can proxy resource reads to the MCP server."),logging:c.object({}).optional().describe("Host accepts log messages."),sandbox:c.object({permissions:po.optional().describe("Permissions granted by the host (camera, microphone, geolocation)."),csp:fo.optional().describe("CSP domains approved by the host.")}).optional().describe("Sandbox configuration applied by the host."),updateModelContext:ts.optional().describe("Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns."),message:ts.optional().describe("Host supports receiving content messages (ui/message) from the view.")}),kI=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),tools:c.object({listChanged:c.boolean().optional().describe("App supports tools/list_changed notifications.")}).optional().describe("App exposes MCP-style tools that the host can call."),availableDisplayModes:c.array(Ut).optional().describe("Display modes the app supports.")});c.object({method:c.literal("ui/notifications/initialized"),params:c.object({}).optional()});c.object({csp:fo.optional().describe("Content Security Policy configuration."),permissions:po.optional().describe("Sandbox permissions requested by the UI."),domain:c.string().optional().describe("Dedicated origin for view sandbox."),prefersBorder:c.boolean().optional().describe("Visual boundary preference - true if UI prefers a visible border.")});c.object({method:c.literal("ui/request-display-mode"),params:c.object({mode:Ut.describe("The display mode being requested.")})});var II=c.object({mode:Ut.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),wI=c.union([c.literal("model"),c.literal("app")]).describe("Tool visibility scope - who can access the tool.");c.object({resourceUri:c.string().optional(),visibility:c.array(wI).optional().describe(`Who can access this tool. Default: ["model", "app"]
|
|
91
91
|
- "model": Tool visible to and callable by the agent
|
|
92
92
|
- "app": Tool callable by the app from this server only`)});c.object({mimeTypes:c.array(c.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});c.object({method:c.literal("ui/message"),params:c.object({role:c.literal("user").describe('Message role, currently only "user" is supported.'),content:c.array(Ct).describe("Message content blocks (text, image, etc.).")})});c.object({method:c.literal("ui/notifications/sandbox-resource-ready"),params:c.object({html:c.string().describe("HTML content to load into the inner iframe."),sandbox:c.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:fo.optional().describe("CSP configuration from resource metadata."),permissions:po.optional().describe("Sandbox permissions from resource metadata.")})});var SI=c.object({method:c.literal("ui/notifications/tool-result"),params:Li.describe("Standard MCP tool execution result.")}),Ef=c.object({toolInfo:c.object({id:Pt.optional().describe("JSON-RPC id of the tools/call request."),tool:hr.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:cI.optional().describe("Current color theme preference."),styles:_I.optional().describe("Style configuration for theming the app."),displayMode:Ut.optional().describe("How the UI is currently displayed."),availableDisplayModes:c.array(Ut).optional().describe("Display modes the host supports."),containerDimensions:c.union([c.object({height:c.number().describe("Fixed container height in pixels.")}),c.object({maxHeight:c.union([c.number(),c.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(c.union([c.object({width:c.number().describe("Fixed container width in pixels.")}),c.object({maxWidth:c.union([c.number(),c.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
|
|
93
|
-
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),xI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Ef.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ct).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ai.describe("App identification (name and version)."),appCapabilities:kI.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var zI=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ai.describe("Host application identification and version."),hostCapabilities:yI.describe("Features and capabilities provided by the host."),hostContext:Ef.describe("Rich context about the host environment.")}).passthrough();function ZI(e){let t=document.documentElement;t.setAttribute("data-theme",e),t.style.colorScheme=e}function UI(e,t=document.documentElement){for(let[n,r]of Object.entries(e))r!==void 0&&t.style.setProperty(n,r)}function OI(e){if(document.getElementById("__mcp-host-fonts"))return;let t=document.createElement("style");t.id="__mcp-host-fonts",t.textContent=e,document.head.appendChild(t)}class NI extends U_{constructor(n,r={},i={autoResize:!0}){super(i);se(this,"_appInfo");se(this,"_capabilities");se(this,"options");se(this,"_hostCapabilities");se(this,"_hostInfo");se(this,"_hostContext");se(this,"sendOpenLink",this.openLink);this._appInfo=n,this._capabilities=r,this.options=i,this.setRequestHandler(Ci,a=>(console.log("Received ping:",a.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(n){this.setNotificationHandler(vI,r=>n(r.params))}set ontoolinputpartial(n){this.setNotificationHandler(hI,r=>n(r.params))}set ontoolresult(n){this.setNotificationHandler(SI,r=>n(r.params))}set ontoolcancelled(n){this.setNotificationHandler(gI,r=>n(r.params))}set onhostcontextchanged(n){this.setNotificationHandler(xI,r=>{this._hostContext={...this._hostContext,...r.params},n(r.params)})}set onteardown(n){this.setRequestHandler(bI,(r,i)=>n(r.params,i))}set oncalltool(n){this.setRequestHandler(Ws,(r,i)=>n(r.params,i))}set onlisttools(n){this.setRequestHandler(qs,(r,i)=>n(r.params,i))}assertCapabilityForMethod(n){}assertRequestHandlerCapability(n){switch(n){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${n})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${n} registered`)}}assertNotificationCapability(n){}assertTaskCapability(n){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(n){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(n,r){return await this.request({method:"tools/call",params:n},Li,r)}sendMessage(n,r){return this.request({method:"ui/message",params:n},pI,r)}sendLog(n){return this.notification({method:"notifications/message",params:n})}updateModelContext(n,r){return this.request({method:"ui/update-model-context",params:n},tr,r)}openLink(n,r){return this.request({method:"ui/open-link",params:n},fI,r)}requestDisplayMode(n,r){return this.request({method:"ui/request-display-mode",params:n},II,r)}sendSizeChanged(n){return this.notification({method:"ui/notifications/size-changed",params:n})}setupSizeChangedNotifications(){let n=!1,r=0,i=0,a=()=>{n||(n=!0,requestAnimationFrame(()=>{n=!1;let s=document.documentElement,u=s.style.width,l=s.style.height;s.style.width="fit-content",s.style.height="fit-content";let m=s.getBoundingClientRect();s.style.width=u,s.style.height=l;let d=window.innerWidth-s.clientWidth,p=Math.ceil(m.width+d),g=Math.ceil(m.height);(p!==r||g!==i)&&(r=p,i=g,this.sendSizeChanged({width:p,height:g}))}))};a();let o=new ResizeObserver(a);return o.observe(document.documentElement),o.observe(document.body),()=>o.disconnect()}async connect(n=new N_(window.parent,window.parent),r){var i;await super.connect(n);try{let a=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:T_}},zI,r);if(a===void 0)throw Error(`Server sent invalid initialize result: ${a}`);this._hostCapabilities=a.hostCapabilities,this._hostInfo=a.hostInfo,this._hostContext=a.hostContext,await this.notification({method:"ui/notifications/initialized"}),(i=this.options)!=null&&i.autoResize&&this.setupSizeChangedNotifications()}catch(a){throw this.close(),a}}}const je=document.querySelector(".main"),Ee=document.getElementById("screenshot-container"),$e=document.getElementById("screenshot"),_t=document.getElementById("overlays"),Me=document.getElementById("action-status"),qe=document.getElementById("session-info"),vo=document.getElementById("loading-overlay"),TI=vo.querySelector(".loading-text"),rt=document.createElement("div");rt.id="target-info";rt.className="target-info hidden";let _i=0,qn=0,We=null;function PI(e){return e.structuredContent??{}}function ho(e="Waiting for screenshot..."){TI.textContent=e,vo.classList.remove("hidden")}function yt(){vo.classList.add("hidden")}function Df(e){var n,r,i;e.theme&&ZI(e.theme),(n=e.styles)!=null&&n.variables&&UI(e.styles.variables),(i=(r=e.styles)==null?void 0:r.css)!=null&&i.fonts&&OI(e.styles.css.fonts),e.safeAreaInsets&&(je.style.paddingTop=`${e.safeAreaInsets.top}px`,je.style.paddingRight=`${e.safeAreaInsets.right}px`,je.style.paddingBottom=`${e.safeAreaInsets.bottom}px`,je.style.paddingLeft=`${e.safeAreaInsets.left}px`);const t=e.containerDimensions;t&&("height"in t?(document.documentElement.style.height="100vh",je.style.height="100%"):"maxHeight"in t&&t.maxHeight&&(document.documentElement.style.maxHeight=`${t.maxHeight}px`,je.style.maxHeight="100%"),"width"in t?(document.documentElement.style.width="100vw",je.style.width="100%"):"maxWidth"in t&&t.maxWidth&&(document.documentElement.style.maxWidth=`${t.maxWidth}px`,je.style.maxWidth="100%"))}function is(e,t,n){return t===0?e:e/t*n}function jI(){Ee.style.transform="none",Ee.classList.remove("zoomed")}function Rf(e){requestAnimationFrame(()=>{var i,a;_t.innerHTML="";const t=$e.clientWidth,n=$e.clientHeight;if(console.info("addOverlays:",{action:e.action,hasElement:!!e.element,hasClickPosition:!!e.clickPosition,displayedWidth:t,displayedHeight:n,naturalWidth:_i,naturalHeight:qn}),t===0||n===0){console.warn("addOverlays: Dimensions not ready, retrying..."),setTimeout(()=>Rf(e),50);return}if((e.action==="find"||e.action==="find_and_click"||e.action==="findall")&&e.element){const o=document.createElement("div");o.className="element-target",o.style.left=`${t/2}px`,o.style.top=`${n/2}px`;const s=document.createElement("div");s.className="crosshair-h",o.appendChild(s);const u=document.createElement("div");u.className="crosshair-v",o.appendChild(u);const l=document.createElement("div");l.className="element-label",l.textContent=((i=e.element)==null?void 0:i.description)||"Element",(a=e.element)!=null&&a.confidence&&(l.textContent+=` (${Math.round(e.element.confidence*100)}%)`),o.appendChild(l),_t.appendChild(o),console.info("addOverlays: Added element target at center")}if(e.clickPosition){const o=e.clickPosition.centerX??e.clickPosition.x,s=e.clickPosition.centerY??e.clickPosition.y;if(o!==void 0&&s!==void 0&&_i>0){const u=document.createElement("div");u.className="click-marker";const l=is(o,_i,t),m=is(s,qn,n);u.style.left=`${l}px`,u.style.top=`${m}px`;const d=document.createElement("div");d.className="click-ripple",u.appendChild(d),_t.appendChild(u),console.info("addOverlays: Added click marker at",{clickX:o,clickY:s,scaledX:l,scaledY:m})}}if(e.scrollDirection){const o=document.createElement("div");o.className=`scroll-indicator scroll-${e.scrollDirection}`,o.textContent=e.scrollDirection==="up"?"↑":e.scrollDirection==="down"?"↓":e.scrollDirection==="left"?"←":"→",_t.appendChild(o)}jI(),delete Ee.dataset.focalX,delete Ee.dataset.focalY})}function EI(e){_t.innerHTML="";const t=e.action||"unknown",n=e.success?"✓":"✗",r=e.success?"success":"error";let i=`${n} ${t}`;if(e.duration&&(i+=` (${e.duration}ms)`),e.assertion&&(i+=`: "${e.assertion}"`),e.text&&e.action==="type"&&(i+=`: "${e.text}"`),e.error&&(i+=` - ${e.error}`),Me.textContent=i,Me.className=r,e.debuggerUrl&&(We=e.debuggerUrl),e.session){const a=e.session.expiresIn?Math.round(e.session.expiresIn/1e3):0;if(qe.innerHTML="",We){const o=document.createElement("a");o.href=We,o.target="_blank",o.rel="noopener noreferrer",o.textContent=`${a}s remaining`,o.className="debugger-link",o.title=`Open debugger: ${We}`,qe.appendChild(o)}else qe.textContent=`${a}s remaining`;qe.className=a<30?"warning":""}else if(We){qe.innerHTML="";const a=document.createElement("a");a.href=We,a.target="_blank",a.rel="noopener noreferrer",a.textContent="Open Debugger",a.className="debugger-link",a.title=We,qe.appendChild(a)}else e.action==="session_start"&&(qe.textContent="Session started");if(e.element&&(e.action==="find"||e.action==="find_and_click")){const a=e.element;let o=`<strong>Target:</strong> "${a.description||"Element"}"`;if(a.centerX!==void 0&&a.centerY!==void 0&&(o+=` <span class="target-coords">(${Math.round(a.centerX)}, ${Math.round(a.centerY)})</span>`),a.confidence!==void 0){const s=Math.round(a.confidence*100);o+=` <span class="target-confidence ${s>=70?"high":s>=40?"medium":"low"}">${s}%</span>`}a.ref&&(o+=` <span class="target-ref">ref: ${a.ref}</span>`),rt.innerHTML=o,rt.classList.remove("hidden")}else rt.classList.add("hidden");e.imageUrl?(ho("Loading image..."),$e.onerror=()=>{console.error("Image failed to load"),$e.alt="Image failed to load",Ee.style.display="none",yt()},$e.onload=()=>{console.info("Image loaded:",$e.naturalWidth,"x",$e.naturalHeight),_i=$e.naturalWidth,qn=$e.naturalHeight,Ee.style.display="block",Rf(e),yt()},$e.src=e.imageUrl,$e.style.display="block"):($e.style.display="none",Ee.style.display="none",yt())}const Oe=new NI({name:"TestDriver Screenshot",version:"1.0.0"});async function DI(e){try{console.info("Fetching screenshot from resource:",e);const n=(await Oe.request({method:"resources/read",params:{uri:e}},Js)).contents[0];if(!n||!("blob"in n))return console.error("Resource did not contain blob data"),null;const r=`data:${n.mimeType||"image/png"};base64,${n.blob}`;return console.info("Screenshot fetched successfully, blob length:",n.blob.length),r}catch(t){return console.error("Failed to fetch screenshot resource:",t),null}}Oe.onteardown=async()=>(console.info("TestDriver app being torn down"),{});Oe.ontoolinput=e=>{console.info("Received tool input:",e),Me.textContent="Running action...",Me.className="loading",Ee.style.display="none",ho("Running action...")};Oe.ontoolresult=async e=>{console.info("Received tool result:",e),console.info("structuredContent:",e.structuredContent);const t=PI(e);console.info("Extracted data keys:",Object.keys(t)),console.info("Has imageUrl:",!!t.imageUrl),console.info("Has screenshotResourceUri:",!!t.screenshotResourceUri),console.info("Has croppedImageResourceUri:",!!t.croppedImageResourceUri);const n=t.screenshotResourceUri||t.croppedImageResourceUri;if(n&&!t.imageUrl){ho("Fetching image...");const r=await DI(n);r&&(t.imageUrl=r)}EI(t)};Oe.ontoolcancelled=e=>{console.info("Tool cancelled:",e.reason),Me.textContent=`Cancelled: ${e.reason}`,Me.className="error",yt()};Oe.onerror=e=>{console.error("App error:",e),Me.textContent=`Error: ${e}`,Me.className="error",yt()};Oe.onhostcontextchanged=Df;Oe.connect().then(()=>{const e=Oe.getHostContext();e&&Df(e)});const $i=document.querySelector(".screenshot-wrapper");$i&&$i.parentNode&&$i.parentNode.insertBefore(rt,$i.nextSibling);</script>
|
|
94
|
-
<style rel="stylesheet" crossorigin>*{box-sizing:border-box;margin:0;padding:0}html,body{font-family:var(--font-sans, system-ui, -apple-system, sans-serif);font-size:var(--font-text-sm-size, 14px);line-height:var(--font-text-sm-line-height, 1.5);background:var(--color-background-primary, #fff);color:var(--color-text-primary, #1a1a1a);overflow:hidden;margin:0;padding:0}body{height:100%;display:flex;flex-direction:column}.main{width:100%;max-width:100%;display:flex;flex-direction:column;overflow:hidden}.screenshot-wrapper{position:relative;width:100%;flex:1;min-height:0;background:var(--color-background-secondary, #f5f5f5);border-radius:var(--border-radius-md, 8px);overflow:hidden;display:flex;flex-direction:column}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px;flex:1;min-height:100px;background:var(--color-background-secondary, #f5f5f5)}.loading-overlay.hidden{display:none}.loading-spinner{width:32px;height:32px;border:3px solid var(--color-border-primary, #e5e5e5);border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:var(--font-text-sm-size, 14px);color:var(--color-text-secondary, #666)}.screenshot-container{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center;justify-content:center}#screenshot{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}#overlays{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.element-target{position:absolute;width:24px;height:24px;margin-left:-12px;margin-top:-12px;background:#3b82f64d;border:3px solid #3b82f6;border-radius:50%;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d;animation:target-pulse 1.5s ease-in-out infinite}@keyframes target-pulse{0%,to{transform:scale(1);border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d}50%{transform:scale(1.1);border-color:#60a5fa;box-shadow:0 0 0 2px #fff,0 0 12px #3b82f680}}.crosshair-h,.crosshair-v{position:absolute;background:#3b82f6}.crosshair-h{width:40px;height:2px;left:50%;top:50%;margin-left:-20px;margin-top:-1px}.crosshair-v{width:2px;height:40px;left:50%;top:50%;margin-left:-1px;margin-top:-20px}.element-label{position:absolute;top:100%;left:50%;transform:translate(-50%);margin-top:24px;background:#3b82f6;color:#fff;font-size:11px;padding:4px 8px;border-radius:4px;white-space:nowrap;max-width:300px;overflow:hidden;text-overflow:ellipsis;box-shadow:0 2px 8px #0003}.click-marker{position:absolute;width:16px;height:16px;margin-left:-8px;margin-top:-8px;background:#ef4444;border:2px solid white;border-radius:50%;box-shadow:0 2px 8px #0000004d}.click-ripple{position:absolute;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px;border:2px solid #ef4444;border-radius:50%;animation:ripple 1s ease-out infinite}@keyframes ripple{0%{transform:scale(.5);opacity:1}to{transform:scale(2);opacity:0}}.scroll-indicator{position:absolute;font-size:48px;color:#3b82f6;text-shadow:0 2px 8px rgba(0,0,0,.3);animation:scroll-bounce .5s ease-in-out}.scroll-up{top:20px;left:50%;transform:translate(-50%)}.scroll-down{bottom:20px;left:50%;transform:translate(-50%)}.scroll-left{left:20px;top:50%;transform:translateY(-50%)}.scroll-right{right:20px;top:50%;transform:translateY(-50%)}@keyframes scroll-bounce{0%,to{opacity:1}50%{opacity:.5}}.status-bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-top:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-xs-size, 12px)}#action-status{font-weight:var(--font-weight-medium, 500)}#action-status.success{color:#10b981}#action-status.error{color:#ef4444}#action-status.loading,#session-info{color:var(--color-text-secondary, #666)}#session-info.warning{color:#f59e0b}.debugger-link{color:#3b82f6;text-decoration:none;transition:color .15s ease}.debugger-link:hover{color:#2563eb;text-decoration:underline}.target-info{padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-bottom:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-sm-size, 14px);color:var(--color-text-primary, #1a1a1a)}.target-info.hidden{display:none}.target-info strong{color:#3b82f6;font-weight:var(--font-weight-medium, 500)}.target-coords{color:var(--color-text-secondary, #666);font-family:var(--font-mono, ui-monospace, monospace);font-size:var(--font-text-xs-size, 12px);margin-left:4px}.target-confidence{display:inline-block;padding:1px 6px;border-radius:10px;font-size:var(--font-text-xs-size, 11px);font-weight:var(--font-weight-medium, 500);margin-left:6px}.target-confidence.high{background:#dcfce7;color:#166534}.target-confidence.medium{background:#fef3c7;color:#92400e}.target-confidence.low{background:#fee2e2;color:#991b1b}
|
|
93
|
+
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),xI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Ef.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ct).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ai.describe("App identification (name and version)."),appCapabilities:kI.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var zI=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ai.describe("Host application identification and version."),hostCapabilities:yI.describe("Features and capabilities provided by the host."),hostContext:Ef.describe("Rich context about the host environment.")}).passthrough();function ZI(e){let t=document.documentElement;t.setAttribute("data-theme",e),t.style.colorScheme=e}function UI(e,t=document.documentElement){for(let[n,r]of Object.entries(e))r!==void 0&&t.style.setProperty(n,r)}function OI(e){if(document.getElementById("__mcp-host-fonts"))return;let t=document.createElement("style");t.id="__mcp-host-fonts",t.textContent=e,document.head.appendChild(t)}class NI extends U_{constructor(n,r={},i={autoResize:!0}){super(i);se(this,"_appInfo");se(this,"_capabilities");se(this,"options");se(this,"_hostCapabilities");se(this,"_hostInfo");se(this,"_hostContext");se(this,"sendOpenLink",this.openLink);this._appInfo=n,this._capabilities=r,this.options=i,this.setRequestHandler(Ci,a=>(console.log("Received ping:",a.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(n){this.setNotificationHandler(vI,r=>n(r.params))}set ontoolinputpartial(n){this.setNotificationHandler(hI,r=>n(r.params))}set ontoolresult(n){this.setNotificationHandler(SI,r=>n(r.params))}set ontoolcancelled(n){this.setNotificationHandler(gI,r=>n(r.params))}set onhostcontextchanged(n){this.setNotificationHandler(xI,r=>{this._hostContext={...this._hostContext,...r.params},n(r.params)})}set onteardown(n){this.setRequestHandler(bI,(r,i)=>n(r.params,i))}set oncalltool(n){this.setRequestHandler(Ws,(r,i)=>n(r.params,i))}set onlisttools(n){this.setRequestHandler(qs,(r,i)=>n(r.params,i))}assertCapabilityForMethod(n){}assertRequestHandlerCapability(n){switch(n){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${n})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${n} registered`)}}assertNotificationCapability(n){}assertTaskCapability(n){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(n){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(n,r){return await this.request({method:"tools/call",params:n},Li,r)}sendMessage(n,r){return this.request({method:"ui/message",params:n},pI,r)}sendLog(n){return this.notification({method:"notifications/message",params:n})}updateModelContext(n,r){return this.request({method:"ui/update-model-context",params:n},tr,r)}openLink(n,r){return this.request({method:"ui/open-link",params:n},fI,r)}requestDisplayMode(n,r){return this.request({method:"ui/request-display-mode",params:n},II,r)}sendSizeChanged(n){return this.notification({method:"ui/notifications/size-changed",params:n})}setupSizeChangedNotifications(){let n=!1,r=0,i=0,a=()=>{n||(n=!0,requestAnimationFrame(()=>{n=!1;let s=document.documentElement,u=s.style.width,l=s.style.height;s.style.width="fit-content",s.style.height="fit-content";let m=s.getBoundingClientRect();s.style.width=u,s.style.height=l;let d=window.innerWidth-s.clientWidth,p=Math.ceil(m.width+d),g=Math.ceil(m.height);(p!==r||g!==i)&&(r=p,i=g,this.sendSizeChanged({width:p,height:g}))}))};a();let o=new ResizeObserver(a);return o.observe(document.documentElement),o.observe(document.body),()=>o.disconnect()}async connect(n=new N_(window.parent,window.parent),r){var i;await super.connect(n);try{let a=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:T_}},zI,r);if(a===void 0)throw Error(`Server sent invalid initialize result: ${a}`);this._hostCapabilities=a.hostCapabilities,this._hostInfo=a.hostInfo,this._hostContext=a.hostContext,await this.notification({method:"ui/notifications/initialized"}),(i=this.options)!=null&&i.autoResize&&this.setupSizeChangedNotifications()}catch(a){throw this.close(),a}}}const je=document.querySelector(".main"),Ee=document.getElementById("screenshot-container"),$e=document.getElementById("screenshot"),_t=document.getElementById("overlays"),Me=document.getElementById("action-status"),qe=document.getElementById("session-info"),vo=document.getElementById("loading-overlay"),TI=vo.querySelector(".loading-text"),rt=document.createElement("div");rt.id="target-info";rt.className="target-info hidden";let _i=0,qn=0,We=null;function PI(e){return e.structuredContent??{}}function ho(e="Waiting for screenshot..."){TI.textContent=e,vo.classList.remove("hidden")}function yt(){vo.classList.add("hidden")}function Df(e){var n,r,i;e.theme&&ZI(e.theme),(n=e.styles)!=null&&n.variables&&UI(e.styles.variables),(i=(r=e.styles)==null?void 0:r.css)!=null&&i.fonts&&OI(e.styles.css.fonts),e.safeAreaInsets&&(je.style.paddingTop=`${e.safeAreaInsets.top}px`,je.style.paddingRight=`${e.safeAreaInsets.right}px`,je.style.paddingBottom=`${e.safeAreaInsets.bottom}px`,je.style.paddingLeft=`${e.safeAreaInsets.left}px`);const t=e.containerDimensions;t&&("height"in t?(document.documentElement.style.height="100vh",je.style.height="100%"):"maxHeight"in t&&t.maxHeight&&(document.documentElement.style.maxHeight=`${t.maxHeight}px`,je.style.maxHeight="100%"),"width"in t?(document.documentElement.style.width="100vw",je.style.width="100%"):"maxWidth"in t&&t.maxWidth&&(document.documentElement.style.maxWidth=`${t.maxWidth}px`,je.style.maxWidth="100%"))}function is(e,t,n){return t===0?e:e/t*n}function jI(){Ee.style.transform="none",Ee.classList.remove("zoomed")}function Rf(e){requestAnimationFrame(()=>{var i,a;_t.innerHTML="";const t=$e.clientWidth,n=$e.clientHeight;if(console.info("addOverlays:",{action:e.action,hasElement:!!e.element,hasClickPosition:!!e.clickPosition,displayedWidth:t,displayedHeight:n,naturalWidth:_i,naturalHeight:qn}),t===0||n===0){console.warn("addOverlays: Dimensions not ready, retrying..."),setTimeout(()=>Rf(e),50);return}if((e.action==="find"||e.action==="find_and_click"||e.action==="findall")&&e.element){const o=document.createElement("div");o.className="element-target",o.style.left=`${t/2}px`,o.style.top=`${n/2}px`;const s=document.createElement("div");s.className="crosshair-h",o.appendChild(s);const u=document.createElement("div");u.className="crosshair-v",o.appendChild(u);const l=document.createElement("div");l.className="element-label",l.textContent=((i=e.element)==null?void 0:i.description)||"Element",(a=e.element)!=null&&a.confidence&&(l.textContent+=` (${Math.round(e.element.confidence*100)}%)`),o.appendChild(l),_t.appendChild(o),console.info("addOverlays: Added element target at center")}if(e.clickPosition){const o=e.clickPosition.centerX??e.clickPosition.x,s=e.clickPosition.centerY??e.clickPosition.y;if(o!==void 0&&s!==void 0&&_i>0){const u=document.createElement("div");u.className="click-marker";const l=is(o,_i,t),m=is(s,qn,n);u.style.left=`${l}px`,u.style.top=`${m}px`;const d=document.createElement("div");d.className="click-ripple",u.appendChild(d),_t.appendChild(u),console.info("addOverlays: Added click marker at",{clickX:o,clickY:s,scaledX:l,scaledY:m})}}if(e.scrollDirection){const o=document.createElement("div");o.className=`scroll-indicator scroll-${e.scrollDirection}`,o.textContent=e.scrollDirection==="up"?"↑":e.scrollDirection==="down"?"↓":e.scrollDirection==="left"?"←":"→",_t.appendChild(o)}jI(),delete Ee.dataset.focalX,delete Ee.dataset.focalY})}function EI(e){_t.innerHTML="";const t=e.action||"unknown",n=e.success?"✓":"✗",r=e.success?"success":"error";let i=`${n} ${t}`;if(e.duration&&(i+=` (${e.duration}ms)`),e.assertion&&(i+=`: "${e.assertion}"`),e.text&&e.action==="type"&&(i+=`: "${e.text}"`),e.error&&(i+=` - ${e.error}`),Me.textContent=i,Me.className=r,e.debuggerUrl&&(We=e.debuggerUrl),e.session){const a=e.session.expiresIn?Math.round(e.session.expiresIn/1e3):0;if(qe.innerHTML="",We){const o=document.createElement("a");o.href=We,o.target="_blank",o.rel="noopener noreferrer",o.textContent=`${a}s remaining`,o.className="debugger-link",o.title=`Open debugger: ${We}`,qe.appendChild(o)}else qe.textContent=`${a}s remaining`;qe.className=a<30?"warning":""}else if(We){qe.innerHTML="";const a=document.createElement("a");a.href=We,a.target="_blank",a.rel="noopener noreferrer",a.textContent="Open Debugger",a.className="debugger-link",a.title=We,qe.appendChild(a)}else e.action==="session_start"&&(qe.textContent="Session started");if(e.element&&(e.action==="find"||e.action==="find_and_click")){const a=e.element;let o=`<strong>Target:</strong> "${a.description||"Element"}"`;if(a.centerX!==void 0&&a.centerY!==void 0&&(o+=` <span class="target-coords">(${Math.round(a.centerX)}, ${Math.round(a.centerY)})</span>`),a.confidence!==void 0){const s=Math.round(a.confidence*100);o+=` <span class="target-confidence ${s>=70?"high":s>=40?"medium":"low"}">${s}%</span>`}rt.innerHTML=o,rt.classList.remove("hidden")}else rt.classList.add("hidden");e.imageUrl?(ho("Loading image..."),$e.onerror=()=>{console.error("Image failed to load"),$e.alt="Image failed to load",Ee.style.display="none",yt()},$e.onload=()=>{console.info("Image loaded:",$e.naturalWidth,"x",$e.naturalHeight),_i=$e.naturalWidth,qn=$e.naturalHeight,Ee.style.display="block",Rf(e),yt()},$e.src=e.imageUrl,$e.style.display="block"):($e.style.display="none",Ee.style.display="none",yt())}const Oe=new NI({name:"TestDriver Screenshot",version:"1.0.0"});async function DI(e){try{console.info("Fetching screenshot from resource:",e);const n=(await Oe.request({method:"resources/read",params:{uri:e}},Js)).contents[0];if(!n||!("blob"in n))return console.error("Resource did not contain blob data"),null;const r=`data:${n.mimeType||"image/png"};base64,${n.blob}`;return console.info("Screenshot fetched successfully, blob length:",n.blob.length),r}catch(t){return console.error("Failed to fetch screenshot resource:",t),null}}Oe.onteardown=async()=>(console.info("TestDriver app being torn down"),{});Oe.ontoolinput=e=>{console.info("Received tool input:",e);const t=e.arguments,n=[];t&&(t.description&&n.push(`"${t.description}"`),t.text&&n.push(`"${t.text}"`),t.url&&n.push(`${t.url}`),t.direction&&n.push(`${t.direction}`),t.assertion&&n.push(`"${t.assertion}"`),t.task&&n.push(`"${t.task}"`),t.keys&&n.push(`[${t.keys.join("+")}]`),t.type&&n.push(`${t.type}`));const r=n.length>0?n.join(" "):"action";Me.textContent=`Running ${r}...`,Me.className="loading",Ee.style.display="none",ho(`Running ${r}...`)};Oe.ontoolresult=async e=>{console.info("Received tool result:",e),console.info("structuredContent:",e.structuredContent);const t=PI(e);console.info("Extracted data keys:",Object.keys(t)),console.info("Has imageUrl:",!!t.imageUrl),console.info("Has screenshotResourceUri:",!!t.screenshotResourceUri),console.info("Has croppedImageResourceUri:",!!t.croppedImageResourceUri);const n=t.screenshotResourceUri||t.croppedImageResourceUri;if(n&&!t.imageUrl){ho("Fetching image...");const r=await DI(n);r&&(t.imageUrl=r)}EI(t)};Oe.ontoolcancelled=e=>{console.info("Tool cancelled:",e.reason),Me.textContent=`Cancelled: ${e.reason}`,Me.className="error",yt()};Oe.onerror=e=>{console.error("App error:",e),Me.textContent=`Error: ${e}`,Me.className="error",yt()};Oe.onhostcontextchanged=Df;Oe.connect().then(()=>{const e=Oe.getHostContext();e&&Df(e)});const $i=document.querySelector(".screenshot-wrapper");$i&&$i.parentNode&&$i.parentNode.insertBefore(rt,$i.nextSibling);</script>
|
|
94
|
+
<style rel="stylesheet" crossorigin>*{box-sizing:border-box;margin:0;padding:0}html,body{font-family:var(--font-sans, system-ui, -apple-system, sans-serif);font-size:var(--font-text-sm-size, 14px);line-height:var(--font-text-sm-line-height, 1.5);background:var(--color-background-primary, #fff);color:var(--color-text-primary, #1a1a1a);overflow:hidden;margin:0;padding:0}body{height:100%;display:flex;flex-direction:column}.main{width:100%;max-width:100%;display:flex;flex-direction:column;overflow:hidden}.screenshot-wrapper{position:relative;width:100%;flex:1;min-height:0;background:var(--color-background-secondary, #f5f5f5);border-radius:var(--border-radius-md, 8px);overflow:hidden;display:flex;flex-direction:column}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px;flex:1;min-height:100px;background:var(--color-background-secondary, #f5f5f5)}.loading-overlay.hidden{display:none}.loading-spinner{width:32px;height:32px;border:3px solid var(--color-border-primary, #e5e5e5);border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:var(--font-text-sm-size, 14px);color:var(--color-text-secondary, #666)}.screenshot-container{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center;justify-content:center}#screenshot{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}#overlays{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.element-target{position:absolute;width:24px;height:24px;margin-left:-12px;margin-top:-12px;background:#3b82f64d;border:3px solid #3b82f6;border-radius:50%;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d;animation:target-pulse 1.5s ease-in-out infinite}@keyframes target-pulse{0%,to{transform:scale(1);border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d}50%{transform:scale(1.1);border-color:#60a5fa;box-shadow:0 0 0 2px #fff,0 0 12px #3b82f680}}.crosshair-h,.crosshair-v{position:absolute;background:#3b82f6}.crosshair-h{width:40px;height:2px;left:50%;top:50%;margin-left:-20px;margin-top:-1px}.crosshair-v{width:2px;height:40px;left:50%;top:50%;margin-left:-1px;margin-top:-20px}.element-label{position:absolute;top:100%;left:50%;transform:translate(-50%);margin-top:24px;background:#3b82f6;color:#fff;font-size:11px;padding:4px 8px;border-radius:4px;white-space:nowrap;max-width:300px;overflow:hidden;text-overflow:ellipsis;box-shadow:0 2px 8px #0003}.click-marker{position:absolute;width:16px;height:16px;margin-left:-8px;margin-top:-8px;background:#ef4444;border:2px solid white;border-radius:50%;box-shadow:0 2px 8px #0000004d}.click-ripple{position:absolute;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px;border:2px solid #ef4444;border-radius:50%;animation:ripple 1s ease-out infinite}@keyframes ripple{0%{transform:scale(.5);opacity:1}to{transform:scale(2);opacity:0}}.scroll-indicator{position:absolute;font-size:48px;color:#3b82f6;text-shadow:0 2px 8px rgba(0,0,0,.3);animation:scroll-bounce .5s ease-in-out}.scroll-up{top:20px;left:50%;transform:translate(-50%)}.scroll-down{bottom:20px;left:50%;transform:translate(-50%)}.scroll-left{left:20px;top:50%;transform:translateY(-50%)}.scroll-right{right:20px;top:50%;transform:translateY(-50%)}@keyframes scroll-bounce{0%,to{opacity:1}50%{opacity:.5}}.status-bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-top:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-xs-size, 12px)}#action-status{font-weight:var(--font-weight-medium, 500)}#action-status.success{color:#10b981}#action-status.error{color:#ef4444}#action-status.loading,#session-info{color:var(--color-text-secondary, #666)}#session-info.warning{color:#f59e0b}.debugger-link{color:#3b82f6;text-decoration:none;transition:color .15s ease}.debugger-link:hover{color:#2563eb;text-decoration:underline}.target-info{padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-bottom:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-sm-size, 14px);color:var(--color-text-primary, #1a1a1a)}.target-info.hidden{display:none}.target-info strong{color:#3b82f6;font-weight:var(--font-weight-medium, 500)}.target-coords{color:var(--color-text-secondary, #666);font-family:var(--font-mono, ui-monospace, monospace);font-size:var(--font-text-xs-size, 12px);margin-left:4px}.target-confidence{display:inline-block;padding:1px 6px;border-radius:10px;font-size:var(--font-text-xs-size, 11px);font-weight:var(--font-weight-medium, 500);margin-left:6px}.target-confidence.high{background:#dcfce7;color:#166534}.target-confidence.medium{background:#fef3c7;color:#92400e}.target-confidence.low{background:#fee2e2;color:#991b1b}@media(prefers-color-scheme:dark){html:not([data-theme=light]) .element-target{border-color:#60a5fa;background:#60a5fa33;box-shadow:0 0 0 2px #1f2937,0 2px 8px #00000080}html:not([data-theme=light]) .crosshair-h,html:not([data-theme=light]) .crosshair-v{background:#60a5fa}html:not([data-theme=light]) .element-label{background:#2563eb}html:not([data-theme=light]) .click-marker{background:#f87171;border-color:#1f2937}html:not([data-theme=light]) .click-ripple{border-color:#f87171}html:not([data-theme=light]) .scroll-indicator,html:not([data-theme=light]) .debugger-link{color:#60a5fa}html:not([data-theme=light]) .debugger-link:hover{color:#93c5fd}html:not([data-theme=light]) .target-info strong{color:#60a5fa}html:not([data-theme=light]) .target-confidence.high{background:#166534;color:#dcfce7}html:not([data-theme=light]) .target-confidence.medium{background:#92400e;color:#fef3c7}html:not([data-theme=light]) .target-confidence.low{background:#991b1b;color:#fee2e2}}</style>
|
|
95
95
|
</head>
|
|
96
96
|
<body>
|
|
97
97
|
<main class="main">
|
|
@@ -150,7 +150,7 @@ export declare const SessionStartInputSchema: z.ZodObject<{
|
|
|
150
150
|
os: z.ZodDefault<z.ZodEnum<["linux", "windows"]>>;
|
|
151
151
|
/** Keep sandbox alive duration in ms (default: 5 minutes) */
|
|
152
152
|
keepAlive: z.ZodDefault<z.ZodNumber>;
|
|
153
|
-
/** Path to test file
|
|
153
|
+
/** Path to test file - when provided, you MUST append generated code to this file after each action */
|
|
154
154
|
testFile: z.ZodOptional<z.ZodString>;
|
|
155
155
|
/** Reconnect to last sandbox */
|
|
156
156
|
reconnect: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -121,8 +121,8 @@ export const SessionStartInputSchema = z.object({
|
|
|
121
121
|
os: z.enum(["linux", "windows"]).default("linux").describe("Sandbox OS"),
|
|
122
122
|
/** Keep sandbox alive duration in ms (default: 5 minutes) */
|
|
123
123
|
keepAlive: z.number().default(300000).describe("Keep sandbox alive for this many ms"),
|
|
124
|
-
/** Path to test file
|
|
125
|
-
testFile: z.string().optional().describe("Path to test file
|
|
124
|
+
/** Path to test file - when provided, you MUST append generated code to this file after each action */
|
|
125
|
+
testFile: z.string().optional().describe("Path to test file. When provided, append generated code from each action to this file immediately."),
|
|
126
126
|
/** Reconnect to last sandbox */
|
|
127
127
|
reconnect: z.boolean().default(false).describe("Reconnect to last sandbox"),
|
|
128
128
|
/** API endpoint URL */
|
|
@@ -226,14 +226,22 @@ function requireActiveSession() {
|
|
|
226
226
|
* Images: imageUrl (data URL) goes to structuredContent for UI to display
|
|
227
227
|
* The croppedImage from find() is small (~10KB) so it's acceptable as data URL
|
|
228
228
|
*
|
|
229
|
-
* If generatedCode is provided, it's appended to the text response
|
|
230
|
-
*
|
|
229
|
+
* If generatedCode is provided, it's appended to the text response with instructions
|
|
230
|
+
* for the agent to write it to the test file.
|
|
231
231
|
*/
|
|
232
232
|
function createToolResult(success, textContent, structuredData, generatedCode) {
|
|
233
|
-
// Build text content - append generated code if provided
|
|
233
|
+
// Build text content - append generated code if provided with directive instructions
|
|
234
234
|
let fullText = textContent;
|
|
235
235
|
if (generatedCode && success) {
|
|
236
|
-
|
|
236
|
+
// Get the test file from the current session
|
|
237
|
+
const session = sessionManager.getCurrentSession();
|
|
238
|
+
const testFile = session?.testFile;
|
|
239
|
+
if (testFile) {
|
|
240
|
+
fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to ${testFile}:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to the test file:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
|
|
244
|
+
}
|
|
237
245
|
}
|
|
238
246
|
const content = [{ type: "text", text: fullText }];
|
|
239
247
|
logger.debug("createToolResult", {
|
|
@@ -244,9 +252,17 @@ function createToolResult(success, textContent, structuredData, generatedCode) {
|
|
|
244
252
|
hasGeneratedCode: !!generatedCode
|
|
245
253
|
});
|
|
246
254
|
// structuredContent goes to UI (includes imageUrl for display)
|
|
255
|
+
// Always include success flag so UI can display correct status indicator
|
|
256
|
+
// Include generatedCode and testFile in structured data so agents can programmatically handle it
|
|
257
|
+
const session = sessionManager.getCurrentSession();
|
|
247
258
|
return {
|
|
248
259
|
content,
|
|
249
|
-
structuredContent:
|
|
260
|
+
structuredContent: {
|
|
261
|
+
...structuredData,
|
|
262
|
+
success,
|
|
263
|
+
generatedCode: generatedCode && success ? generatedCode : undefined,
|
|
264
|
+
testFile: session?.testFile || undefined,
|
|
265
|
+
},
|
|
250
266
|
};
|
|
251
267
|
}
|
|
252
268
|
// Create MCP server
|
|
@@ -322,6 +338,10 @@ registerAppTool(server, "session_start", {
|
|
|
322
338
|
title: "Session Start",
|
|
323
339
|
description: `Start a new TestDriver session and provision a sandbox with browser or app.
|
|
324
340
|
|
|
341
|
+
⚠️ IMPORTANT - Test File Parameter:
|
|
342
|
+
When 'testFile' is provided, you MUST append the generated code to that file after EVERY successful action.
|
|
343
|
+
Each tool response includes "ACTION REQUIRED: Append this code..." - you must write that code to the test file immediately.
|
|
344
|
+
|
|
325
345
|
Provision types:
|
|
326
346
|
- chrome: Launch Chrome browser (default). Use 'url' for starting page.
|
|
327
347
|
- chromeExtension: Launch Chrome with an extension. Use 'extensionPath' or 'extensionId'.
|
|
@@ -1369,50 +1389,6 @@ Only use 'screenshot' when you explicitly want to show something to the human us
|
|
|
1369
1389
|
return createToolResult(false, `Screenshot failed: ${error}`, { error: String(error) });
|
|
1370
1390
|
}
|
|
1371
1391
|
});
|
|
1372
|
-
// Verify
|
|
1373
|
-
server.registerTool("verify", {
|
|
1374
|
-
description: "Run the test file from scratch to verify it works",
|
|
1375
|
-
inputSchema: z.object({
|
|
1376
|
-
testFile: z.string().describe("Path to test file to run"),
|
|
1377
|
-
}),
|
|
1378
|
-
}, async (params) => {
|
|
1379
|
-
const startTime = Date.now();
|
|
1380
|
-
logger.info("verify: Starting", { testFile: params.testFile });
|
|
1381
|
-
const session = sessionManager.getCurrentSession();
|
|
1382
|
-
if (!fs.existsSync(params.testFile)) {
|
|
1383
|
-
logger.warn("verify: Test file not found", { testFile: params.testFile });
|
|
1384
|
-
return createToolResult(false, `Test file not found: ${params.testFile}`, { error: "Test file not found" });
|
|
1385
|
-
}
|
|
1386
|
-
const { execSync } = await import("child_process");
|
|
1387
|
-
try {
|
|
1388
|
-
logger.info("verify: Running vitest", { testFile: params.testFile });
|
|
1389
|
-
const output = execSync(`npx vitest run "${params.testFile}" --reporter=verbose`, {
|
|
1390
|
-
encoding: "utf-8",
|
|
1391
|
-
timeout: 300000,
|
|
1392
|
-
cwd: process.cwd(),
|
|
1393
|
-
env: { ...process.env },
|
|
1394
|
-
});
|
|
1395
|
-
const duration = Date.now() - startTime;
|
|
1396
|
-
logger.info("verify: Test passed", { testFile: params.testFile, duration });
|
|
1397
|
-
return createToolResult(true, `✓ Test passed!\n\n${output}`, {
|
|
1398
|
-
action: "verify",
|
|
1399
|
-
success: true,
|
|
1400
|
-
session: getSessionData(session),
|
|
1401
|
-
duration,
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
catch (error) {
|
|
1405
|
-
const duration = Date.now() - startTime;
|
|
1406
|
-
logger.error("verify: Test failed", { testFile: params.testFile, error: error.message, duration });
|
|
1407
|
-
return createToolResult(false, `✗ Test failed!\n\n${error.stdout || error.message}`, {
|
|
1408
|
-
action: "verify",
|
|
1409
|
-
success: false,
|
|
1410
|
-
error: error.stdout || error.message,
|
|
1411
|
-
session: getSessionData(session),
|
|
1412
|
-
duration,
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
1392
|
// Start the server
|
|
1417
1393
|
async function main() {
|
|
1418
1394
|
logger.info("Starting TestDriver MCP Server", {
|
package/package.json
CHANGED
package/sdk.d.ts
CHANGED
|
@@ -867,7 +867,22 @@ 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
|
+
* // Or pass API key explicitly
|
|
883
|
+
* const client = new TestDriver('your-api-key');
|
|
884
|
+
*/
|
|
885
|
+
constructor(apiKey?: string, options?: TestDriverOptions);
|
|
871
886
|
|
|
872
887
|
/**
|
|
873
888
|
* Whether the SDK is currently connected to a sandbox
|
package/sdk.js
CHANGED
|
@@ -5,6 +5,9 @@ const crypto = require("crypto");
|
|
|
5
5
|
const { formatter } = require("./sdk-log-formatter");
|
|
6
6
|
const logger = require("./agent/lib/logger");
|
|
7
7
|
|
|
8
|
+
// Load .env file into process.env by default
|
|
9
|
+
require("dotenv").config();
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Get the file path of the caller (the file that called TestDriver)
|
|
10
13
|
* @returns {string|null} File path or null if not found
|
|
@@ -1233,13 +1236,18 @@ function createChainablePromise(promise) {
|
|
|
1233
1236
|
* TestDriver SDK
|
|
1234
1237
|
*
|
|
1235
1238
|
* This SDK provides programmatic access to TestDriver's AI-powered testing capabilities.
|
|
1239
|
+
* Automatically loads environment variables from .env file via dotenv.
|
|
1236
1240
|
*
|
|
1237
1241
|
* @example
|
|
1238
1242
|
* const TestDriver = require('testdriverai');
|
|
1239
1243
|
*
|
|
1240
|
-
*
|
|
1244
|
+
* // API key loaded automatically from TD_API_KEY in .env
|
|
1245
|
+
* const client = new TestDriver();
|
|
1241
1246
|
* await client.connect();
|
|
1242
1247
|
*
|
|
1248
|
+
* // Or pass API key explicitly
|
|
1249
|
+
* const client = new TestDriver('your-api-key');
|
|
1250
|
+
*
|
|
1243
1251
|
* // New API
|
|
1244
1252
|
* const element = await client.find('Submit button');
|
|
1245
1253
|
* await element.click();
|
|
@@ -1264,9 +1272,18 @@ const { createMarkdownLogger } = require("./interfaces/logger.js");
|
|
|
1264
1272
|
|
|
1265
1273
|
class TestDriverSDK {
|
|
1266
1274
|
constructor(apiKey, options = {}) {
|
|
1275
|
+
// Support calling with just options: new TestDriver({ os: 'windows' })
|
|
1276
|
+
if (typeof apiKey === 'object' && apiKey !== null) {
|
|
1277
|
+
options = apiKey;
|
|
1278
|
+
apiKey = null;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Use provided API key or fall back to environment variable
|
|
1282
|
+
const resolvedApiKey = apiKey || process.env.TD_API_KEY;
|
|
1283
|
+
|
|
1267
1284
|
// Set up environment with API key
|
|
1268
1285
|
const environment = {
|
|
1269
|
-
TD_API_KEY:
|
|
1286
|
+
TD_API_KEY: resolvedApiKey,
|
|
1270
1287
|
TD_API_ROOT: options.apiRoot || "https://testdriver-api.onrender.com",
|
|
1271
1288
|
TD_RESOLUTION: options.resolution || "1366x768",
|
|
1272
1289
|
TD_ANALYTICS: options.analytics !== false,
|