mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
119 lines
4.7 KiB
TypeScript
119 lines
4.7 KiB
TypeScript
import type { Page, Route } from "@playwright/test"
|
|
import type { MockRequest, MockResponse, PrepareContext } from "@hona/strobe"
|
|
|
|
type Handler = Parameters<Page["route"]>[1]
|
|
|
|
type Fulfill = {
|
|
status?: number
|
|
headers?: Record<string, string>
|
|
body?: string | Buffer
|
|
json?: object
|
|
contentType?: string
|
|
}
|
|
|
|
/**
|
|
* The block's page for the e2e mock server and the fixtures, with its routes answered through strobe's `mock`
|
|
* (`prepare`'s `ctx.mock`). Playwright's routing turns the browser's HTTP cache off for the whole page, so a case that
|
|
* reloads in `reset` would refetch every script, which no person does; `mock` pauses only the mocked URLs. Routes keep
|
|
* Playwright's order (the latest answers first, `fallback` passes on), take URL globs and last for the whole case.
|
|
*
|
|
* In Chromium it is Playwright's page, so the mock server and its fixtures run as in the e2e suites. Other engines
|
|
* (`--engine firefox`) have no Playwright page: a stand-in takes init scripts through strobe's `addInitScript` and
|
|
* cannot evaluate in the page, so the mock server publishes no events there after a mutation.
|
|
*/
|
|
export function cached(ctx: Pick<PrepareContext<unknown>, "playwright" | "mock" | "addInitScript">) {
|
|
const page = playwrightPage(ctx) ?? standIn(ctx.addInitScript)
|
|
const { mock } = ctx
|
|
|
|
page.route = async (url, handler) => {
|
|
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- discriminates Playwright's own URL matcher union
|
|
if (typeof url !== "string") throw new Error("strobe mocks URL globs; no benchmark routes by RegExp or predicate")
|
|
|
|
await mock(url.replaceAll("**", "*"), (request) => answer(request, handler))
|
|
|
|
return { dispose: kept, [Symbol.asyncDispose]: kept }
|
|
}
|
|
|
|
// The mock terminal's socket: no benchmark opens a terminal.
|
|
page.routeWebSocket = async () => {}
|
|
|
|
return page
|
|
}
|
|
|
|
// `ctx.playwright` is Chromium's: reading it in another engine throws.
|
|
function playwrightPage(ctx: Pick<PrepareContext<unknown>, "playwright">): Page | undefined {
|
|
try {
|
|
// SAFETY: strobe hands Chromium blocks Playwright's own page.
|
|
return ctx.playwright.page as Page
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
// The members of a page the e2e mock server and the fixtures use, over strobe's calls. `route` is set by `cached`.
|
|
function standIn(addInitScript: PrepareContext<unknown>["addInitScript"]) {
|
|
const page = {
|
|
addInitScript,
|
|
// The block's browser closes with the block.
|
|
on: () => page,
|
|
isClosed: () => false,
|
|
// The mock server's event delivery treats a missing stream as retryable and drops the events.
|
|
evaluate: async () => {
|
|
throw new Error("No mock event stream: strobe drives this engine without Playwright")
|
|
},
|
|
}
|
|
|
|
// SAFETY: the e2e mock server and the benchmark fixtures call only these members (and `route`, `routeWebSocket`).
|
|
// oxlint-disable-next-line anti-slop/no-chained-type-assertions -- see SAFETY above
|
|
return page as unknown as Page
|
|
}
|
|
|
|
async function kept() {
|
|
throw new Error("a strobe mock cannot be removed")
|
|
}
|
|
|
|
// Runs a Playwright route handler against a paused request and resolves with what it fulfilled.
|
|
function answer(request: MockRequest, handler: Handler) {
|
|
const done = Promise.withResolvers<MockResponse | undefined>()
|
|
|
|
const headers = Object.fromEntries(
|
|
Object.entries(request.headers).map(([name, value]) => [name.toLowerCase(), value]),
|
|
)
|
|
|
|
const route = {
|
|
request: () => ({
|
|
url: () => request.url,
|
|
method: () => request.method,
|
|
headers: () => headers,
|
|
headerValue: async (name: string) => headers[name.toLowerCase()] ?? null,
|
|
postDataBuffer: () => (request.body === undefined ? null : Buffer.from(request.body)),
|
|
}),
|
|
fulfill: async (response: Fulfill) => done.resolve(fulfilled(response)),
|
|
fallback: async () => done.resolve(undefined),
|
|
continue: async () => done.resolve(undefined),
|
|
}
|
|
|
|
// SAFETY: the e2e mock server and the benchmark fixtures call only these members of a route and its request.
|
|
// oxlint-disable-next-line anti-slop/no-chained-type-assertions -- see SAFETY above
|
|
const playwright = route as unknown as Route
|
|
|
|
// A handler that throws after answering (an unmocked API request) reports in the runner's log.
|
|
Promise.resolve(handler(playwright, playwright.request())).catch((error) => {
|
|
console.error(error)
|
|
done.resolve(undefined)
|
|
})
|
|
|
|
return done.promise
|
|
}
|
|
|
|
function fulfilled(response: Fulfill): MockResponse {
|
|
const headers = { ...response.headers }
|
|
|
|
if (response.contentType) headers["content-type"] = response.contentType
|
|
|
|
if (response.json === undefined) return { status: response.status ?? 200, headers, body: response.body }
|
|
headers["content-type"] = "application/json"
|
|
|
|
return { status: response.status ?? 200, headers, body: JSON.stringify(response.json) }
|
|
}
|