diff --git a/output/debug/customers-debug.png b/output/debug/customers-debug.png new file mode 100644 index 00000000..6d360f6b Binary files /dev/null and b/output/debug/customers-debug.png differ diff --git a/playwright.global-setup.mjs b/playwright.global-setup.mjs index ebf9cd79..d8d00733 100644 --- a/playwright.global-setup.mjs +++ b/playwright.global-setup.mjs @@ -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(); } } diff --git a/src/components/displays/Customers.vue b/src/components/displays/Customers.vue index 3ed61abc..b398a508 100644 --- a/src/components/displays/Customers.vue +++ b/src/components/displays/Customers.vue @@ -1,23 +1,28 @@