55fc75eb54
- 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
240 lines
8.1 KiB
TypeScript
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);
|
|
});
|
|
});
|