fix: close final portfolio quality gaps
This commit is contained in:
@@ -1,29 +1,29 @@
|
||||
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';
|
||||
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 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',
|
||||
".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 = (
|
||||
@@ -32,10 +32,10 @@ const sendText = (
|
||||
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',
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
});
|
||||
response.end(body);
|
||||
};
|
||||
@@ -43,15 +43,15 @@ const sendText = (
|
||||
const isInside = (root: string, candidate: string): boolean => {
|
||||
const pathFromRoot = relative(root, candidate);
|
||||
return (
|
||||
pathFromRoot === '' ||
|
||||
pathFromRoot === "" ||
|
||||
(!isAbsolute(pathFromRoot) &&
|
||||
pathFromRoot !== '..' &&
|
||||
pathFromRoot !== ".." &&
|
||||
!pathFromRoot.startsWith(`..${sep}`))
|
||||
);
|
||||
};
|
||||
|
||||
const decodedRequestPath = (requestTarget: string): string | null => {
|
||||
const rawPath = requestTarget.split(/[?#]/u, 1)[0] ?? '/';
|
||||
const rawPath = requestTarget.split(/[?#]/u, 1)[0] ?? "/";
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = decodeURIComponent(rawPath);
|
||||
@@ -60,19 +60,19 @@ const decodedRequestPath = (requestTarget: string): string | null => {
|
||||
}
|
||||
|
||||
if (
|
||||
!decoded.startsWith('/') ||
|
||||
decoded.includes('\0') ||
|
||||
decoded.includes('\\')
|
||||
!decoded.startsWith("/") ||
|
||||
decoded.includes("\0") ||
|
||||
decoded.includes("\\")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const segments = decoded.split('/');
|
||||
if (segments.some((segment) => segment === '.' || segment === '..')) {
|
||||
const segments = decoded.split("/");
|
||||
if (segments.some((segment) => segment === "." || segment === "..")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decoded === '/' || decoded.endsWith('/')
|
||||
return decoded === "/" || decoded.endsWith("/")
|
||||
? `${decoded}index.html`
|
||||
: decoded;
|
||||
};
|
||||
@@ -116,8 +116,24 @@ export async function expectInsetFocusIndicator(
|
||||
});
|
||||
}
|
||||
|
||||
export async function revealNavigationLinks(page: Page): Promise<void> {
|
||||
const summary = page.locator(".site-navigation summary");
|
||||
if (!(await summary.isVisible())) return;
|
||||
await page.locator(".site-navigation details").evaluate((details) => {
|
||||
if (details instanceof HTMLDetailsElement) details.open = true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function clickNavigationLink(
|
||||
page: Page,
|
||||
href: string,
|
||||
): Promise<void> {
|
||||
await revealNavigationLinks(page);
|
||||
await page.locator(`nav a[href="${href}"]`).click();
|
||||
}
|
||||
|
||||
export async function startDistServer(
|
||||
distRoot = resolve(process.cwd(), 'dist'),
|
||||
distRoot = resolve(process.cwd(), "dist"),
|
||||
): Promise<DistPreviewServer> {
|
||||
const root = await realpath(distRoot);
|
||||
if (!(await stat(root)).isDirectory()) {
|
||||
@@ -126,21 +142,21 @@ export async function startDistServer(
|
||||
|
||||
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.');
|
||||
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||
response.setHeader("Allow", "GET, HEAD");
|
||||
sendText(response, 405, "Method not allowed.");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestPath = decodedRequestPath(request.url ?? '/');
|
||||
const requestPath = decodedRequestPath(request.url ?? "/");
|
||||
if (requestPath === null) {
|
||||
sendText(response, 403, 'Forbidden.');
|
||||
sendText(response, 403, "Forbidden.");
|
||||
return;
|
||||
}
|
||||
|
||||
const candidate = resolve(root, `.${requestPath}`);
|
||||
if (!isInside(root, candidate)) {
|
||||
sendText(response, 403, 'Forbidden.');
|
||||
sendText(response, 403, "Forbidden.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,34 +164,34 @@ export async function startDistServer(
|
||||
try {
|
||||
filePath = await realpath(candidate);
|
||||
} catch {
|
||||
sendText(response, 404, 'Not found.');
|
||||
sendText(response, 404, "Not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInside(root, filePath)) {
|
||||
sendText(response, 403, 'Forbidden.');
|
||||
sendText(response, 403, "Forbidden.");
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await stat(filePath);
|
||||
if (!file.isFile()) {
|
||||
sendText(response, 404, 'Not found.');
|
||||
sendText(response, 404, "Not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await readFile(filePath);
|
||||
response.writeHead(200, {
|
||||
'Cache-Control': 'no-store',
|
||||
'Content-Length': body.byteLength,
|
||||
'Content-Type':
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Length": body.byteLength,
|
||||
"Content-Type":
|
||||
contentTypes[extname(filePath).toLowerCase()] ??
|
||||
'application/octet-stream',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
"application/octet-stream",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
});
|
||||
response.end(request.method === 'HEAD' ? undefined : body);
|
||||
response.end(request.method === "HEAD" ? undefined : body);
|
||||
})().catch(() => {
|
||||
if (!response.headersSent) {
|
||||
sendText(response, 500, 'Internal server error.');
|
||||
sendText(response, 500, "Internal server error.");
|
||||
} else {
|
||||
response.destroy();
|
||||
}
|
||||
@@ -184,9 +200,9 @@ export async function startDistServer(
|
||||
|
||||
await new Promise<void>((resolveListening, reject) => {
|
||||
const onError = (error: Error): void => reject(error);
|
||||
server.once('error', onError);
|
||||
server.once("error", onError);
|
||||
server.listen({ host: LOOPBACK_HOST, port: 0, exclusive: true }, () => {
|
||||
server.off('error', onError);
|
||||
server.off("error", onError);
|
||||
resolveListening();
|
||||
});
|
||||
});
|
||||
@@ -194,7 +210,7 @@ export async function startDistServer(
|
||||
const address = server.address();
|
||||
if (
|
||||
address === null ||
|
||||
typeof address === 'string' ||
|
||||
typeof address === "string" ||
|
||||
address.address !== LOOPBACK_HOST
|
||||
) {
|
||||
server.closeAllConnections();
|
||||
@@ -202,7 +218,7 @@ export async function startDistServer(
|
||||
server.close(() => resolveClosed()),
|
||||
);
|
||||
throw new Error(
|
||||
'The preview server did not bind the owned loopback listener.',
|
||||
"The preview server did not bind the owned loopback listener.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -233,7 +249,7 @@ export const test = base.extend<{}, { distUrl: string }>({
|
||||
await preview.close();
|
||||
}
|
||||
},
|
||||
{ scope: 'worker' },
|
||||
{ scope: "worker" },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user