149 lines
5.0 KiB
TypeScript
149 lines
5.0 KiB
TypeScript
import type { Page } from '@playwright/test';
|
|
import { RESPONSE_MAPPERS } from './mappers';
|
|
import { HTTP_ROUTES } from './routes';
|
|
|
|
type ElectronApi = {
|
|
emit?: (name: string, data: unknown) => Promise<unknown>;
|
|
on?: (callback: (payload: { event: unknown; value: unknown }) => void) => () => void;
|
|
};
|
|
|
|
/**
|
|
* Invoke a bridge provider from renderer test context.
|
|
*
|
|
* Preferred path: map `key` to a backend HTTP route and issue a `fetch` from
|
|
* the renderer against `window.__backendPort`. This matches how the running
|
|
* app talks to aioncore.
|
|
*
|
|
* Fallback: for any key not in the route map, fall back to the legacy
|
|
* `@office-ai/platform` IPC protocol:
|
|
* emit('subscribe-{key}', { id, data }) -> on('subscribe.callback-{key}{id}', result)
|
|
*/
|
|
export async function invokeBridge<T = unknown>(
|
|
page: Page,
|
|
key: string,
|
|
data?: unknown,
|
|
timeoutMs = 10_000
|
|
): Promise<T> {
|
|
const route = HTTP_ROUTES[key];
|
|
if (route) {
|
|
const params = (data ?? {}) as Record<string, unknown>;
|
|
const resolvedPath = typeof route.path === 'function' ? route.path(params) : route.path;
|
|
const body =
|
|
route.method === 'GET' || route.method === 'DELETE' ? undefined : route.mapBody ? route.mapBody(params) : data;
|
|
|
|
const raw = await page.evaluate(
|
|
async ({ method, path, requestBody, requestTimeoutMs }) => {
|
|
const port = (window as unknown as { __backendPort?: number }).__backendPort;
|
|
if (!port) {
|
|
throw new Error('window.__backendPort is not available in renderer context');
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
|
|
try {
|
|
const headers: Record<string, string> = {};
|
|
if (requestBody !== undefined) headers['Content-Type'] = 'application/json';
|
|
|
|
const requestInit: RequestInit = {
|
|
method,
|
|
headers,
|
|
signal: controller.signal,
|
|
};
|
|
if (requestBody !== undefined && method !== 'GET') {
|
|
requestInit.body = JSON.stringify(requestBody);
|
|
}
|
|
|
|
const response = await fetch(`http://127.0.0.1:${port}${path}`, requestInit);
|
|
|
|
if (!response.ok) {
|
|
let errBody: unknown;
|
|
try {
|
|
errBody = await response.json();
|
|
} catch {
|
|
errBody = await response.text();
|
|
}
|
|
throw new Error(`Backend ${method} ${path} failed (${response.status}): ${JSON.stringify(errBody)}`);
|
|
}
|
|
|
|
const contentType = response.headers.get('Content-Type');
|
|
if (!contentType?.includes('application/json')) {
|
|
return undefined;
|
|
}
|
|
|
|
const json = (await response.json()) as { success?: boolean; data?: unknown };
|
|
if (json && typeof json === 'object' && 'data' in json) {
|
|
return json.data;
|
|
}
|
|
return json;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
},
|
|
{
|
|
method: route.method,
|
|
path: resolvedPath,
|
|
requestBody: body,
|
|
requestTimeoutMs: timeoutMs,
|
|
}
|
|
);
|
|
|
|
return (route.mapResponse ? RESPONSE_MAPPERS[route.mapResponse](raw) : raw) as T;
|
|
}
|
|
|
|
// Fallback: legacy IPC subscribe/callback protocol.
|
|
return page.evaluate(
|
|
async ({ requestKey, requestData, requestTimeoutMs }) => {
|
|
const api = (window as unknown as { electronAPI?: ElectronApi }).electronAPI;
|
|
if (!api?.emit || !api?.on) {
|
|
throw new Error('electronAPI bridge is unavailable in renderer context');
|
|
}
|
|
|
|
const id = `e2e_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
|
|
const callbackEventName = `subscribe.callback-${requestKey}${id}`;
|
|
const requestEventName = `subscribe-${requestKey}`;
|
|
|
|
return new Promise<unknown>((resolve, reject) => {
|
|
let settled = false;
|
|
const off = api.on?.((payload) => {
|
|
try {
|
|
const rawValue = payload?.value;
|
|
const parsed =
|
|
typeof rawValue === 'string'
|
|
? (JSON.parse(rawValue) as { name?: string; data?: unknown })
|
|
: (rawValue as { name?: string; data?: unknown });
|
|
if (parsed?.name !== callbackEventName) return;
|
|
if (settled) return;
|
|
settled = true;
|
|
off?.();
|
|
clearTimeout(timer);
|
|
resolve(parsed.data);
|
|
} catch (error) {
|
|
if (settled) return;
|
|
settled = true;
|
|
off?.();
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
const timer = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
off?.();
|
|
reject(new Error(`Bridge invoke timeout: ${requestKey}`));
|
|
}, requestTimeoutMs);
|
|
|
|
api.emit?.(requestEventName, { id, data: requestData }).catch((error) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
off?.();
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
});
|
|
});
|
|
},
|
|
{ requestKey: key, requestData: data, requestTimeoutMs: timeoutMs }
|
|
) as Promise<T>;
|
|
}
|