Files
2026-07-13 13:20:22 +08:00

77 lines
2.8 KiB
TypeScript

import type { Page } from '@playwright/test';
/**
* HTTP bridge helper for E2E tests.
*
* The renderer migrated from IPC `invokeBridge('subscribe-<key>')` to direct
* HTTP calls against `aioncore` via `fetch('http://127.0.0.1:<port>/api/...')`.
* The backend port is exposed on `window.__backendPort` by the preload script
* (`src/preload/main.ts:71`).
*
* These helpers drive backend calls from the renderer context (via `page.evaluate`)
* so tests execute in the same network context the app itself uses — identical
* port, identical base URL, no host-side HTTP plumbing.
*
* Backend responses are wrapped as `{ success, data, ... }`. This helper unwraps
* `data` when present, matching `httpBridge.ts:76` in the renderer adapter.
*/
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
export async function httpInvoke<T = unknown>(
page: Page,
method: HttpMethod,
path: string,
body?: unknown
): Promise<T> {
return page.evaluate(
async ({ method: m, path: p, body: b }) => {
const port = (window as unknown as { __backendPort?: number }).__backendPort ?? 13400;
const url = `http://127.0.0.1:${port}${p}`;
// DELETE routes require Content-Type: application/json AND a JSON-parseable
// body even when the operation takes no body (e.g. DELETE /api/skills/external-paths
// where the path is in the query string). Send `{}` as default body for DELETE.
const effectiveBody = b !== undefined ? b : m === 'DELETE' ? {} : undefined;
const headers: Record<string, string> = {};
if (effectiveBody !== undefined) headers['Content-Type'] = 'application/json';
const requestInit: RequestInit = {
method: m,
headers,
};
if (effectiveBody !== undefined && m !== 'GET') {
requestInit.body = JSON.stringify(effectiveBody);
}
const res = await fetch(url, requestInit);
if (!res.ok) {
let errText: string;
try {
errText = JSON.stringify(await res.json());
} catch {
errText = await res.text();
}
throw new Error(`Backend ${m} ${p} failed (${res.status}): ${errText}`);
}
const contentType = res.headers.get('Content-Type');
if (!contentType?.includes('application/json')) {
return undefined;
}
const json = await res.json();
if (json && typeof json === 'object' && 'data' in json) {
return (json as { data: unknown }).data;
}
return json;
},
{ method, path, body }
) as Promise<T>;
}
export const httpGet = <T = unknown>(page: Page, path: string) => httpInvoke<T>(page, 'GET', path);
export const httpPost = <T = unknown>(page: Page, path: string, body?: unknown) =>
httpInvoke<T>(page, 'POST', path, body);
export const httpDelete = <T = unknown>(page: Page, path: string) => httpInvoke<T>(page, 'DELETE', path);