💬 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:
Adam Ladachowski
2026-02-06 21:43:28 +01:00
parent 2736a4b467
commit 880e24877d
20 changed files with 5254 additions and 5 deletions
+90
View File
@@ -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');
}
});
});
});
+125
View File
@@ -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);
});
});
+123
View File
@@ -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);
});
});
});