💬 Commit message: Update 2026-02-06 21:43:28, 20 files, 5493 lines
📁 Files changed: 20 📝 Lines changed: 5493 • base.css • block-navigation.js • browser.ts.html • clover.xml • coverage-final.json • favicon.png • index.html • logger.ts.html • prettify.css • prettify.js • server.ts.html • sort-arrow-sprite.png • sorter.js • types.ts.html • package-lock.json • package.json • browser.test.ts • logger.test.ts • server.test.ts • vitest.config.ts
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ClaudeBrowser } from './browser.js';
|
||||
|
||||
describe('ClaudeBrowser', () => {
|
||||
describe('constructor', () => {
|
||||
it('creates browser with default options', () => {
|
||||
const browser = new ClaudeBrowser();
|
||||
expect(browser).toBeInstanceOf(ClaudeBrowser);
|
||||
});
|
||||
|
||||
it('accepts custom options', () => {
|
||||
const browser = new ClaudeBrowser({
|
||||
headless: false,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
});
|
||||
expect(browser).toBeInstanceOf(ClaudeBrowser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensurePage (via executeCommand)', () => {
|
||||
it('throws error when browser not launched', async () => {
|
||||
const browser = new ClaudeBrowser();
|
||||
const result = await browser.executeCommand({ cmd: 'url' });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain('Browser not launched');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeCommand', () => {
|
||||
it('returns error for commands without launch', async () => {
|
||||
const browser = new ClaudeBrowser();
|
||||
|
||||
const gotoResult = await browser.executeCommand({ cmd: 'goto', url: 'https://example.com' });
|
||||
expect(gotoResult.ok).toBe(false);
|
||||
|
||||
const clickResult = await browser.executeCommand({ cmd: 'click', selector: '#btn' });
|
||||
expect(clickResult.ok).toBe(false);
|
||||
|
||||
const typeResult = await browser.executeCommand({
|
||||
cmd: 'type',
|
||||
selector: '#input',
|
||||
text: 'test',
|
||||
});
|
||||
expect(typeResult.ok).toBe(false);
|
||||
|
||||
const queryResult = await browser.executeCommand({ cmd: 'query', selector: '.item' });
|
||||
expect(queryResult.ok).toBe(false);
|
||||
|
||||
const screenshotResult = await browser.executeCommand({ cmd: 'screenshot' });
|
||||
expect(screenshotResult.ok).toBe(false);
|
||||
|
||||
const htmlResult = await browser.executeCommand({ cmd: 'html' });
|
||||
expect(htmlResult.ok).toBe(false);
|
||||
|
||||
const backResult = await browser.executeCommand({ cmd: 'back' });
|
||||
expect(backResult.ok).toBe(false);
|
||||
|
||||
const forwardResult = await browser.executeCommand({ cmd: 'forward' });
|
||||
expect(forwardResult.ok).toBe(false);
|
||||
|
||||
const reloadResult = await browser.executeCommand({ cmd: 'reload' });
|
||||
expect(reloadResult.ok).toBe(false);
|
||||
|
||||
const waitResult = await browser.executeCommand({ cmd: 'wait', ms: 100 });
|
||||
expect(waitResult.ok).toBe(false);
|
||||
|
||||
const evalResult = await browser.executeCommand({ cmd: 'eval', script: '1+1' });
|
||||
expect(evalResult.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('handles close command without error when not launched', async () => {
|
||||
const browser = new ClaudeBrowser();
|
||||
const result = await browser.executeCommand({ cmd: 'close' });
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('handles newpage command error when not launched', async () => {
|
||||
const browser = new ClaudeBrowser();
|
||||
const result = await browser.executeCommand({ cmd: 'newpage' });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain('Browser not launched');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
createLogger,
|
||||
formatCommand,
|
||||
formatResult,
|
||||
getCommandDetail,
|
||||
icons,
|
||||
truncate,
|
||||
} from './logger.js';
|
||||
|
||||
describe('truncate', () => {
|
||||
it('returns string unchanged if shorter than max', () => {
|
||||
expect(truncate('hello', 10)).toBe('hello');
|
||||
});
|
||||
|
||||
it('truncates and adds ellipsis if longer than max', () => {
|
||||
expect(truncate('hello world', 5)).toBe('hello...');
|
||||
});
|
||||
|
||||
it('handles exact length', () => {
|
||||
expect(truncate('hello', 5)).toBe('hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('icons', () => {
|
||||
it('has icon for each command type', () => {
|
||||
expect(icons.goto).toBe('→');
|
||||
expect(icons.click).toBe('◉');
|
||||
expect(icons.type).toBe('⌨');
|
||||
expect(icons.screenshot).toBe('📷');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCommandDetail', () => {
|
||||
it('returns url for goto command', () => {
|
||||
const result = getCommandDetail({ cmd: 'goto', url: 'https://example.com' });
|
||||
expect(result).toContain('https://example.com');
|
||||
});
|
||||
|
||||
it('returns selector for click command', () => {
|
||||
const result = getCommandDetail({ cmd: 'click', selector: '#btn' });
|
||||
expect(result).toContain('#btn');
|
||||
});
|
||||
|
||||
it('returns selector and text for type command', () => {
|
||||
const result = getCommandDetail({ cmd: 'type', selector: '#input', text: 'hello' });
|
||||
expect(result).toContain('#input');
|
||||
expect(result).toContain('hello');
|
||||
});
|
||||
|
||||
it('returns path for screenshot command', () => {
|
||||
const result = getCommandDetail({ cmd: 'screenshot', path: 'test.png' });
|
||||
expect(result).toContain('test.png');
|
||||
});
|
||||
|
||||
it('returns default path when none provided for screenshot', () => {
|
||||
const result = getCommandDetail({ cmd: 'screenshot' });
|
||||
expect(result).toContain('screenshot.png');
|
||||
});
|
||||
|
||||
it('returns undefined for url command', () => {
|
||||
expect(getCommandDetail({ cmd: 'url' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns ms for wait command', () => {
|
||||
const result = getCommandDetail({ cmd: 'wait', ms: 500 });
|
||||
expect(result).toContain('500');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatCommand', () => {
|
||||
it('formats goto command', () => {
|
||||
const result = formatCommand({ cmd: 'goto', url: 'https://example.com' });
|
||||
expect(result).toContain('GOTO');
|
||||
expect(result).toContain('https://example.com');
|
||||
});
|
||||
|
||||
it('formats click command', () => {
|
||||
const result = formatCommand({ cmd: 'click', selector: '#btn' });
|
||||
expect(result).toContain('CLICK');
|
||||
expect(result).toContain('#btn');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatResult', () => {
|
||||
it('formats error result', () => {
|
||||
const result = formatResult({ cmd: 'goto' }, { ok: false, error: 'Failed' });
|
||||
expect(result).toContain('Failed');
|
||||
});
|
||||
|
||||
it('formats successful goto result with title', () => {
|
||||
const result = formatResult({ cmd: 'goto' }, { ok: true, title: 'Example' });
|
||||
expect(result).toContain('Example');
|
||||
});
|
||||
|
||||
it('formats query result with count', () => {
|
||||
const result = formatResult({ cmd: 'query' }, { ok: true, count: 5 });
|
||||
expect(result).toContain('5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLogger', () => {
|
||||
it('creates logger with custom log function', () => {
|
||||
const logs: string[] = [];
|
||||
const logFn = (msg: string) => logs.push(msg);
|
||||
const logger = createLogger(logFn);
|
||||
|
||||
logger.command({ cmd: 'goto', url: 'https://example.com' });
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0]).toContain('GOTO');
|
||||
|
||||
logger.result({ cmd: 'goto' }, { ok: true, title: 'Example' });
|
||||
expect(logs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('logs command and result', () => {
|
||||
const spy = vi.fn();
|
||||
const logger = createLogger(spy);
|
||||
|
||||
logger.command({ cmd: 'click', selector: '#btn' });
|
||||
logger.result({ cmd: 'click' }, { ok: true, url: '/page' });
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { BrowserServer } from './server.js';
|
||||
|
||||
// Mock the browser module
|
||||
vi.mock('./browser.js', () => ({
|
||||
ClaudeBrowser: class MockClaudeBrowser {
|
||||
async launch() {}
|
||||
async close() {}
|
||||
async executeCommand(cmd: { cmd: string; url?: string }) {
|
||||
switch (cmd.cmd) {
|
||||
case 'goto':
|
||||
return { ok: true, url: cmd.url, title: 'Test Page' };
|
||||
case 'click':
|
||||
return { ok: true, url: '/clicked' };
|
||||
case 'type':
|
||||
return { ok: true };
|
||||
case 'query':
|
||||
return { ok: true, count: 2, elements: [] };
|
||||
case 'url':
|
||||
return { ok: true, url: 'https://example.com', title: 'Example' };
|
||||
case 'html':
|
||||
return { ok: true, html: '<html></html>' };
|
||||
default:
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('BrowserServer', () => {
|
||||
let server: BrowserServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = new BrowserServer({ port: 0 }); // Use port 0 to get random available port
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('creates server with default port', () => {
|
||||
const s = new BrowserServer();
|
||||
expect(s.getPort()).toBe(13373);
|
||||
});
|
||||
|
||||
it('creates server with custom port', () => {
|
||||
const s = new BrowserServer({ port: 8080 });
|
||||
expect(s.getPort()).toBe(8080);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /', () => {
|
||||
it('handles goto command', async () => {
|
||||
const app = server.getApp();
|
||||
const res = await request(app)
|
||||
.post('/')
|
||||
.send({ cmd: 'goto', url: 'https://example.com' })
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.ok).toBe(true);
|
||||
expect(res.body.url).toBe('https://example.com');
|
||||
expect(res.body.title).toBe('Test Page');
|
||||
});
|
||||
|
||||
it('handles click command', async () => {
|
||||
const app = server.getApp();
|
||||
const res = await request(app).post('/').send({ cmd: 'click', selector: '#btn' }).expect(200);
|
||||
|
||||
expect(res.body.ok).toBe(true);
|
||||
expect(res.body.url).toBe('/clicked');
|
||||
});
|
||||
|
||||
it('handles type command', async () => {
|
||||
const app = server.getApp();
|
||||
const res = await request(app)
|
||||
.post('/')
|
||||
.send({ cmd: 'type', selector: '#input', text: 'hello' })
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('handles query command', async () => {
|
||||
const app = server.getApp();
|
||||
const res = await request(app)
|
||||
.post('/')
|
||||
.send({ cmd: 'query', selector: '.items' })
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.ok).toBe(true);
|
||||
expect(res.body.count).toBe(2);
|
||||
});
|
||||
|
||||
it('handles url command', async () => {
|
||||
const app = server.getApp();
|
||||
const res = await request(app).post('/').send({ cmd: 'url' }).expect(200);
|
||||
|
||||
expect(res.body.ok).toBe(true);
|
||||
expect(res.body.url).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('handles html command', async () => {
|
||||
const app = server.getApp();
|
||||
const res = await request(app).post('/').send({ cmd: 'html' }).expect(200);
|
||||
|
||||
expect(res.body.ok).toBe(true);
|
||||
expect(res.body.html).toBe('<html></html>');
|
||||
});
|
||||
|
||||
it('handles JSON string body', async () => {
|
||||
const app = server.getApp();
|
||||
const res = await request(app)
|
||||
.post('/')
|
||||
.set('Content-Type', 'text/plain')
|
||||
.send(JSON.stringify({ cmd: 'url' }))
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user