import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createBrowserTools } from './tools.js'; import type { BrowserManager } from './manager.js'; const mockGoto = vi.fn().mockResolvedValue(undefined); const mockTitle = vi.fn().mockResolvedValue('Test Page'); const mockUrl = vi.fn().mockReturnValue('https://example.com'); const mockClick = vi.fn().mockResolvedValue(undefined); const mockType = vi.fn().mockResolvedValue(undefined); const mock$eval = vi.fn().mockResolvedValue('Page content here'); const mockEvaluate = vi.fn().mockResolvedValue({ result: 42 }); const mockScreenshot = vi.fn().mockResolvedValue('base64data'); const mock$ = vi.fn().mockResolvedValue({ screenshot: vi.fn().mockResolvedValue('element-base64') }); const mockKeyboard = { press: vi.fn().mockResolvedValue(undefined) }; const mockPage = { goto: mockGoto, title: mockTitle, url: mockUrl, click: mockClick, type: mockType, $eval: mock$eval, evaluate: mockEvaluate, screenshot: mockScreenshot, $: mock$, keyboard: mockKeyboard, isClosed: () => false, }; const mockManager = { getPage: vi.fn().mockResolvedValue(mockPage), } as unknown as BrowserManager; describe('Browser tools', () => { let tools: ReturnType; beforeEach(() => { vi.clearAllMocks(); tools = createBrowserTools(mockManager); }); it('creates all browser tools', () => { const names = tools.map(t => t.name); expect(names).toContain('browser.navigate'); expect(names).toContain('browser.screenshot'); expect(names).toContain('browser.click'); expect(names).toContain('browser.type'); expect(names).toContain('browser.content'); expect(names).toContain('browser.eval'); expect(names).toHaveLength(6); }); it('browser.navigate navigates to URL', async () => { const tool = tools.find(t => t.name === 'browser.navigate')!; const result = await tool.execute({ url: 'https://example.com' }); expect(result.success).toBe(true); expect(result.output).toContain('example.com'); expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'domcontentloaded' }); }); it('browser.navigate respects custom waitUntil', async () => { const tool = tools.find(t => t.name === 'browser.navigate')!; await tool.execute({ url: 'https://example.com', waitUntil: 'networkidle0' }); expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'networkidle0' }); }); it('browser.screenshot takes page screenshot', async () => { const tool = tools.find(t => t.name === 'browser.screenshot')!; const result = await tool.execute({}); expect(result.success).toBe(true); expect(result.output).toContain('Screenshot captured'); expect(mockScreenshot).toHaveBeenCalledWith({ encoding: 'base64', fullPage: false }); }); it('browser.screenshot takes element screenshot', async () => { const tool = tools.find(t => t.name === 'browser.screenshot')!; const result = await tool.execute({ selector: '#header' }); expect(result.success).toBe(true); expect(mock$).toHaveBeenCalledWith('#header'); }); it('browser.screenshot fails for missing element', async () => { mock$.mockResolvedValueOnce(null); const tool = tools.find(t => t.name === 'browser.screenshot')!; const result = await tool.execute({ selector: '#nonexistent' }); expect(result.success).toBe(false); expect(result.error).toContain('Element not found'); }); it('browser.click clicks element', async () => { const tool = tools.find(t => t.name === 'browser.click')!; const result = await tool.execute({ selector: '#submit' }); expect(result.success).toBe(true); expect(mockClick).toHaveBeenCalledWith('#submit'); }); it('browser.type types into element', async () => { const tool = tools.find(t => t.name === 'browser.type')!; const result = await tool.execute({ selector: '#search', text: 'hello' }); expect(result.success).toBe(true); expect(mockType).toHaveBeenCalledWith('#search', 'hello'); }); it('browser.type clears field before typing when clear=true', async () => { const tool = tools.find(t => t.name === 'browser.type')!; await tool.execute({ selector: '#search', text: 'hello', clear: true }); expect(mockClick).toHaveBeenCalledWith('#search', { count: 3 }); expect(mockKeyboard.press).toHaveBeenCalledWith('Backspace'); expect(mockType).toHaveBeenCalledWith('#search', 'hello'); }); it('browser.content returns page text', async () => { const tool = tools.find(t => t.name === 'browser.content')!; const result = await tool.execute({}); expect(result.success).toBe(true); expect(result.output).toContain('Page content here'); expect(result.output).toContain('example.com'); expect(result.output).toContain('Test Page'); }); it('browser.content uses custom selector', async () => { const tool = tools.find(t => t.name === 'browser.content')!; await tool.execute({ selector: '#main' }); expect(mock$eval).toHaveBeenCalledWith('#main', expect.any(Function)); }); it('browser.eval evaluates JS', async () => { const tool = tools.find(t => t.name === 'browser.eval')!; const result = await tool.execute({ expression: '1 + 1' }); expect(result.success).toBe(true); expect(result.output).toContain('42'); }); it('browser.eval returns string results directly', async () => { mockEvaluate.mockResolvedValueOnce('hello world'); const tool = tools.find(t => t.name === 'browser.eval')!; const result = await tool.execute({ expression: '"hello world"' }); expect(result.success).toBe(true); expect(result.output).toBe('hello world'); }); it('handles navigation errors gracefully', async () => { mockGoto.mockRejectedValueOnce(new Error('Navigation failed')); const tool = tools.find(t => t.name === 'browser.navigate')!; const result = await tool.execute({ url: 'https://broken.example.com' }); expect(result.success).toBe(false); expect(result.error).toContain('Navigation failed'); }); it('handles click errors gracefully', async () => { mockClick.mockRejectedValueOnce(new Error('Element not found')); const tool = tools.find(t => t.name === 'browser.click')!; const result = await tool.execute({ selector: '#missing' }); expect(result.success).toBe(false); expect(result.error).toContain('Element not found'); }); it('returns aborted error when signal is already aborted', async () => { const tool = tools.find(t => t.name === 'browser.navigate')!; const controller = new AbortController(); controller.abort(); const result = await tool.execute({ url: 'https://example.com' }, { signal: controller.signal }); expect(result.success).toBe(false); expect(result.error).toContain('aborted'); expect(mockManager.getPage).not.toHaveBeenCalled(); expect(mockGoto).not.toHaveBeenCalled(); }); });