Add mass-import feature for customer data including UI, parsing enhancements, and E2E testing

This commit is contained in:
Jeppe Bundgaard
2026-04-23 19:28:13 +02:00
parent 04467316c2
commit 4febb998c3
13 changed files with 737 additions and 77 deletions
+97 -3
View File
@@ -13,6 +13,7 @@ const runtimeNamespace = String(process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || `po
.replace(/[^a-zA-Z0-9._-]+/g, "-");
const pidFile = path.resolve(process.cwd(), "output/playwright", `dev-server-${runtimeNamespace}.json`);
const serverOutputLimit = 80;
const activeOutputReaders = [];
// Hardlinked Windows worktrees can break Vite's bundled config temp paths during Playwright boot.
const viteDevArgs = [
"run",
@@ -289,12 +290,107 @@ async function warmUpAsset(url, expectedContentType, timeoutMs = 120_000) {
throw new Error(`Timed out warming Playwright dev asset ${url}.`);
}
async function fetchWarmedAsset(url, expectedContentType, timeoutMs = 120_000) {
const deadline = Date.now() + timeoutMs;
const perRequestTimeoutMs = 10_000;
while (Date.now() < deadline) {
try {
const response = await fetch(url, {
redirect: "manual",
signal: AbortSignal.timeout(perRequestTimeoutMs),
});
const contentType = response.headers.get("content-type") || "";
if (response.ok && contentType.toLowerCase().includes(expectedContentType)) {
return await response.text();
}
} catch {
// keep polling until the Vite transform is ready
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`Timed out warming Playwright dev asset ${url}.`);
}
function extractModuleImports(source) {
const specifiers = new Set();
const importPattern =
/\bimport(?:\s+[^"'()]+?\s+from\s+)?["']([^"']+)["']|\bimport\(\s*["']([^"']+)["']\s*\)/g;
for (const match of source.matchAll(importPattern)) {
const specifier = match[1] || match[2];
if (!specifier) {
continue;
}
if (specifier.startsWith("/src/") || specifier.startsWith("/node_modules/.vite/deps/")) {
specifiers.add(specifier);
}
}
return [...specifiers];
}
function resolveExpectedContentType(specifier) {
const cleanSpecifier = specifier.split("?")[0];
if (cleanSpecifier.endsWith(".css")) {
return "css";
}
if (cleanSpecifier.endsWith(".json")) {
return "json";
}
return "javascript";
}
async function warmModuleGraph(entryUrl, { depth = 2, timeoutMs = 120_000 } = {}) {
const queue = [{ url: entryUrl, depth: 0 }];
const visited = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (!current || visited.has(current.url)) {
continue;
}
visited.add(current.url);
const source = await fetchWarmedAsset(current.url, "javascript", timeoutMs);
if (current.depth >= depth) {
continue;
}
for (const specifier of extractModuleImports(source)) {
const expectedContentType = resolveExpectedContentType(specifier);
const importUrl = new URL(specifier, current.url).toString();
if (expectedContentType === "javascript" && specifier.startsWith("/src/")) {
queue.push({
url: importUrl,
depth: current.depth + 1,
});
continue;
}
await warmUpAsset(importUrl, expectedContentType, timeoutMs);
}
}
}
async function warmUpDevServer(url) {
const warmupTargets = await buildWarmupTargets();
for (const target of warmupTargets) {
await warmUpAsset(new URL(target.pathname, url).toString(), target.expectedContentType);
}
await warmModuleGraph(new URL("/src/main.js", url).toString());
await new Promise((resolve) => setTimeout(resolve, 1000));
}
export default async function globalSetup() {
@@ -339,6 +435,7 @@ export default async function globalSetup() {
const stderrLines = [];
const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines);
const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines);
activeOutputReaders.push(stdoutReader, stderrReader);
serverProcess.unref();
await fs.writeFile(pidFile, JSON.stringify({ pid: serverProcess.pid, port: devPort }), "utf8");
@@ -349,8 +446,5 @@ export default async function globalSetup() {
} catch (error) {
await killProcessTree(serverProcess.pid);
throw error;
} finally {
stdoutReader.close();
stderrReader.close();
}
}