feat: add VisionLClient HTTP client for daemon communication
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
|
import http from 'node:http';
|
||||||
|
import { VisionLClient } from '../client.js';
|
||||||
|
import type { ApiResponse, PageInfo } from '../types/api.js';
|
||||||
|
|
||||||
|
function createMockServer() {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
const url = new URL(req.url!, 'http://localhost');
|
||||||
|
|
||||||
|
if (req.method === 'GET' && url.pathname === '/health') {
|
||||||
|
res.end(JSON.stringify({ status: 'ok' }));
|
||||||
|
} else if (req.method === 'POST' && url.pathname === '/pages') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', (chunk) => { body += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
const { url: pageUrl, alias } = JSON.parse(body);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
data: { id: 'p_a1b2c3d4', url: pageUrl, alias, title: 'Test Page', status: 'active', profile: 'desktop-chrome' }
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
} else if (req.method === 'GET' && url.pathname === '/pages') {
|
||||||
|
res.end(JSON.stringify({ ok: true, data: [{ id: 'p_test', url: 'https://test.com', title: 'Test', status: 'active' }] }));
|
||||||
|
} else if (req.method === 'GET' && url.pathname.startsWith('/pages/p_test/text')) {
|
||||||
|
res.end(JSON.stringify({ ok: true, data: { text: 'Hello World' } }));
|
||||||
|
} else {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end(JSON.stringify({ ok: false, error: { code: 'PAGE_NOT_FOUND', message: 'not found' } }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VisionLClient', () => {
|
||||||
|
let server: http.Server;
|
||||||
|
let client: VisionLClient;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = createMockServer();
|
||||||
|
await new Promise<void>((resolve) => server.listen(19527, resolve));
|
||||||
|
client = new VisionLClient('http://127.0.0.1:19527');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('health returns true when daemon is up', async () => {
|
||||||
|
const result = await client.health();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('openPage returns page info', async () => {
|
||||||
|
const result = await client.openPage('https://example.com', 'demo');
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(result.data!.id).toBe('p_a1b2c3d4');
|
||||||
|
expect(result.data!.alias).toBe('demo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('listPages returns array', async () => {
|
||||||
|
const result = await client.listPages();
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(result.data!).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('text returns page content', async () => {
|
||||||
|
const result = await client.text('p_test');
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(result.data!.text).toBe('Hello World');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type { ApiResponse, PageInfo } from './types/api.js';
|
||||||
|
|
||||||
|
export class VisionLClient {
|
||||||
|
constructor(private baseUrl: string) {}
|
||||||
|
|
||||||
|
private async request<T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> {
|
||||||
|
const url = `${this.baseUrl}${path}`;
|
||||||
|
const options: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
if (body !== undefined) {
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
const json = await response.json() as ApiResponse<T>;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async health(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const res = await this.request<{ status: string }>('GET', '/health');
|
||||||
|
return res.ok || (res.data as any)?.status === 'ok' || (res as any).status === 'ok';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async openPage(url: string, alias?: string, profile?: string): Promise<ApiResponse<PageInfo>> {
|
||||||
|
return this.request<PageInfo>('POST', '/pages', { url, alias, profile });
|
||||||
|
}
|
||||||
|
|
||||||
|
async listPages(): Promise<ApiResponse<PageInfo[]>> {
|
||||||
|
return this.request<PageInfo[]>('GET', '/pages');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPage(id: string): Promise<ApiResponse<PageInfo>> {
|
||||||
|
return this.request<PageInfo>('GET', `/pages/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async killPage(id: string): Promise<ApiResponse<null>> {
|
||||||
|
return this.request<null>('DELETE', `/pages/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigate(id: string, url: string): Promise<ApiResponse<{ url: string; title: string }>> {
|
||||||
|
return this.request('POST', `/pages/${id}/navigate`, { url });
|
||||||
|
}
|
||||||
|
|
||||||
|
async click(id: string, selector: string): Promise<ApiResponse<{ success: boolean }>> {
|
||||||
|
return this.request('POST', `/pages/${id}/click`, { selector });
|
||||||
|
}
|
||||||
|
|
||||||
|
async type(id: string, selector: string, text: string): Promise<ApiResponse<{ success: boolean }>> {
|
||||||
|
return this.request('POST', `/pages/${id}/type`, { selector, text });
|
||||||
|
}
|
||||||
|
|
||||||
|
async scroll(id: string, opts: { deltaY?: number; toBottom?: boolean }): Promise<ApiResponse<{ success: boolean }>> {
|
||||||
|
return this.request('POST', `/pages/${id}/scroll`, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async eval(id: string, code: string): Promise<ApiResponse<{ result: unknown }>> {
|
||||||
|
return this.request('POST', `/pages/${id}/eval`, { code });
|
||||||
|
}
|
||||||
|
|
||||||
|
async wait(id: string, opts: { selector?: string; ms?: number }): Promise<ApiResponse<{ success: boolean }>> {
|
||||||
|
return this.request('POST', `/pages/${id}/wait`, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async screenshot(id: string): Promise<ApiResponse<{ base64: string; mime: string }>> {
|
||||||
|
return this.request('GET', `/pages/${id}/screenshot`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async text(id: string): Promise<ApiResponse<{ text: string }>> {
|
||||||
|
return this.request('GET', `/pages/${id}/text`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async html(id: string): Promise<ApiResponse<{ html: string }>> {
|
||||||
|
return this.request('GET', `/pages/${id}/html`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProfiles(): Promise<ApiResponse<Array<{ id: string; name: string }>>> {
|
||||||
|
return this.request('GET', '/profiles');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,3 +9,4 @@ export {
|
|||||||
type PermissionState,
|
type PermissionState,
|
||||||
} from './types/fingerprint.js';
|
} from './types/fingerprint.js';
|
||||||
export { safeStringify, isValidJson } from './escape.js';
|
export { safeStringify, isValidJson } from './escape.js';
|
||||||
|
export { VisionLClient } from './client.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user