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
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user