test: lock semantic and no-script portfolio behavior

This commit is contained in:
2026-09-02 03:12:12 +03:00
parent 3638a0beef
commit 091ff70deb
8 changed files with 811 additions and 2 deletions

206
tests/helpers/serve-dist.ts Normal file
View File

@@ -0,0 +1,206 @@
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 } from '@playwright/test';
const LOOPBACK_HOST = '127.0.0.1';
const contentTypes: Readonly<Record<string, string>> = {
'.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<void>;
}
export async function startDistServer(
distRoot = resolve(process.cwd(), 'dist'),
): Promise<DistPreviewServer> {
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<void>((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<void>((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<void> {
if (closed) return;
closed = true;
await new Promise<void>((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 };