diff --git a/package.json b/package.json index 8c13e80..7dc9548 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "dev": "tsx src/index.ts", "typecheck": "tsc --noEmit", "format": "prettier --write 'src/**/*.ts'", - "format:check": "prettier --check 'src/**/*.ts'" + "format:check": "prettier --check 'src/**/*.ts'", + "test": "vitest run", + "test:watch": "vitest" }, "keywords": [ "browser", @@ -33,6 +35,7 @@ "@types/node": "^20.10.0", "prettier": "^3.7.4", "tsx": "^4.6.0", - "typescript": "^5.3.0" + "typescript": "^5.3.0", + "vitest": "^4.0.16" } } diff --git a/src/browser.test.ts b/src/browser.test.ts new file mode 100644 index 0000000..b20ff71 --- /dev/null +++ b/src/browser.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { BrowserManager } from './browser.js'; + +describe('BrowserManager', () => { + let browser: BrowserManager; + + beforeAll(async () => { + browser = new BrowserManager(); + await browser.launch({ headless: true }); + }); + + afterAll(async () => { + await browser.close(); + }); + + describe('launch and close', () => { + it('should report as launched', () => { + expect(browser.isLaunched()).toBe(true); + }); + + it('should have a page', () => { + const page = browser.getPage(); + expect(page).toBeDefined(); + }); + }); + + describe('navigation', () => { + it('should navigate to URL', async () => { + const page = browser.getPage(); + await page.goto('https://example.com'); + expect(page.url()).toBe('https://example.com/'); + }); + + it('should get page title', async () => { + const page = browser.getPage(); + const title = await page.title(); + expect(title).toBe('Example Domain'); + }); + }); + + describe('element interaction', () => { + it('should find element by selector', async () => { + const page = browser.getPage(); + const heading = await page.locator('h1').textContent(); + expect(heading).toBe('Example Domain'); + }); + + it('should check element visibility', async () => { + const page = browser.getPage(); + const isVisible = await page.locator('h1').isVisible(); + expect(isVisible).toBe(true); + }); + + it('should count elements', async () => { + const page = browser.getPage(); + const count = await page.locator('p').count(); + expect(count).toBeGreaterThan(0); + }); + }); + + describe('screenshots', () => { + it('should take screenshot as buffer', async () => { + const page = browser.getPage(); + const buffer = await page.screenshot(); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.length).toBeGreaterThan(0); + }); + }); + + describe('evaluate', () => { + it('should evaluate JavaScript', async () => { + const page = browser.getPage(); + const result = await page.evaluate(() => document.title); + expect(result).toBe('Example Domain'); + }); + + it('should evaluate with arguments', async () => { + const page = browser.getPage(); + const result = await page.evaluate((x: number) => x * 2, 5); + expect(result).toBe(10); + }); + }); + + describe('tabs', () => { + it('should create new tab', async () => { + const result = await browser.newTab(); + expect(result.index).toBe(1); + expect(result.total).toBe(2); + }); + + it('should list tabs', async () => { + const tabs = await browser.listTabs(); + expect(tabs.length).toBe(2); + }); + + it('should close tab', async () => { + // Switch to second tab and close it + const page = browser.getPage(); + const tabs = await browser.listTabs(); + if (tabs.length > 1) { + const result = await browser.closeTab(1); + expect(result.remaining).toBe(1); + } + }); + }); + + describe('context operations', () => { + it('should get cookies from context', async () => { + const page = browser.getPage(); + const cookies = await page.context().cookies(); + expect(Array.isArray(cookies)).toBe(true); + }); + + it('should set and get cookies', async () => { + const page = browser.getPage(); + const context = page.context(); + await context.addCookies([{ name: 'test', value: 'value', url: 'https://example.com' }]); + const cookies = await context.cookies(); + const testCookie = cookies.find((c) => c.name === 'test'); + expect(testCookie?.value).toBe('value'); + }); + + it('should clear cookies', async () => { + const page = browser.getPage(); + const context = page.context(); + await context.clearCookies(); + const cookies = await context.cookies(); + expect(cookies.length).toBe(0); + }); + }); + + describe('storage via evaluate', () => { + it('should set and get localStorage', async () => { + const page = browser.getPage(); + await page.evaluate(() => localStorage.setItem('testKey', 'testValue')); + const value = await page.evaluate(() => localStorage.getItem('testKey')); + expect(value).toBe('testValue'); + }); + + it('should clear localStorage', async () => { + const page = browser.getPage(); + await page.evaluate(() => localStorage.clear()); + const value = await page.evaluate(() => localStorage.getItem('testKey')); + expect(value).toBeNull(); + }); + }); + + describe('viewport', () => { + it('should set viewport', async () => { + await browser.setViewport(1920, 1080); + const page = browser.getPage(); + const size = page.viewportSize(); + expect(size?.width).toBe(1920); + expect(size?.height).toBe(1080); + }); + }); +}); diff --git a/src/protocol.test.ts b/src/protocol.test.ts new file mode 100644 index 0000000..41e223a --- /dev/null +++ b/src/protocol.test.ts @@ -0,0 +1,216 @@ +import { describe, it, expect } from 'vitest'; +import { parseCommand } from './protocol.js'; + +// Helper to create command JSON string +const cmd = (obj: object) => JSON.stringify(obj); + +describe('parseCommand', () => { + describe('navigation', () => { + it('should parse navigate command', () => { + const result = parseCommand(cmd({ id: '1', action: 'navigate', url: 'https://example.com' })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('navigate'); + expect(result.command.url).toBe('https://example.com'); + } + }); + + it('should reject navigate without url', () => { + const result = parseCommand(cmd({ id: '1', action: 'navigate' })); + expect(result.success).toBe(false); + }); + }); + + describe('click', () => { + it('should parse click command', () => { + const result = parseCommand(cmd({ id: '1', action: 'click', selector: '#btn' })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('click'); + expect(result.command.selector).toBe('#btn'); + } + }); + + it('should reject click without selector', () => { + const result = parseCommand(cmd({ id: '1', action: 'click' })); + expect(result.success).toBe(false); + }); + }); + + describe('type', () => { + it('should parse type command', () => { + const result = parseCommand( + cmd({ id: '1', action: 'type', selector: '#input', text: 'hello' }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('type'); + expect(result.command.selector).toBe('#input'); + expect(result.command.text).toBe('hello'); + } + }); + }); + + describe('fill', () => { + it('should parse fill command', () => { + const result = parseCommand( + cmd({ id: '1', action: 'fill', selector: '#input', value: 'hello' }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('fill'); + expect(result.command.value).toBe('hello'); + } + }); + }); + + describe('wait', () => { + it('should parse wait with selector', () => { + const result = parseCommand(cmd({ id: '1', action: 'wait', selector: '#loading' })); + expect(result.success).toBe(true); + }); + + it('should parse wait with timeout', () => { + const result = parseCommand(cmd({ id: '1', action: 'wait', timeout: 5000 })); + expect(result.success).toBe(true); + }); + + it('should parse wait with text', () => { + const result = parseCommand(cmd({ id: '1', action: 'wait', text: 'Welcome' })); + expect(result.success).toBe(true); + }); + }); + + describe('screenshot', () => { + it('should parse screenshot command', () => { + const result = parseCommand(cmd({ id: '1', action: 'screenshot', path: 'test.png' })); + expect(result.success).toBe(true); + }); + + it('should parse screenshot with fullPage', () => { + const result = parseCommand(cmd({ id: '1', action: 'screenshot', fullPage: true })); + expect(result.success).toBe(true); + }); + }); + + describe('cookies', () => { + it('should parse cookies_get', () => { + const result = parseCommand(cmd({ id: '1', action: 'cookies_get' })); + expect(result.success).toBe(true); + }); + + it('should parse cookies_set', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'cookies_set', + cookies: [{ name: 'session', value: 'abc123' }], + }) + ); + expect(result.success).toBe(true); + }); + + it('should parse cookies_clear', () => { + const result = parseCommand(cmd({ id: '1', action: 'cookies_clear' })); + expect(result.success).toBe(true); + }); + }); + + describe('storage', () => { + it('should parse storage_get', () => { + const result = parseCommand(cmd({ id: '1', action: 'storage_get', type: 'local' })); + expect(result.success).toBe(true); + }); + + it('should parse storage_set', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'storage_set', + type: 'local', + key: 'test', + value: 'value', + }) + ); + expect(result.success).toBe(true); + }); + }); + + describe('semantic locators', () => { + it('should parse getbyrole', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'getbyrole', + role: 'button', + subaction: 'click', + }) + ); + expect(result.success).toBe(true); + }); + + it('should parse getbytext', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'getbytext', + text: 'Submit', + subaction: 'click', + }) + ); + expect(result.success).toBe(true); + }); + + it('should parse getbylabel', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'getbylabel', + label: 'Email', + subaction: 'fill', + value: 'test@test.com', + }) + ); + expect(result.success).toBe(true); + }); + }); + + describe('tabs', () => { + it('should parse tab_new', () => { + const result = parseCommand(cmd({ id: '1', action: 'tab_new' })); + expect(result.success).toBe(true); + }); + + it('should parse tab_list', () => { + const result = parseCommand(cmd({ id: '1', action: 'tab_list' })); + expect(result.success).toBe(true); + }); + + it('should parse tab_switch', () => { + const result = parseCommand(cmd({ id: '1', action: 'tab_switch', index: 0 })); + expect(result.success).toBe(true); + }); + + it('should parse tab_close', () => { + const result = parseCommand(cmd({ id: '1', action: 'tab_close' })); + expect(result.success).toBe(true); + }); + }); + + describe('invalid commands', () => { + it('should reject unknown action', () => { + const result = parseCommand(cmd({ id: '1', action: 'unknown' })); + expect(result.success).toBe(false); + }); + + it('should reject missing id', () => { + const result = parseCommand(cmd({ action: 'click', selector: '#btn' })); + expect(result.success).toBe(false); + }); + + it('should reject invalid JSON', () => { + const result = parseCommand('not json'); + expect(result.success).toBe(false); + }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..46ef8ae --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + include: ['src/**/*.test.ts'], + testTimeout: 30000, + }, +});