Files
Immich/web/test/medium/mocks/api.ts
T
Jason Rasmussen 8e77a8c6f8 refactor: web tests (#32983)
* refactor: web tests

* fix: e2e ratelimits

* fix: prefer data-testid

* chore: pr feedback
2026-11-05 20:36:37 +03:00

107 lines
3.3 KiB
TypeScript

import type { MockData } from '#test/common/data.ts';
import { Dispatcher, type Response } from '#test/common/dispatcher.ts';
import type { BrowserContext, Request, Route, WebSocketRoute } from '@playwright/test';
const apiPattern = /^https?:\/\/[^/]+\/api\//;
const socketPattern = /\/api\/socket\.io\//;
const customCssPattern = /^https?:\/\/[^/]+\/custom\.css$/;
const mapStylePattern = /^https:\/\/tiles\.immich\.cloud\//;
const emptyMapStyle = { version: 8, sources: {}, layers: [] };
const sockets = new WeakMap<BrowserContext, WebSocketRoute>();
const socketMockedContexts = new WeakSet<BrowserContext>();
const mockWebSocket = async (context: BrowserContext) => {
if (socketMockedContexts.has(context)) {
return;
}
socketMockedContexts.add(context);
await context.routeWebSocket(socketPattern, (socket) => {
sockets.set(context, socket);
socket.send('0{"sid":"mock","upgrades":[],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}');
socket.onMessage((message) => {
if (message === '2') {
socket.send('3');
} else if (typeof message === 'string' && message.startsWith('40')) {
socket.send('40{"sid":"mock"}');
}
});
});
};
const fulfill = async (route: Route, { status, json, body, contentType, headers }: Response) => {
try {
await route.fulfill(
body === undefined
? { status, json, headers }
: { status, body: typeof body === 'string' ? body : Buffer.from(body), contentType, headers },
);
} catch {
return;
}
};
const parseBody = (request: Request) => {
const data = request.postData();
if (data === null) {
return;
}
try {
return JSON.parse(data) as unknown;
} catch {
return data;
}
};
export class MockNetwork extends Dispatcher {
private readonly routedUrls = new Set<string>();
private readonly disposers: Array<() => Promise<void>> = [];
constructor(
private readonly context: BrowserContext,
data: MockData,
) {
super(data);
}
async setup() {
await mockWebSocket(this.context);
await this.route(customCssPattern, (route) => route.fulfill({ body: '', contentType: 'text/css' }));
await this.route(mapStylePattern, (route) => route.fulfill({ json: emptyMapStyle }));
await this.route(apiPattern, (route, request) => this.handle(route, request));
}
async teardown() {
for (const dispose of this.disposers.splice(0).reverse()) {
await dispose();
}
}
emit(event: string, payload?: unknown) {
const socket = sockets.get(this.context);
if (!socket) {
throw new Error('No websocket connection has been established by the page');
}
socket.send(`42${JSON.stringify(payload === undefined ? [event] : [event, payload])}`);
}
override recordDownload(url: URL) {
if (this.routedUrls.has(url.href)) {
return;
}
super.recordDownload(url);
}
private async route(pattern: RegExp, handler: (route: Route, request: Request) => Promise<void>) {
await this.context.route(pattern, handler);
this.disposers.push(() => this.context.unroute(pattern));
}
private async handle(route: Route, request: Request) {
const url = new URL(request.url());
this.routedUrls.add(url.href);
const response = await this.dispatch(request.method(), url, parseBody(request));
await fulfill(route, response);
}
}