@_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.
Files changed (49) hide show
  1. package/.mcp.json +12 -0
  2. package/AGENTS.md +100 -0
  3. package/README.md +502 -0
  4. package/dist/browsers/discover.d.ts +13 -0
  5. package/dist/browsers/discover.d.ts.map +1 -0
  6. package/dist/browsers/discover.js +54 -0
  7. package/dist/browsers/discover.js.map +1 -0
  8. package/dist/browsers/env.d.ts +10 -0
  9. package/dist/browsers/env.d.ts.map +1 -0
  10. package/dist/browsers/env.js +72 -0
  11. package/dist/browsers/env.js.map +1 -0
  12. package/dist/browsers/index.d.ts +5 -0
  13. package/dist/browsers/index.d.ts.map +1 -0
  14. package/dist/browsers/index.js +5 -0
  15. package/dist/browsers/index.js.map +1 -0
  16. package/dist/browsers/installer.d.ts +13 -0
  17. package/dist/browsers/installer.d.ts.map +1 -0
  18. package/dist/browsers/installer.js +59 -0
  19. package/dist/browsers/installer.js.map +1 -0
  20. package/dist/browsers/registry.d.ts +14 -0
  21. package/dist/browsers/registry.d.ts.map +1 -0
  22. package/dist/browsers/registry.js +119 -0
  23. package/dist/browsers/registry.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +153 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +25 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/tools/browser.d.ts +3 -0
  33. package/dist/tools/browser.d.ts.map +1 -0
  34. package/dist/tools/browser.js +885 -0
  35. package/dist/tools/browser.js.map +1 -0
  36. package/dist/tools/executor.d.ts +9 -0
  37. package/dist/tools/executor.d.ts.map +1 -0
  38. package/dist/tools/executor.js +1067 -0
  39. package/dist/tools/executor.js.map +1 -0
  40. package/dist/tools/index.d.ts +3 -0
  41. package/dist/tools/index.d.ts.map +1 -0
  42. package/dist/tools/index.js +3 -0
  43. package/dist/tools/index.js.map +1 -0
  44. package/package.json +60 -0
  45. package/tests/browser-tools.test.ts +329 -0
  46. package/tests/executor.test.ts +356 -0
  47. package/tests/integration.test.ts +467 -0
  48. package/tests/server.test.ts +224 -0
  49. 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
+ });
@@ -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
+ });