testdriverai 7.2.73 → 7.2.74
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} +112 -85
- package/interfaces/cli/commands/init.js +69 -91
- 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
|
@@ -1,13 +1,17 @@
|
|
|
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
|
+
- testdriver/*
|
|
6
|
+
mcp-servers:
|
|
7
|
+
testdriver:
|
|
8
|
+
command: npx
|
|
9
|
+
args:
|
|
10
|
+
- -p
|
|
11
|
+
- testdriverai@beta
|
|
12
|
+
- testdriverai-mcp
|
|
13
|
+
env:
|
|
14
|
+
TD_API_KEY: ${TD_API_KEY}
|
|
11
15
|
---
|
|
12
16
|
|
|
13
17
|
# TestDriver Expert
|
|
@@ -35,11 +39,12 @@ Use this agent when the user asks to:
|
|
|
35
39
|
### Workflow
|
|
36
40
|
|
|
37
41
|
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. **
|
|
42
|
+
2. **Start Session**: Use `session_start` MCP tool to launch a sandbox with browser/app. Specify `testFile` to track where code should be written.
|
|
43
|
+
3. **Interact**: Use MCP tools (`find`, `click`, `type`, etc.) - each returns a screenshot AND generated code.
|
|
44
|
+
4. **⚠️ WRITE CODE IMMEDIATELY**: After EVERY successful action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the end.
|
|
45
|
+
5. **Verify Actions**: Use `check` after actions to verify they succeeded (for YOUR understanding only).
|
|
46
|
+
6. **Add Assertions**: Use `assert` for test conditions that should be in the final test file.
|
|
47
|
+
7. **⚠️ RUN THE TEST YOURSELF**: Use `npx vitest run <testFile>` to run the test - do NOT tell the user to run it. Iterate until it passes.
|
|
43
48
|
|
|
44
49
|
## Prerequisites
|
|
45
50
|
|
|
@@ -108,12 +113,16 @@ describe("My Test Suite", () => {
|
|
|
108
113
|
await testdriver.provision.chrome({
|
|
109
114
|
url: "https://example.com",
|
|
110
115
|
});
|
|
116
|
+
await testdriver.screenshot(); // Capture initial page state
|
|
111
117
|
|
|
112
118
|
// Find elements and interact
|
|
113
119
|
const button = await testdriver.find("Sign In button");
|
|
120
|
+
await testdriver.screenshot(); // Capture before click
|
|
114
121
|
await button.click();
|
|
122
|
+
await testdriver.screenshot(); // Capture after click
|
|
115
123
|
|
|
116
124
|
// Assert using natural language
|
|
125
|
+
await testdriver.screenshot(); // Capture before assertion
|
|
117
126
|
const result = await testdriver.assert("the dashboard is visible");
|
|
118
127
|
expect(result).toBeTruthy();
|
|
119
128
|
});
|
|
@@ -177,9 +186,9 @@ await element.mouseUp(); // release mouse
|
|
|
177
186
|
element.found(); // check if found (boolean)
|
|
178
187
|
```
|
|
179
188
|
|
|
180
|
-
### Screenshots
|
|
189
|
+
### Screenshots for Debugging
|
|
181
190
|
|
|
182
|
-
Use `screenshot()`
|
|
191
|
+
**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
192
|
|
|
184
193
|
```javascript
|
|
185
194
|
// Capture a screenshot - saved to .testdriver/screenshots/<test-file>/
|
|
@@ -190,6 +199,14 @@ console.log("Screenshot saved to:", screenshotPath);
|
|
|
190
199
|
await testdriver.screenshot(1, false, true);
|
|
191
200
|
```
|
|
192
201
|
|
|
202
|
+
**When to add screenshots:**
|
|
203
|
+
- After provisioning (initial page load)
|
|
204
|
+
- Before and after clicking important elements
|
|
205
|
+
- After typing text into fields
|
|
206
|
+
- Before assertions (to see what the AI is evaluating)
|
|
207
|
+
- After any action that changes the page state
|
|
208
|
+
- When debugging a flaky or failing test
|
|
209
|
+
|
|
193
210
|
**Screenshot file organization:**
|
|
194
211
|
|
|
195
212
|
```
|
|
@@ -210,107 +227,106 @@ await testdriver.screenshot(1, false, true);
|
|
|
210
227
|
### Key Advantages
|
|
211
228
|
|
|
212
229
|
- **No need to restart** - continue from current state
|
|
213
|
-
- **
|
|
214
|
-
- **Code generation** - convert recorded commands to test files
|
|
230
|
+
- **Generated code with every action** - each tool returns the code to add to your test
|
|
215
231
|
- **Use `check` to verify** - understand screen state without explicit screenshots
|
|
216
232
|
|
|
233
|
+
### ⚠️ CRITICAL: Write Code Immediately & Run Tests Yourself
|
|
234
|
+
|
|
235
|
+
**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.**
|
|
236
|
+
|
|
237
|
+
**When ready to validate, RUN THE TEST YOURSELF using `npx vitest run`. Do NOT tell the user to run it.**
|
|
238
|
+
|
|
217
239
|
### Step 1: Start a Session
|
|
218
240
|
|
|
219
241
|
```
|
|
220
|
-
session_start({ type: "chrome", url: "https://your-app.com/login" })
|
|
242
|
+
session_start({ type: "chrome", url: "https://your-app.com/login", testFile: "tests/login.test.mjs" })
|
|
221
243
|
→ Screenshot shows login page
|
|
244
|
+
→ Response includes: "ACTION REQUIRED: Append this code..."
|
|
245
|
+
→ ⚠️ IMMEDIATELY write to tests/login.test.mjs:
|
|
246
|
+
await testdriver.provision.chrome({ url: "https://your-app.com/login" });
|
|
247
|
+
await testdriver.screenshot(); // Capture initial page state
|
|
222
248
|
```
|
|
223
249
|
|
|
224
250
|
This provisions a sandbox with Chrome and navigates to your URL. You'll see a screenshot of the initial page.
|
|
225
251
|
|
|
226
252
|
### Step 2: Interact with the App
|
|
227
253
|
|
|
228
|
-
Find elements and interact with them
|
|
254
|
+
Find elements and interact with them. **Write code to file after EACH action, including screenshots for debugging:**
|
|
229
255
|
|
|
230
256
|
```
|
|
231
|
-
|
|
232
|
-
→ Returns: screenshot with element highlighted
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
257
|
+
find_and_click({ description: "email input field" })
|
|
258
|
+
→ Returns: screenshot with element highlighted
|
|
259
|
+
→ ⚠️ IMMEDIATELY append to test file:
|
|
260
|
+
await testdriver.find("email input field").click();
|
|
261
|
+
await testdriver.screenshot(); // Capture after click
|
|
236
262
|
|
|
237
263
|
type({ text: "user@example.com" })
|
|
238
264
|
→ Returns: screenshot showing typed text
|
|
265
|
+
→ ⚠️ IMMEDIATELY append to test file:
|
|
266
|
+
await testdriver.type("user@example.com");
|
|
267
|
+
await testdriver.screenshot(); // Capture after typing
|
|
239
268
|
```
|
|
240
269
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
```
|
|
244
|
-
find_and_click({ description: "Sign In button" })
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### Step 3: Verify Actions Succeeded
|
|
270
|
+
### Step 3: Verify Actions Succeeded (For Your Understanding)
|
|
248
271
|
|
|
249
|
-
After
|
|
272
|
+
After actions, use `check` to verify they worked. This is for YOUR understanding - does NOT generate code:
|
|
250
273
|
|
|
251
274
|
```
|
|
252
275
|
check({ task: "Was the email entered into the field?" })
|
|
253
276
|
→ Returns: AI analysis comparing previous screenshot to current state
|
|
254
277
|
```
|
|
255
278
|
|
|
256
|
-
### Step 4: Add Assertions
|
|
279
|
+
### Step 4: Add Assertions (Generates Code)
|
|
257
280
|
|
|
258
|
-
Use `assert` for pass/fail conditions
|
|
281
|
+
Use `assert` for pass/fail conditions. This DOES generate code for the test file:
|
|
259
282
|
|
|
260
283
|
```
|
|
261
284
|
assert({ assertion: "the dashboard is visible" })
|
|
262
285
|
→ Returns: pass/fail with screenshot
|
|
286
|
+
→ ⚠️ IMMEDIATELY append to test file:
|
|
287
|
+
await testdriver.screenshot(); // Capture before assertion
|
|
288
|
+
const assertResult = await testdriver.assert("the dashboard is visible");
|
|
289
|
+
expect(assertResult).toBeTruthy();
|
|
263
290
|
```
|
|
264
291
|
|
|
265
|
-
### Step 5:
|
|
292
|
+
### Step 5: Run the Test Yourself
|
|
266
293
|
|
|
267
|
-
|
|
294
|
+
**⚠️ YOU must run the test - do NOT tell the user to run it:**
|
|
268
295
|
|
|
296
|
+
```bash
|
|
297
|
+
npx vitest run tests/login.test.mjs
|
|
269
298
|
```
|
|
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
299
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
```
|
|
282
|
-
verify({ testFile: "tests/login.test.mjs" })
|
|
283
|
-
```
|
|
300
|
+
Analyze the output, fix any issues, and iterate until the test passes.
|
|
284
301
|
|
|
285
302
|
### MCP Tools Reference
|
|
286
303
|
|
|
287
304
|
| Tool | Description |
|
|
288
305
|
|------|-------------|
|
|
289
|
-
| `session_start` | Start sandbox with browser/app,
|
|
290
|
-
| `session_status` | Check session health
|
|
306
|
+
| `session_start` | Start sandbox with browser/app, returns screenshot + provision code |
|
|
307
|
+
| `session_status` | Check session health and time remaining |
|
|
291
308
|
| `session_extend` | Add more time before session expires |
|
|
292
309
|
| `find` | Locate element by description, returns ref for later use |
|
|
293
|
-
| `click` | Click on element ref
|
|
310
|
+
| `click` | Click on element ref |
|
|
294
311
|
| `find_and_click` | Find and click in one action |
|
|
295
312
|
| `type` | Type text into focused field |
|
|
296
313
|
| `press_keys` | Press keyboard shortcuts (e.g., `["ctrl", "a"]`) |
|
|
297
314
|
| `scroll` | Scroll page (up/down/left/right) |
|
|
298
|
-
| `check` | AI analysis of
|
|
299
|
-
| `assert` | AI-powered boolean assertion
|
|
315
|
+
| `check` | AI analysis of screen state - for YOUR understanding only, does NOT generate code |
|
|
316
|
+
| `assert` | AI-powered boolean assertion - GENERATES CODE for test files |
|
|
300
317
|
| `exec` | Execute JavaScript, shell, or PowerShell in sandbox |
|
|
301
318
|
| `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
319
|
|
|
306
320
|
### Tips for MCP Workflow
|
|
307
321
|
|
|
308
|
-
1.
|
|
309
|
-
2. **Use `
|
|
310
|
-
3.
|
|
311
|
-
4. **
|
|
312
|
-
5. **
|
|
313
|
-
6. **
|
|
322
|
+
1. **⚠️ Write code IMMEDIATELY** - After EVERY action, append generated code to test file RIGHT AWAY
|
|
323
|
+
2. **⚠️ Run tests YOURSELF** - Use `npx vitest run` - do NOT tell user to run tests
|
|
324
|
+
3. **⚠️ Add screenshots liberally** - Include `await testdriver.screenshot()` after every significant action for debugging
|
|
325
|
+
4. **Work incrementally** - Don't try to build the entire test at once
|
|
326
|
+
5. **Use `check` after actions** - Verify your actions succeeded before moving on (for YOUR understanding)
|
|
327
|
+
6. **Use `assert` for test verifications** - These generate code that goes in the test file
|
|
328
|
+
7. **Be specific with element descriptions** - "the blue Sign In button in the header" is better than "button"
|
|
329
|
+
8. **Extend session proactively** - Sessions expire after 5 minutes; use `session_extend` if needed
|
|
314
330
|
|
|
315
331
|
## Recommended Development Workflow
|
|
316
332
|
|
|
@@ -325,17 +341,21 @@ verify({ testFile: "tests/login.test.mjs" })
|
|
|
325
341
|
it("should incrementally build test", async (context) => {
|
|
326
342
|
const testdriver = TestDriver(context);
|
|
327
343
|
await testdriver.provision.chrome({ url: "https://example.com" });
|
|
344
|
+
await testdriver.screenshot(); // Capture initial state
|
|
328
345
|
|
|
329
346
|
// Step 1: Find and inspect
|
|
330
347
|
const element = await testdriver.find("Some button");
|
|
331
348
|
console.log("Element found:", element.found());
|
|
332
349
|
console.log("Coordinates:", element.x, element.y);
|
|
333
350
|
console.log("Confidence:", element.confidence);
|
|
351
|
+
await testdriver.screenshot(); // Capture after find
|
|
334
352
|
|
|
335
353
|
// Step 2: Interact
|
|
336
354
|
await element.click();
|
|
355
|
+
await testdriver.screenshot(); // Capture after click
|
|
337
356
|
|
|
338
357
|
// Step 3: Assert and log
|
|
358
|
+
await testdriver.screenshot(); // Capture before assertion
|
|
339
359
|
const result = await testdriver.assert("Something happened");
|
|
340
360
|
console.log("Assertion result:", result);
|
|
341
361
|
expect(result).toBeTruthy();
|
|
@@ -417,33 +437,42 @@ const date = await testdriver.exec("pwsh", "Get-Date", 5000);
|
|
|
417
437
|
|
|
418
438
|
### Capturing Screenshots
|
|
419
439
|
|
|
440
|
+
**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.
|
|
441
|
+
|
|
420
442
|
```javascript
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
const filepath = "screenshot.png";
|
|
424
|
-
fs.writeFileSync(filepath, Buffer.from(screenshot, "base64"));
|
|
425
|
-
console.log("Screenshot saved to:", filepath);
|
|
443
|
+
// Basic screenshot - automatically saved to .testdriver/screenshots/<test-file>/
|
|
444
|
+
await testdriver.screenshot();
|
|
426
445
|
|
|
427
446
|
// Capture with mouse cursor visible
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
433
|
-
|
|
447
|
+
await testdriver.screenshot(1, false, true);
|
|
448
|
+
|
|
449
|
+
// Recommended pattern: screenshot after every significant action
|
|
450
|
+
await testdriver.provision.chrome({ url: "https://example.com" });
|
|
451
|
+
await testdriver.screenshot(); // After page load
|
|
452
|
+
|
|
453
|
+
await testdriver.find("Login button").click();
|
|
454
|
+
await testdriver.screenshot(); // After click
|
|
455
|
+
|
|
456
|
+
await testdriver.type("user@example.com");
|
|
457
|
+
await testdriver.screenshot(); // After typing
|
|
458
|
+
|
|
459
|
+
await testdriver.screenshot(); // Before assertion
|
|
460
|
+
const result = await testdriver.assert("dashboard is visible");
|
|
434
461
|
```
|
|
435
462
|
|
|
436
463
|
## Tips for Agents
|
|
437
464
|
|
|
438
|
-
1.
|
|
439
|
-
2. **
|
|
440
|
-
3.
|
|
441
|
-
4. **Use
|
|
442
|
-
5. **
|
|
443
|
-
6. **
|
|
444
|
-
7. **
|
|
445
|
-
8. **
|
|
446
|
-
9. **
|
|
465
|
+
1. **⚠️ WRITE CODE IMMEDIATELY** - After EVERY successful MCP action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the session ends.
|
|
466
|
+
2. **⚠️ RUN TESTS YOURSELF** - Do NOT tell the user to run tests. YOU must run the tests using `npx vitest run <testFile>`. Analyze the output and iterate until the test passes.
|
|
467
|
+
3. **⚠️ ADD SCREENSHOTS LIBERALLY** - Include `await testdriver.screenshot()` throughout your tests: after provision, before/after clicks, after typing, and before assertions. This creates a visual trail that makes debugging failures much easier.
|
|
468
|
+
4. **Use MCP tools for development** - Build tests interactively with visual feedback
|
|
469
|
+
5. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
|
|
470
|
+
6. **Look at test samples** in `node_modules/testdriverai/test` for working examples
|
|
471
|
+
7. **Use `check` to understand screen state** - This is how you verify what the sandbox shows during MCP development.
|
|
472
|
+
8. **Use `check` after actions, `assert` for test files** - `check` gives detailed AI analysis (no code), `assert` gives boolean pass/fail (generates code)
|
|
473
|
+
9. **Be specific with element descriptions** - "blue Sign In button in the header" > "button"
|
|
474
|
+
10. **Start simple** - get one step working before adding more
|
|
475
|
+
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
476
|
|
|
448
477
|
```json
|
|
449
478
|
// eslint.config.js (for TypeScript projects)
|
|
@@ -453,5 +482,3 @@ console.log("Screenshot with mouse saved to: screenshot-with-mouse.png");
|
|
|
453
482
|
}
|
|
454
483
|
}
|
|
455
484
|
```
|
|
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
|
|
|
@@ -478,10 +488,10 @@ jobs:
|
|
|
478
488
|
|
|
479
489
|
if (!fs.existsSync(mcpConfigFile)) {
|
|
480
490
|
const mcpConfig = {
|
|
481
|
-
|
|
491
|
+
servers: {
|
|
482
492
|
testdriver: {
|
|
483
493
|
command: "npx",
|
|
484
|
-
args: ["testdriverai-mcp"],
|
|
494
|
+
args: ["-p", "testdriverai@beta", "testdriverai-mcp"],
|
|
485
495
|
env: {
|
|
486
496
|
TD_API_KEY: "${TD_API_KEY}",
|
|
487
497
|
},
|
|
@@ -499,6 +509,36 @@ jobs:
|
|
|
499
509
|
}
|
|
500
510
|
}
|
|
501
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Create VSCode extensions recommendations
|
|
514
|
+
*/
|
|
515
|
+
async createVscodeExtensions() {
|
|
516
|
+
const vscodeDir = path.join(process.cwd(), ".vscode");
|
|
517
|
+
const extensionsFile = path.join(vscodeDir, "extensions.json");
|
|
518
|
+
|
|
519
|
+
// Create .vscode directory if it doesn't exist
|
|
520
|
+
if (!fs.existsSync(vscodeDir)) {
|
|
521
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
522
|
+
console.log(chalk.gray(` Created directory: ${vscodeDir}`));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!fs.existsSync(extensionsFile)) {
|
|
526
|
+
const extensionsConfig = {
|
|
527
|
+
recommendations: [
|
|
528
|
+
"vitest.explorer",
|
|
529
|
+
],
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
fs.writeFileSync(
|
|
533
|
+
extensionsFile,
|
|
534
|
+
JSON.stringify(extensionsConfig, null, 2) + "\n",
|
|
535
|
+
);
|
|
536
|
+
console.log(chalk.green(` Created extensions config: ${extensionsFile}`));
|
|
537
|
+
} else {
|
|
538
|
+
console.log(chalk.gray(" Extensions config already exists, skipping..."));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
502
542
|
/**
|
|
503
543
|
* Copy TestDriver skills from the package to the project
|
|
504
544
|
*/
|
|
@@ -557,7 +597,7 @@ jobs:
|
|
|
557
597
|
}
|
|
558
598
|
|
|
559
599
|
/**
|
|
560
|
-
*
|
|
600
|
+
* Copy TestDriver agents to .github/agents
|
|
561
601
|
*/
|
|
562
602
|
async createAgents() {
|
|
563
603
|
const agentsDestDir = path.join(process.cwd(), ".github", "agents");
|
|
@@ -576,98 +616,36 @@ jobs:
|
|
|
576
616
|
}
|
|
577
617
|
}
|
|
578
618
|
|
|
619
|
+
if (!agentsSourceDir) {
|
|
620
|
+
console.log(chalk.yellow(" ⚠️ Agents directory not found, skipping agents copy..."));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
579
624
|
// Create .github/agents directory if it doesn't exist
|
|
580
625
|
if (!fs.existsSync(agentsDestDir)) {
|
|
581
626
|
fs.mkdirSync(agentsDestDir, { recursive: true });
|
|
582
627
|
console.log(chalk.gray(` Created directory: ${agentsDestDir}`));
|
|
583
628
|
}
|
|
584
629
|
|
|
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
|
-
}
|
|
630
|
+
// Copy agent files with .agent.md extension
|
|
631
|
+
const agentFiles = fs.readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
|
|
632
|
+
let copiedCount = 0;
|
|
633
|
+
|
|
634
|
+
for (const agentFile of agentFiles) {
|
|
635
|
+
const sourcePath = path.join(agentsSourceDir, agentFile);
|
|
636
|
+
const agentName = agentFile.replace(".md", "");
|
|
637
|
+
const destPath = path.join(agentsDestDir, `${agentName}.agent.md`);
|
|
638
|
+
|
|
639
|
+
if (!fs.existsSync(destPath)) {
|
|
640
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
641
|
+
copiedCount++;
|
|
630
642
|
}
|
|
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
|
-
`;
|
|
643
|
+
}
|
|
667
644
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
645
|
+
if (copiedCount > 0) {
|
|
646
|
+
console.log(chalk.green(` Copied ${copiedCount} agent(s) to ${agentsDestDir}`));
|
|
647
|
+
} else {
|
|
648
|
+
console.log(chalk.gray(" Agents already exist, skipping..."));
|
|
671
649
|
}
|
|
672
650
|
}
|
|
673
651
|
|
|
@@ -705,7 +683,7 @@ You are an expert at writing automated tests using the TestDriver library. Your
|
|
|
705
683
|
console.log(" 1. Run your tests:");
|
|
706
684
|
console.log(chalk.gray(" npx vitest run\n"));
|
|
707
685
|
console.log(" 2. Use AI agents to write tests:");
|
|
708
|
-
console.log(chalk.gray(" Open VSCode/Cursor and use @
|
|
686
|
+
console.log(chalk.gray(" Open VSCode/Cursor and use @testdriver agent\n"));
|
|
709
687
|
console.log(" 3. MCP server configured:");
|
|
710
688
|
console.log(chalk.gray(" TestDriver tools available via MCP in .vscode/mcp.json\n"));
|
|
711
689
|
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", {
|