Files
VisionL/packages/daemon/src/__tests__/content-routes.test.ts
T
AskaEth 55fc75eb54 feat(daemon): implement content routes for screenshot/text/html retrieval (Task 9)
- Add contentRoutes handler factory in routes/content.ts
- GET /pages/:id/screenshot returns PNG as base64
- GET /pages/:id/text returns document.body.innerText
- GET /pages/:id/html returns document.documentElement.outerHTML
- All use safeStringify, 404 on PAGE_NOT_FOUND
- Register contentRoutes in server.ts
- Add 8 integration tests with mock BrowserManager
2026-08-12 20:58:37 +08:00

240 lines
8.1 KiB
TypeScript

// Content routes integration tests with mock BrowserManager / 内容路由集成测试,使用模拟浏览器管理器
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import http from 'node:http';
import { startServer } from '../server.js';
import type { PageInfo } from '@visionl/core';
function createMockBM() {
const pages = new Map<string, PageInfo>();
const aliases = new Map<string, string>();
const mockPage = {
async screenshot(_opts: any): Promise<Buffer> {
return Buffer.from('fake-png-data');
},
async evaluate(fn: any): Promise<any> {
const src = fn.toString();
if (src.includes('body.innerText')) return 'Hello World';
if (src.includes('documentElement.outerHTML')) return '<html><head></head><body>Hello World</body></html>';
return null;
},
};
const mock = {
async createPage(pageUrl: string, alias?: string): Promise<PageInfo> {
if (alias && aliases.has(alias)) {
throw Object.assign(new Error(`Alias "${alias}" already exists`), { code: 'ALIAS_EXISTS' });
}
try {
new URL(pageUrl);
} catch {
throw Object.assign(new Error(`Invalid URL: ${pageUrl}`), { code: 'INVALID_URL' });
}
const id = 'p_' + Math.random().toString(16).slice(2, 10);
const info: PageInfo = { id, url: pageUrl, alias, title: 'Test Page', status: 'active', profile: 'fp_test' };
pages.set(id, info);
if (alias) aliases.set(alias, id);
return info;
},
getPage(idOrAlias: string) {
const id = aliases.get(idOrAlias) || idOrAlias;
const info = pages.get(id);
if (!info) return undefined;
return { info, page: mockPage, context: null };
},
listPages(): PageInfo[] {
return Array.from(pages.values());
},
async closePage(idOrAlias: string): Promise<void> {
const id = aliases.get(idOrAlias) || idOrAlias;
if (!pages.has(id)) {
throw Object.assign(new Error(`Page "${idOrAlias}" not found`), { code: 'PAGE_NOT_FOUND' });
}
const info = pages.get(id)!;
if (info.alias) aliases.delete(info.alias);
pages.delete(id);
},
};
return { mock, pages, aliases, mockPage };
}
describe('content routes', () => {
let server: http.Server;
let baseUrl: string;
const port = 19530;
const { mock } = createMockBM();
beforeAll(async () => {
server = await startServer(port, mock as any);
baseUrl = `http://127.0.0.1:${port}`;
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
let mockPages: Map<string, PageInfo>;
let mockAliases: Map<string, string>;
function resetMock() {
mockPages = new Map();
mockAliases = new Map();
mock.createPage = async function (pageUrl: string, alias?: string): Promise<PageInfo> {
if (alias && mockAliases.has(alias)) {
throw Object.assign(new Error(`Alias "${alias}" already exists`), { code: 'ALIAS_EXISTS' });
}
try {
new URL(pageUrl);
} catch {
throw Object.assign(new Error(`Invalid URL: ${pageUrl}`), { code: 'INVALID_URL' });
}
const id = 'p_' + Math.random().toString(16).slice(2, 10);
const info: PageInfo = { id, url: pageUrl, alias, title: 'Test Page', status: 'active', profile: 'fp_test' };
mockPages.set(id, info);
if (alias) mockAliases.set(alias, id);
return info;
};
mock.getPage = function (idOrAlias: string) {
const id = mockAliases.get(idOrAlias) || idOrAlias;
const info = mockPages.get(id);
if (!info) return undefined;
return {
info,
page: {
async screenshot(_opts: any): Promise<Buffer> {
return Buffer.from('fake-png-data');
},
async evaluate(fn: any): Promise<any> {
const src = fn.toString();
if (src.includes('body.innerText')) return 'Hello World';
if (src.includes('documentElement.outerHTML')) return '<html><head></head><body>Hello World</body></html>';
return null;
},
},
context: null,
};
};
mock.listPages = function (): PageInfo[] {
return Array.from(mockPages.values());
};
mock.closePage = async function (idOrAlias: string): Promise<void> {
const id = mockAliases.get(idOrAlias) || idOrAlias;
if (!mockPages.has(id)) {
throw Object.assign(new Error(`Page "${idOrAlias}" not found`), { code: 'PAGE_NOT_FOUND' });
}
const info = mockPages.get(id)!;
if (info.alias) mockAliases.delete(info.alias);
mockPages.delete(id);
};
}
beforeEach(() => {
resetMock();
});
it('GET /pages/:id/screenshot returns base64 png', async () => {
const createRes = await fetch(`${baseUrl}/pages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com' }),
});
const { data: created } = await createRes.json();
const res = await fetch(`${baseUrl}/pages/${created.id}/screenshot`);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.ok).toBe(true);
expect(json.data.base64).toBe('ZmFrZS1wbmctZGF0YQ=='); // base64 of 'fake-png-data'
expect(json.data.mime).toBe('image/png');
});
it('GET /pages/:id/text returns body text', async () => {
const createRes = await fetch(`${baseUrl}/pages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com' }),
});
const { data: created } = await createRes.json();
const res = await fetch(`${baseUrl}/pages/${created.id}/text`);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.ok).toBe(true);
expect(json.data.text).toBe('Hello World');
});
it('GET /pages/:id/html returns full HTML', async () => {
const createRes = await fetch(`${baseUrl}/pages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com' }),
});
const { data: created } = await createRes.json();
const res = await fetch(`${baseUrl}/pages/${created.id}/html`);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.ok).toBe(true);
expect(json.data.html).toBe('<html><head></head><body>Hello World</body></html>');
});
it('GET /pages/:id/screenshot returns 404 for unknown page', async () => {
const res = await fetch(`${baseUrl}/pages/nonexistent/screenshot`);
expect(res.status).toBe(404);
const json = await res.json();
expect(json.ok).toBe(false);
expect(json.error.code).toBe('PAGE_NOT_FOUND');
});
it('GET /pages/:id/text returns 404 for unknown page', async () => {
const res = await fetch(`${baseUrl}/pages/nonexistent/text`);
expect(res.status).toBe(404);
const json = await res.json();
expect(json.error.code).toBe('PAGE_NOT_FOUND');
});
it('GET /pages/:id/html returns 404 for unknown page', async () => {
const res = await fetch(`${baseUrl}/pages/nonexistent/html`);
expect(res.status).toBe(404);
const json = await res.json();
expect(json.error.code).toBe('PAGE_NOT_FOUND');
});
it('screenshot accessible by alias', async () => {
await fetch(`${baseUrl}/pages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com', alias: 'home' }),
});
const res = await fetch(`${baseUrl}/pages/home/screenshot`);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.ok).toBe(true);
expect(json.data.base64).toBe('ZmFrZS1wbmctZGF0YQ==');
});
it('unknown content action returns false (falls through to 404)', async () => {
const createRes = await fetch(`${baseUrl}/pages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com' }),
});
const { data: created } = await createRes.json();
const res = await fetch(`${baseUrl}/pages/${created.id}/unknown`);
expect(res.status).toBe(404);
const json = await res.json();
expect(json.ok).toBe(false);
});
});