import { readFile, realpath, stat } from 'node:fs/promises'; import { createServer, type ServerResponse } from 'node:http'; import { extname, isAbsolute, relative, resolve, sep } from 'node:path'; import { expect, test as base, type Page } from '@playwright/test'; const LOOPBACK_HOST = '127.0.0.1'; const contentTypes: Readonly> = { '.avif': 'image/avif', '.br': 'application/octet-stream', '.css': 'text/css; charset=utf-8', '.gif': 'image/gif', '.html': 'text/html; charset=utf-8', '.ico': 'image/x-icon', '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.png': 'image/png', '.svg': 'image/svg+xml; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.webp': 'image/webp', '.woff': 'font/woff', '.woff2': 'font/woff2', '.xml': 'application/xml; charset=utf-8', }; const sendText = ( response: ServerResponse, status: number, body: string, ): void => { response.writeHead(status, { 'Cache-Control': 'no-store', 'Content-Length': Buffer.byteLength(body), 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff', }); response.end(body); }; const isInside = (root: string, candidate: string): boolean => { const pathFromRoot = relative(root, candidate); return ( pathFromRoot === '' || (!isAbsolute(pathFromRoot) && pathFromRoot !== '..' && !pathFromRoot.startsWith(`..${sep}`)) ); }; const decodedRequestPath = (requestTarget: string): string | null => { const rawPath = requestTarget.split(/[?#]/u, 1)[0] ?? '/'; let decoded: string; try { decoded = decodeURIComponent(rawPath); } catch { return null; } if ( !decoded.startsWith('/') || decoded.includes('\0') || decoded.includes('\\') ) { return null; } const segments = decoded.split('/'); if (segments.some((segment) => segment === '.' || segment === '..')) { return null; } return decoded === '/' || decoded.endsWith('/') ? `${decoded}index.html` : decoded; }; export interface DistPreviewServer { readonly origin: string; close(): Promise; } export async function expectInsetFocusIndicator( page: Page, selector: string, ): Promise { await expect .poll(() => page.locator(selector).evaluate((element) => { const rect = element.getBoundingClientRect(); const viewport = window.visualViewport; const viewportLeft = viewport?.offsetLeft ?? 0; const viewportTop = viewport?.offsetTop ?? 0; const viewportRight = viewportLeft + (viewport?.width ?? window.innerWidth); const viewportBottom = viewportTop + (viewport?.height ?? window.innerHeight); const style = getComputedStyle(element); return { focused: document.activeElement === element, intersectsViewport: rect.right > viewportLeft && rect.left < viewportRight && rect.bottom > viewportTop && rect.top < viewportBottom, insetIndicator: /\binset\b/iu.test(style.boxShadow), }; }), ) .toEqual({ focused: true, intersectsViewport: true, insetIndicator: true, }); } export async function startDistServer( distRoot = resolve(process.cwd(), 'dist'), ): Promise { const root = await realpath(distRoot); if (!(await stat(root)).isDirectory()) { throw new Error(`The preview output is not a directory: ${distRoot}`); } const server = createServer((request, response) => { void (async () => { if (request.method !== 'GET' && request.method !== 'HEAD') { response.setHeader('Allow', 'GET, HEAD'); sendText(response, 405, 'Method not allowed.'); return; } const requestPath = decodedRequestPath(request.url ?? '/'); if (requestPath === null) { sendText(response, 403, 'Forbidden.'); return; } const candidate = resolve(root, `.${requestPath}`); if (!isInside(root, candidate)) { sendText(response, 403, 'Forbidden.'); return; } let filePath: string; try { filePath = await realpath(candidate); } catch { sendText(response, 404, 'Not found.'); return; } if (!isInside(root, filePath)) { sendText(response, 403, 'Forbidden.'); return; } const file = await stat(filePath); if (!file.isFile()) { sendText(response, 404, 'Not found.'); return; } const body = await readFile(filePath); response.writeHead(200, { 'Cache-Control': 'no-store', 'Content-Length': body.byteLength, 'Content-Type': contentTypes[extname(filePath).toLowerCase()] ?? 'application/octet-stream', 'X-Content-Type-Options': 'nosniff', }); response.end(request.method === 'HEAD' ? undefined : body); })().catch(() => { if (!response.headersSent) { sendText(response, 500, 'Internal server error.'); } else { response.destroy(); } }); }); await new Promise((resolveListening, reject) => { const onError = (error: Error): void => reject(error); server.once('error', onError); server.listen({ host: LOOPBACK_HOST, port: 0, exclusive: true }, () => { server.off('error', onError); resolveListening(); }); }); const address = server.address(); if ( address === null || typeof address === 'string' || address.address !== LOOPBACK_HOST ) { server.closeAllConnections(); await new Promise((resolveClosed) => server.close(() => resolveClosed()), ); throw new Error( 'The preview server did not bind the owned loopback listener.', ); } let closed = false; return { origin: `http://${LOOPBACK_HOST}:${address.port}`, async close(): Promise { if (closed) return; closed = true; await new Promise((resolveClosed, reject) => { server.close((error) => error === undefined ? resolveClosed() : reject(error), ); server.closeIdleConnections(); server.closeAllConnections(); }); }, }; } export const test = base.extend<{}, { distUrl: string }>({ distUrl: [ async ({}, use) => { const preview = await startDistServer(); try { await use(preview.origin); } finally { await preview.close(); } }, { scope: 'worker' }, ], }); export { expect };