@_nazmiforreal/agent-browser-mcp 0.2.0
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/.mcp.json +12 -0
- package/AGENTS.md +100 -0
- package/README.md +502 -0
- package/dist/browsers/discover.d.ts +13 -0
- package/dist/browsers/discover.d.ts.map +1 -0
- package/dist/browsers/discover.js +54 -0
- package/dist/browsers/discover.js.map +1 -0
- package/dist/browsers/env.d.ts +10 -0
- package/dist/browsers/env.d.ts.map +1 -0
- package/dist/browsers/env.js +72 -0
- package/dist/browsers/env.js.map +1 -0
- package/dist/browsers/index.d.ts +5 -0
- package/dist/browsers/index.d.ts.map +1 -0
- package/dist/browsers/index.js +5 -0
- package/dist/browsers/index.js.map +1 -0
- package/dist/browsers/installer.d.ts +13 -0
- package/dist/browsers/installer.d.ts.map +1 -0
- package/dist/browsers/installer.js +59 -0
- package/dist/browsers/installer.js.map +1 -0
- package/dist/browsers/registry.d.ts +14 -0
- package/dist/browsers/registry.d.ts.map +1 -0
- package/dist/browsers/registry.js +119 -0
- package/dist/browsers/registry.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +153 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/browser.d.ts +3 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +885 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/executor.d.ts +9 -0
- package/dist/tools/executor.d.ts.map +1 -0
- package/dist/tools/executor.js +1067 -0
- package/dist/tools/executor.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -0
- package/package.json +60 -0
- package/tests/browser-tools.test.ts +329 -0
- package/tests/executor.test.ts +356 -0
- package/tests/integration.test.ts +467 -0
- package/tests/server.test.ts +224 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
|
|
4
|
+
// Create a shared mock process factory
|
|
5
|
+
function createMockProcess(exitCode = 0) {
|
|
6
|
+
const stdout = new EventEmitter();
|
|
7
|
+
const stderr = new EventEmitter();
|
|
8
|
+
|
|
9
|
+
const mockProc = {
|
|
10
|
+
stdout,
|
|
11
|
+
stderr,
|
|
12
|
+
on: vi.fn((event: string, callback: Function) => {
|
|
13
|
+
if (event === "close") {
|
|
14
|
+
setImmediate(() => callback(exitCode));
|
|
15
|
+
}
|
|
16
|
+
return mockProc;
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return mockProc;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Mock child_process at module level with factory
|
|
24
|
+
const mockSpawn = vi.fn();
|
|
25
|
+
vi.mock("child_process", () => ({
|
|
26
|
+
spawn: mockSpawn,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe("Integration Tests", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.resetModules();
|
|
32
|
+
mockSpawn.mockReset();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("Full workflow scenarios", () => {
|
|
40
|
+
it("should handle a complete navigation workflow", async () => {
|
|
41
|
+
const mockProc = createMockProcess();
|
|
42
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
43
|
+
|
|
44
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
45
|
+
|
|
46
|
+
const navPromise = execBrowser("navigate", { url: "https://example.com" });
|
|
47
|
+
mockProc.stdout.emit("data", "Navigated to https://example.com");
|
|
48
|
+
const navResult = await navPromise;
|
|
49
|
+
|
|
50
|
+
expect(navResult).toContain("Navigated");
|
|
51
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
52
|
+
expect.any(String),
|
|
53
|
+
expect.arrayContaining(["open", "https://example.com"]),
|
|
54
|
+
expect.any(Object)
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should handle form filling workflow", async () => {
|
|
59
|
+
const mockProc = createMockProcess();
|
|
60
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
61
|
+
|
|
62
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
63
|
+
|
|
64
|
+
// Fill email field
|
|
65
|
+
const fillPromise = execBrowser("fill", {
|
|
66
|
+
selector: "#email",
|
|
67
|
+
value: "test@example.com",
|
|
68
|
+
});
|
|
69
|
+
mockProc.stdout.emit("data", "Filled #email");
|
|
70
|
+
await fillPromise;
|
|
71
|
+
|
|
72
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
73
|
+
expect.any(String),
|
|
74
|
+
expect.arrayContaining(["fill", "#email", "test@example.com"]),
|
|
75
|
+
expect.any(Object)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should handle screenshot workflow", async () => {
|
|
80
|
+
const mockProc = createMockProcess();
|
|
81
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
82
|
+
|
|
83
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
84
|
+
|
|
85
|
+
const screenshotPromise = execBrowser("screenshot", {
|
|
86
|
+
path: "/tmp/test-screenshot.png",
|
|
87
|
+
fullPage: true,
|
|
88
|
+
});
|
|
89
|
+
mockProc.stdout.emit("data", "Screenshot saved to /tmp/test-screenshot.png");
|
|
90
|
+
const result = await screenshotPromise;
|
|
91
|
+
|
|
92
|
+
expect(result).toContain("Screenshot saved");
|
|
93
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
94
|
+
expect.any(String),
|
|
95
|
+
expect.arrayContaining(["screenshot", "/tmp/test-screenshot.png", "-f"]),
|
|
96
|
+
expect.any(Object)
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should handle session management", async () => {
|
|
101
|
+
const mockProc = createMockProcess();
|
|
102
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
103
|
+
|
|
104
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
105
|
+
|
|
106
|
+
// Create new session
|
|
107
|
+
const createPromise = execBrowser("new_session", {
|
|
108
|
+
viewport: { width: 1920, height: 1080 },
|
|
109
|
+
});
|
|
110
|
+
mockProc.stdout.emit("data", "session-abc123");
|
|
111
|
+
const sessionId = await createPromise;
|
|
112
|
+
|
|
113
|
+
expect(sessionId).toBe("session-abc123");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should pass sessionId to execBrowser", async () => {
|
|
117
|
+
const mockProc = createMockProcess();
|
|
118
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
119
|
+
|
|
120
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
121
|
+
|
|
122
|
+
const navPromise = execBrowser("navigate", { url: "https://example.com" }, "sess-123");
|
|
123
|
+
mockProc.stdout.emit("data", "Navigated");
|
|
124
|
+
await navPromise;
|
|
125
|
+
|
|
126
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
127
|
+
expect.any(String),
|
|
128
|
+
expect.arrayContaining(["open", "--session", "sess-123"]),
|
|
129
|
+
expect.objectContaining({
|
|
130
|
+
env: expect.objectContaining({
|
|
131
|
+
AGENT_BROWSER_SESSION: "sess-123",
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("Error scenarios", () => {
|
|
139
|
+
it("should handle element not found error", async () => {
|
|
140
|
+
const mockProc = createMockProcess(1);
|
|
141
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
142
|
+
|
|
143
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
144
|
+
|
|
145
|
+
const clickPromise = execBrowser("click", { selector: "#non-existent" });
|
|
146
|
+
mockProc.stderr.emit("data", "Element not found: #non-existent");
|
|
147
|
+
|
|
148
|
+
await expect(clickPromise).rejects.toThrow("Element not found");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should handle navigation timeout", async () => {
|
|
152
|
+
const mockProc = createMockProcess(1);
|
|
153
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
154
|
+
|
|
155
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
156
|
+
|
|
157
|
+
const navPromise = execBrowser("wait_for_navigation", { timeout: 100 });
|
|
158
|
+
mockProc.stderr.emit("data", "Timeout exceeded");
|
|
159
|
+
|
|
160
|
+
await expect(navPromise).rejects.toThrow("Timeout");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should handle invalid selector", async () => {
|
|
164
|
+
const mockProc = createMockProcess(1);
|
|
165
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
166
|
+
|
|
167
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
168
|
+
|
|
169
|
+
const clickPromise = execBrowser("click", { selector: "::invalid::" });
|
|
170
|
+
mockProc.stderr.emit("data", "Invalid selector syntax");
|
|
171
|
+
|
|
172
|
+
await expect(clickPromise).rejects.toThrow("Invalid selector");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should handle spawn error", async () => {
|
|
176
|
+
const stdout = new EventEmitter();
|
|
177
|
+
const stderr = new EventEmitter();
|
|
178
|
+
|
|
179
|
+
const mockProc = {
|
|
180
|
+
stdout,
|
|
181
|
+
stderr,
|
|
182
|
+
on: vi.fn((event: string, callback: Function) => {
|
|
183
|
+
if (event === "error") {
|
|
184
|
+
setImmediate(() => callback(new Error("Command not found")));
|
|
185
|
+
}
|
|
186
|
+
return mockProc;
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
190
|
+
|
|
191
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
192
|
+
|
|
193
|
+
const promise = execBrowser("navigate", { url: "https://example.com" });
|
|
194
|
+
await expect(promise).rejects.toThrow("Failed to execute agent-browser");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("Concurrent operations", () => {
|
|
199
|
+
it("should handle multiple parallel operations", async () => {
|
|
200
|
+
// Create separate mock processes for each call
|
|
201
|
+
const mockProcs = [createMockProcess(), createMockProcess(), createMockProcess()];
|
|
202
|
+
let callIndex = 0;
|
|
203
|
+
mockSpawn.mockImplementation(() => mockProcs[callIndex++]);
|
|
204
|
+
|
|
205
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
206
|
+
|
|
207
|
+
// Start multiple operations in parallel
|
|
208
|
+
const operations = [
|
|
209
|
+
execBrowser("get_url", {}),
|
|
210
|
+
execBrowser("get_title", {}),
|
|
211
|
+
execBrowser("get_text", {}),
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
// Emit results for each
|
|
215
|
+
mockProcs[0].stdout.emit("data", "https://example.com");
|
|
216
|
+
mockProcs[1].stdout.emit("data", "Example Domain");
|
|
217
|
+
mockProcs[2].stdout.emit("data", "Hello World");
|
|
218
|
+
|
|
219
|
+
const results = await Promise.all(operations);
|
|
220
|
+
|
|
221
|
+
expect(results).toHaveLength(3);
|
|
222
|
+
expect(results[0]).toBe("https://example.com");
|
|
223
|
+
expect(results[1]).toBe("Example Domain");
|
|
224
|
+
expect(results[2]).toBe("Hello World");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("Edge Cases", () => {
|
|
230
|
+
beforeEach(() => {
|
|
231
|
+
vi.resetModules();
|
|
232
|
+
mockSpawn.mockReset();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("Special characters handling", () => {
|
|
236
|
+
it("should handle selectors with special characters", async () => {
|
|
237
|
+
const mockProc = createMockProcess();
|
|
238
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
239
|
+
|
|
240
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
241
|
+
|
|
242
|
+
const promise = execBrowser("click", {
|
|
243
|
+
selector: '[data-testid="submit-btn"]',
|
|
244
|
+
});
|
|
245
|
+
mockProc.stdout.emit("data", "Clicked");
|
|
246
|
+
await promise;
|
|
247
|
+
|
|
248
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
249
|
+
expect.any(String),
|
|
250
|
+
expect.arrayContaining(["click", '[data-testid="submit-btn"]']),
|
|
251
|
+
expect.any(Object)
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should handle URLs with query parameters", async () => {
|
|
256
|
+
const mockProc = createMockProcess();
|
|
257
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
258
|
+
|
|
259
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
260
|
+
|
|
261
|
+
const promise = execBrowser("navigate", {
|
|
262
|
+
url: "https://example.com/search?q=hello&page=1",
|
|
263
|
+
});
|
|
264
|
+
mockProc.stdout.emit("data", "Navigated");
|
|
265
|
+
await promise;
|
|
266
|
+
|
|
267
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
268
|
+
expect.any(String),
|
|
269
|
+
expect.arrayContaining(["open", "https://example.com/search?q=hello&page=1"]),
|
|
270
|
+
expect.any(Object)
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should handle text with newlines", async () => {
|
|
275
|
+
const mockProc = createMockProcess();
|
|
276
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
277
|
+
|
|
278
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
279
|
+
|
|
280
|
+
const promise = execBrowser("fill", {
|
|
281
|
+
selector: "#textarea",
|
|
282
|
+
value: "Line 1\nLine 2\nLine 3",
|
|
283
|
+
});
|
|
284
|
+
mockProc.stdout.emit("data", "Filled");
|
|
285
|
+
await promise;
|
|
286
|
+
|
|
287
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
288
|
+
expect.any(String),
|
|
289
|
+
expect.arrayContaining(["fill", "#textarea", "Line 1\nLine 2\nLine 3"]),
|
|
290
|
+
expect.any(Object)
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should handle unicode characters", async () => {
|
|
295
|
+
const mockProc = createMockProcess();
|
|
296
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
297
|
+
|
|
298
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
299
|
+
|
|
300
|
+
const promise = execBrowser("type", {
|
|
301
|
+
selector: "#input",
|
|
302
|
+
text: "Hello 世界",
|
|
303
|
+
});
|
|
304
|
+
mockProc.stdout.emit("data", "Typed");
|
|
305
|
+
await promise;
|
|
306
|
+
|
|
307
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
308
|
+
expect.any(String),
|
|
309
|
+
expect.arrayContaining(["type", "#input", "Hello 世界"]),
|
|
310
|
+
expect.any(Object)
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("Empty and null values", () => {
|
|
316
|
+
it("should handle empty string values", async () => {
|
|
317
|
+
const mockProc = createMockProcess();
|
|
318
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
319
|
+
|
|
320
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
321
|
+
|
|
322
|
+
const promise = execBrowser("fill", {
|
|
323
|
+
selector: "#input",
|
|
324
|
+
value: "",
|
|
325
|
+
});
|
|
326
|
+
mockProc.stdout.emit("data", "Cleared");
|
|
327
|
+
await promise;
|
|
328
|
+
|
|
329
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
330
|
+
expect.any(String),
|
|
331
|
+
expect.arrayContaining(["fill", "#input", ""]),
|
|
332
|
+
expect.any(Object)
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should handle empty response from browser", async () => {
|
|
337
|
+
const mockProc = createMockProcess();
|
|
338
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
339
|
+
|
|
340
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
341
|
+
|
|
342
|
+
const promise = execBrowser("get_text", { selector: "#empty" });
|
|
343
|
+
// Don't emit any stdout data - let it close with empty output
|
|
344
|
+
|
|
345
|
+
const result = await promise;
|
|
346
|
+
expect(result).toBe("Command executed successfully");
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("Large data handling", () => {
|
|
351
|
+
it("should handle large HTML response", async () => {
|
|
352
|
+
const mockProc = createMockProcess();
|
|
353
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
354
|
+
|
|
355
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
356
|
+
|
|
357
|
+
const promise = execBrowser("get_html", {});
|
|
358
|
+
|
|
359
|
+
// Simulate large HTML response
|
|
360
|
+
const largeHtml = "<html>" + "<div>".repeat(10000) + "</div>".repeat(10000) + "</html>";
|
|
361
|
+
mockProc.stdout.emit("data", largeHtml);
|
|
362
|
+
|
|
363
|
+
const result = await promise;
|
|
364
|
+
expect(result.length).toBeGreaterThan(100000);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("should handle multiple cookies", async () => {
|
|
368
|
+
const mockProc = createMockProcess();
|
|
369
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
370
|
+
|
|
371
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
372
|
+
|
|
373
|
+
const cookies = Array.from({ length: 50 }, (_, i) => ({
|
|
374
|
+
name: `cookie_${i}`,
|
|
375
|
+
value: `value_${i}`,
|
|
376
|
+
domain: ".example.com",
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
const promise = execBrowser("set_cookies", { cookies });
|
|
380
|
+
mockProc.stdout.emit("data", "50 cookies set");
|
|
381
|
+
|
|
382
|
+
const result = await promise;
|
|
383
|
+
expect(result).toContain("50 cookies");
|
|
384
|
+
|
|
385
|
+
// Verify first cookie was passed as positional args
|
|
386
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
387
|
+
expect.any(String),
|
|
388
|
+
expect.arrayContaining(["cookies", "set", "cookie_0", "value_0"]),
|
|
389
|
+
expect.any(Object)
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe("Boolean and object options", () => {
|
|
395
|
+
it("should only include boolean flag when true", async () => {
|
|
396
|
+
const mockProc = createMockProcess();
|
|
397
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
398
|
+
|
|
399
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
400
|
+
|
|
401
|
+
const promise = execBrowser("screenshot", { fullPage: true });
|
|
402
|
+
mockProc.stdout.emit("data", "Done");
|
|
403
|
+
await promise;
|
|
404
|
+
|
|
405
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
406
|
+
expect.any(String),
|
|
407
|
+
expect.arrayContaining(["screenshot", "-f"]),
|
|
408
|
+
expect.any(Object)
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("should not include boolean flag when false", async () => {
|
|
413
|
+
const mockProc = createMockProcess();
|
|
414
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
415
|
+
|
|
416
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
417
|
+
|
|
418
|
+
const promise = execBrowser("screenshot", { fullPage: false, path: "/tmp/shot.png" });
|
|
419
|
+
mockProc.stdout.emit("data", "Done");
|
|
420
|
+
await promise;
|
|
421
|
+
|
|
422
|
+
const callArgs = mockSpawn.mock.calls[0][1];
|
|
423
|
+
expect(callArgs).not.toContain("-f");
|
|
424
|
+
expect(callArgs).toContain("/tmp/shot.png");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("should pass viewport as JSON object", async () => {
|
|
428
|
+
const mockProc = createMockProcess();
|
|
429
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
430
|
+
|
|
431
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
432
|
+
|
|
433
|
+
const viewport = { width: 1920, height: 1080 };
|
|
434
|
+
const promise = execBrowser("new_session", { viewport });
|
|
435
|
+
mockProc.stdout.emit("data", "session-123");
|
|
436
|
+
await promise;
|
|
437
|
+
|
|
438
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
439
|
+
expect.any(String),
|
|
440
|
+
expect.arrayContaining(["session"]),
|
|
441
|
+
expect.any(Object)
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("CamelCase to kebab-case conversion", () => {
|
|
448
|
+
beforeEach(() => {
|
|
449
|
+
vi.resetModules();
|
|
450
|
+
mockSpawn.mockReset();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("should convert camelCase options to kebab-case flags", async () => {
|
|
454
|
+
const mockProc = createMockProcess();
|
|
455
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
456
|
+
|
|
457
|
+
const { execBrowser } = await import("../src/tools/executor.js");
|
|
458
|
+
|
|
459
|
+
const promise = execBrowser("screenshot", { fullPage: true });
|
|
460
|
+
mockProc.stdout.emit("data", "Done");
|
|
461
|
+
await promise;
|
|
462
|
+
|
|
463
|
+
const callArgs = mockSpawn.mock.calls[0][1];
|
|
464
|
+
expect(callArgs).toContain("-f");
|
|
465
|
+
expect(callArgs).not.toContain("--fullPage");
|
|
466
|
+
});
|
|
467
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
|
|
4
|
+
// Mock the executor module
|
|
5
|
+
vi.mock("../src/tools/executor.js", () => ({
|
|
6
|
+
execBrowser: vi.fn().mockResolvedValue("Success"),
|
|
7
|
+
checkAgentBrowser: vi.fn().mockResolvedValue(true),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe("MCP Server", () => {
|
|
11
|
+
describe("server initialization", () => {
|
|
12
|
+
it("should create McpServer with correct metadata", () => {
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: "agent-browser-mcp",
|
|
15
|
+
version: "0.1.0",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(server).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should have tool registration method", () => {
|
|
22
|
+
const server = new McpServer({
|
|
23
|
+
name: "agent-browser-mcp",
|
|
24
|
+
version: "0.1.0",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(typeof server.tool).toBe("function");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("tool registration integration", () => {
|
|
32
|
+
let server: McpServer;
|
|
33
|
+
let toolCalls: Array<{ name: string; description: string }>;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
toolCalls = [];
|
|
37
|
+
server = {
|
|
38
|
+
tool: vi.fn((name, description) => {
|
|
39
|
+
toolCalls.push({ name, description });
|
|
40
|
+
}),
|
|
41
|
+
} as unknown as McpServer;
|
|
42
|
+
|
|
43
|
+
const { registerBrowserTools } = await import("../src/tools/browser.js");
|
|
44
|
+
registerBrowserTools(server);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should register tools with descriptive names", () => {
|
|
48
|
+
const toolNames = toolCalls.map((t) => t.name);
|
|
49
|
+
|
|
50
|
+
// All tools should have browser_ prefix
|
|
51
|
+
expect(toolNames.every((name) => name.startsWith("browser_"))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should provide meaningful descriptions for all tools", () => {
|
|
55
|
+
toolCalls.forEach((tool) => {
|
|
56
|
+
expect(tool.description).toBeDefined();
|
|
57
|
+
expect(tool.description.length).toBeGreaterThan(10);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should register navigation tools with correct descriptions", () => {
|
|
62
|
+
const navigateTool = toolCalls.find((t) => t.name === "browser_navigate");
|
|
63
|
+
expect(navigateTool?.description).toContain("Navigate");
|
|
64
|
+
|
|
65
|
+
const goBackTool = toolCalls.find((t) => t.name === "browser_go_back");
|
|
66
|
+
expect(goBackTool?.description).toContain("back");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should register interaction tools with correct descriptions", () => {
|
|
70
|
+
const clickTool = toolCalls.find((t) => t.name === "browser_click");
|
|
71
|
+
expect(clickTool?.description.toLowerCase()).toContain("click");
|
|
72
|
+
|
|
73
|
+
const fillTool = toolCalls.find((t) => t.name === "browser_fill");
|
|
74
|
+
expect(fillTool?.description.toLowerCase()).toContain("fill");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("Tool Schema Validation", () => {
|
|
80
|
+
it("should validate URL in browser_navigate", async () => {
|
|
81
|
+
const { z } = await import("zod");
|
|
82
|
+
|
|
83
|
+
const schema = z.object({
|
|
84
|
+
url: z.string().url(),
|
|
85
|
+
sessionId: z.string().optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Valid URL
|
|
89
|
+
expect(() => schema.parse({ url: "https://example.com" })).not.toThrow();
|
|
90
|
+
|
|
91
|
+
// Invalid URL
|
|
92
|
+
expect(() => schema.parse({ url: "not-a-url" })).toThrow();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should validate scroll direction enum", async () => {
|
|
96
|
+
const { z } = await import("zod");
|
|
97
|
+
|
|
98
|
+
const schema = z.object({
|
|
99
|
+
direction: z.enum(["up", "down", "left", "right"]),
|
|
100
|
+
amount: z.number().optional(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Valid directions
|
|
104
|
+
expect(() => schema.parse({ direction: "up" })).not.toThrow();
|
|
105
|
+
expect(() => schema.parse({ direction: "down" })).not.toThrow();
|
|
106
|
+
expect(() => schema.parse({ direction: "left" })).not.toThrow();
|
|
107
|
+
expect(() => schema.parse({ direction: "right" })).not.toThrow();
|
|
108
|
+
|
|
109
|
+
// Invalid direction
|
|
110
|
+
expect(() => schema.parse({ direction: "diagonal" })).toThrow();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should validate wait state enum", async () => {
|
|
114
|
+
const { z } = await import("zod");
|
|
115
|
+
|
|
116
|
+
const schema = z.object({
|
|
117
|
+
selector: z.string(),
|
|
118
|
+
state: z.enum(["attached", "detached", "visible", "hidden"]).optional(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Valid states
|
|
122
|
+
expect(() => schema.parse({ selector: "#el", state: "visible" })).not.toThrow();
|
|
123
|
+
expect(() => schema.parse({ selector: "#el", state: "hidden" })).not.toThrow();
|
|
124
|
+
|
|
125
|
+
// Invalid state
|
|
126
|
+
expect(() => schema.parse({ selector: "#el", state: "loading" })).toThrow();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should validate cookie schema", async () => {
|
|
130
|
+
const { z } = await import("zod");
|
|
131
|
+
|
|
132
|
+
const cookieSchema = z.object({
|
|
133
|
+
name: z.string(),
|
|
134
|
+
value: z.string(),
|
|
135
|
+
domain: z.string().optional(),
|
|
136
|
+
path: z.string().optional(),
|
|
137
|
+
expires: z.number().optional(),
|
|
138
|
+
httpOnly: z.boolean().optional(),
|
|
139
|
+
secure: z.boolean().optional(),
|
|
140
|
+
sameSite: z.enum(["Strict", "Lax", "None"]).optional(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const schema = z.object({
|
|
144
|
+
cookies: z.array(cookieSchema),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Valid cookie
|
|
148
|
+
expect(() =>
|
|
149
|
+
schema.parse({
|
|
150
|
+
cookies: [
|
|
151
|
+
{
|
|
152
|
+
name: "session",
|
|
153
|
+
value: "abc123",
|
|
154
|
+
domain: ".example.com",
|
|
155
|
+
httpOnly: true,
|
|
156
|
+
sameSite: "Strict",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
})
|
|
160
|
+
).not.toThrow();
|
|
161
|
+
|
|
162
|
+
// Invalid sameSite
|
|
163
|
+
expect(() =>
|
|
164
|
+
schema.parse({
|
|
165
|
+
cookies: [{ name: "test", value: "val", sameSite: "Invalid" }],
|
|
166
|
+
})
|
|
167
|
+
).toThrow();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should validate viewport schema", async () => {
|
|
171
|
+
const { z } = await import("zod");
|
|
172
|
+
|
|
173
|
+
const schema = z.object({
|
|
174
|
+
viewport: z
|
|
175
|
+
.object({
|
|
176
|
+
width: z.number(),
|
|
177
|
+
height: z.number(),
|
|
178
|
+
})
|
|
179
|
+
.optional(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Valid viewport
|
|
183
|
+
expect(() =>
|
|
184
|
+
schema.parse({
|
|
185
|
+
viewport: { width: 1920, height: 1080 },
|
|
186
|
+
})
|
|
187
|
+
).not.toThrow();
|
|
188
|
+
|
|
189
|
+
// Invalid viewport (missing height)
|
|
190
|
+
expect(() =>
|
|
191
|
+
schema.parse({
|
|
192
|
+
viewport: { width: 1920 },
|
|
193
|
+
})
|
|
194
|
+
).toThrow();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("Response Format", () => {
|
|
199
|
+
it("should return MCP-compliant response format", async () => {
|
|
200
|
+
vi.mock("../src/tools/executor.js", () => ({
|
|
201
|
+
execBrowser: vi.fn().mockResolvedValue("Test result"),
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
const mockServer = {
|
|
205
|
+
tool: vi.fn((name, description, schema, handler) => {
|
|
206
|
+
// Store handler for testing
|
|
207
|
+
(mockServer as any).handlers = (mockServer as any).handlers || {};
|
|
208
|
+
(mockServer as any).handlers[name] = handler;
|
|
209
|
+
}),
|
|
210
|
+
} as unknown as McpServer;
|
|
211
|
+
|
|
212
|
+
const { registerBrowserTools } = await import("../src/tools/browser.js");
|
|
213
|
+
registerBrowserTools(mockServer);
|
|
214
|
+
|
|
215
|
+
const handler = (mockServer as any).handlers["browser_navigate"];
|
|
216
|
+
const result = await handler({ url: "https://example.com" });
|
|
217
|
+
|
|
218
|
+
// Verify MCP response format
|
|
219
|
+
expect(result).toHaveProperty("content");
|
|
220
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
221
|
+
expect(result.content[0]).toHaveProperty("type", "text");
|
|
222
|
+
expect(result.content[0]).toHaveProperty("text");
|
|
223
|
+
});
|
|
224
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: "v8",
|
|
10
|
+
reporter: ["text", "json", "html"],
|
|
11
|
+
include: ["src/**/*.ts"],
|
|
12
|
+
exclude: ["src/**/*.test.ts", "src/**/*.d.ts"],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|