diff --git a/dashboard/public/app.js b/dashboard/public/app.js index 1f4675e..5b614a0 100644 --- a/dashboard/public/app.js +++ b/dashboard/public/app.js @@ -121,10 +121,6 @@ const playwrightStatus = document.querySelector("#playwright-status"); const playwrightGuidance = document.querySelector("#playwright-guidance"); const playwrightCheck = document.querySelector("#playwright-check"); const playwrightConnect = document.querySelector("#playwright-connect"); -const frontendStatusBadge = document.querySelector("#frontend-status"); -const frontendGuidance = document.querySelector("#frontend-guidance"); -const frontendStart = document.querySelector("#frontend-start"); -const frontendStop = document.querySelector("#frontend-stop"); const onboardingStatus = document.querySelector("#onboarding-status"); const onboardingOrganization = document.querySelector( "#onboarding-organization" @@ -576,64 +572,6 @@ async function resumeAfterConnect(status) { } } -function renderFrontendStatus(status) { - frontendStatusBadge.textContent = !status.configured - ? "Not configured" - : status.healthy - ? "Ready" - : status.running - ? "Starting…" - : "Stopped"; - frontendStart.disabled = - !status.configured || status.running || status.healthy; - frontendStop.disabled = !status.running; - frontendGuidance.textContent = status.error - ? status.error - : status.healthy - ? `React is answering at ${status.healthUrl}. Playwright authentication and live tests reuse it.` - : status.running - ? "React is compiling. The first start can take several minutes; this updates automatically." - : status.lastResult && !status.lastResult.ok - ? status.lastResult.message - : "Start the profile-defined React frontend ahead of time so Playwright authentication and live tests reuse it instead of waiting for a cold compile."; -} - -async function refreshFrontendStatus() { - try { - renderFrontendStatus(await fetchJson("/api/frontend/status")); - } catch (error) { - frontendGuidance.textContent = error.message; - } -} - -async function startFrontend() { - try { - renderFrontendStatus( - await fetchJson("/api/frontend/start", { method: "POST" }) - ); - } catch { - await refreshFrontendStatus(); - } -} - -frontendStart.addEventListener("click", () => { - formError.textContent = ""; - frontendStart.disabled = true; - void startFrontend(); -}); - -frontendStop.addEventListener("click", async () => { - frontendStop.disabled = true; - try { - renderFrontendStatus( - await fetchJson("/api/frontend/stop", { method: "POST" }) - ); - } catch (error) { - formError.textContent = error.message; - await refreshFrontendStatus(); - } -}); - async function refreshPlaywrightStatus() { try { renderPlaywrightStatus(await fetchJson("/api/playwright/status")); @@ -1433,7 +1371,6 @@ connectRetryLiveTestButton.addEventListener("click", async () => { connectRetryLiveTestButton.disabled = true; try { pendingRetryAfterConnectJobId = activeJob?.id ?? null; - await startFrontend(); renderPlaywrightStatus( await fetchJson("/api/playwright/connect", { method: "POST" }) ); @@ -1510,7 +1447,6 @@ playwrightConnect.addEventListener("click", async () => { formError.textContent = ""; playwrightConnect.disabled = true; try { - await startFrontend(); renderPlaywrightStatus( await fetchJson("/api/playwright/connect", { method: "POST" }) ); @@ -1550,11 +1486,9 @@ onboardingSave.addEventListener("click", async () => { refresh(); refreshMetrics(); refreshPlaywrightStatus(); -refreshFrontendStatus(); refreshPlaywrightEvidence(); refreshOnboarding(); setInterval(refresh, 1000); setInterval(refreshMetrics, 5000); setInterval(refreshPlaywrightStatus, 5000); -setInterval(refreshFrontendStatus, 5000); setInterval(refreshPlaywrightEvidence, 10000); diff --git a/dashboard/public/index.html b/dashboard/public/index.html index a259557..3786a1b 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -29,18 +29,6 @@

Playwright authentication

-
-
-

React frontend

- Checking… -
-

Start the profile-defined React frontend ahead of time so Playwright authentication and live tests reuse it instead of waiting for a cold compile.

-
- - -
-
-

Repository onboarding

diff --git a/dashboard/server.js b/dashboard/server.js index 06954cb..e3942d5 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -2186,77 +2186,6 @@ function playwrightAuthenticationConfig(repository) { }; } -function frontendStartConfig(repository) { - const profile = loadRepositoryProfile(repository); - const frontend = profile.applications?.frontend ?? {}; - const command = - typeof frontend.command === "string" ? frontend.command.trim() : ""; - if (!command) { - throw new Error("repository profile must configure applications.frontend.command"); - } - return { - command, - healthUrl: validatedManualLocalhostUrl(frontend.healthUrl), - workingDirectory: resolveContainedPath( - repository, - frontend.workingDirectory ?? ".", - "applications.frontend.workingDirectory", - { mustExist: true } - ) - }; -} - -async function frontendIsHealthy(healthUrl) { - try { - const response = await fetch(healthUrl, { - signal: AbortSignal.timeout(2000) - }); - await response.body?.cancel(); - return response.status < 500; - } catch { - return false; - } -} - -function stopProcessTree(child) { - if (!child || child.exitCode !== null || child.signalCode !== null) { - return; - } - if (process.platform === "win32" && child.pid) { - spawnSync("taskkill", ["/pid", String(child.pid), "/t", "/f"], { - stdio: "ignore", - windowsHide: true - }); - return; - } - child.kill(); -} - -async function frontendStatus(repository, connection) { - let healthUrl = ""; - try { - ({ healthUrl } = frontendStartConfig(repository)); - } catch (error) { - return { - configured: false, - running: Boolean(connection.handle), - healthy: false, - owned: Boolean(connection.handle), - error: error.message, - lastResult: connection.lastResult - }; - } - return { - configured: true, - running: Boolean(connection.handle), - healthy: await frontendIsHealthy(healthUrl), - owned: Boolean(connection.handle), - healthUrl, - error: "", - lastResult: connection.lastResult - }; -} - function playwrightAuthenticationStatus(repository, connection) { let configuration; try { @@ -2434,7 +2363,6 @@ export function createDashboardServer({ const completedJobs = []; const artifactDirectories = new Set(); const playwrightConnection = { handle: null, lastResult: null }; - const frontendConnection = { handle: null, lastResult: null }; const playwrightArtifacts = new Map(); const cacheContext = createCacheContext(resolvedRepository); const metricsPath = cacheContext @@ -3714,87 +3642,6 @@ export function createDashboardServer({ return; } - if ( - request.method === "GET" && - requestUrl.pathname === "/api/frontend/status" - ) { - sendJson( - response, - 200, - await frontendStatus(resolvedRepository, frontendConnection) - ); - return; - } - - if ( - request.method === "POST" && - requestUrl.pathname === "/api/frontend/start" - ) { - let configuration; - try { - configuration = frontendStartConfig(resolvedRepository); - } catch (error) { - sendJson(response, 400, { error: error.message }); - return; - } - if (frontendConnection.handle) { - sendJson(response, 409, { error: "FixLab already started the frontend" }); - return; - } - if (await frontendIsHealthy(configuration.healthUrl)) { - sendJson(response, 200, { - ...(await frontendStatus(resolvedRepository, frontendConnection)), - started: false - }); - return; - } - frontendConnection.lastResult = null; - const child = spawn(configuration.command, { - cwd: configuration.workingDirectory, - shell: true, - stdio: "ignore", - windowsHide: true - }); - frontendConnection.handle = child; - child.once("error", (error) => { - frontendConnection.lastResult = { - ok: false, - message: error.message, - finishedAt: new Date().toISOString() - }; - frontendConnection.handle = null; - }); - child.once("close", (code) => { - if (frontendConnection.handle === child) { - frontendConnection.handle = null; - } - frontendConnection.lastResult = { - ok: code === 0, - message: `Frontend process exited with code ${code ?? "unknown"}.`, - finishedAt: new Date().toISOString() - }; - }); - sendJson(response, 202, { - ...(await frontendStatus(resolvedRepository, frontendConnection)), - started: true - }); - return; - } - - if ( - request.method === "POST" && - requestUrl.pathname === "/api/frontend/stop" - ) { - const child = frontendConnection.handle; - frontendConnection.handle = null; - stopProcessTree(child); - sendJson(response, 200, { - ...(await frontendStatus(resolvedRepository, frontendConnection)), - stopped: Boolean(child) - }); - return; - } - if (request.method === "GET" && staticFiles.has(requestUrl.pathname)) { const fileName = staticFiles.get(requestUrl.pathname); const filePath = join(publicDirectory, fileName); @@ -3846,8 +3693,6 @@ export function createDashboardServer({ ) { playwrightConnection.handle.kill(); } - stopProcessTree(frontendConnection.handle); - frontendConnection.handle = null; for (const directory of artifactDirectories) { removeArtifactDirectory(directory); } diff --git a/docs/getting-started.md b/docs/getting-started.md index 93e0f9b..3bc760b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -226,14 +226,6 @@ runs the same configured command, waits for you to sign in, and retries the live test automatically only after the command exits successfully. Sign-in always stays with you; FixLab never handles credentials. -The **React frontend** panel starts the profile's `applications.frontend.command` -in `applications.frontend.workingDirectory` ahead of time and watches -`healthUrl`. Connect Playwright starts it automatically, so authentication and -live tests reuse a warm server instead of waiting for a cold compile. FixLab -stops only the process it started, when you click **Stop React** or the -dashboard exits, and never touches a server that was already running. A -Playwright config that sets `reuseExistingServer` picks up the running server. - The dashboard remains **Not ready** until the profile also includes a frontend The dashboard remains **Not ready** until the profile also includes a frontend **Not ready** until the profile also includes a frontendstartup command, loopback health URL, Playwright test command, explicit diff --git a/test/dashboard-auth-resume.test.js b/test/dashboard-auth-resume.test.js index bd28511..c638438 100644 --- a/test/dashboard-auth-resume.test.js +++ b/test/dashboard-auth-resume.test.js @@ -23,21 +23,6 @@ test("connect-then-retry starts the profile authentication through the API", () assert.match(handler, /method: "POST"/u); }); -test("connecting Playwright starts the frontend first", () => { - for (const marker of [ - 'playwrightConnect.addEventListener("click"', - 'connectRetryLiveTestButton.addEventListener("click"' - ]) { - const handler = appSource.slice(appSource.indexOf(marker)); - assert.ok( - handler.indexOf("await startFrontend()") < - handler.indexOf("/api/playwright/connect"), - marker - ); - } - assert.match(appSource, /\/api\/frontend\/start/u); -}); - test("the live test is retried only after successful authentication", () => { const start = appSource.indexOf("async function resumeAfterConnect"); const body = appSource.slice( diff --git a/test/dashboard-server.test.js b/test/dashboard-server.test.js index 340fd95..0fa2837 100644 --- a/test/dashboard-server.test.js +++ b/test/dashboard-server.test.js @@ -11,7 +11,6 @@ import { symlinkSync, writeFileSync } from "node:fs"; -import { createServer as createHttpServer } from "node:http"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join, parse } from "node:path"; @@ -1645,122 +1644,6 @@ test("dashboard checks and starts repository-owned Playwright authentication", a } }); -test("dashboard starts, reuses and stops the profile-defined frontend", async () => { - const repository = createRepository(); - const port = await availablePort(); - writeFileSync( - join(repository, ".github", "fixlab", "repository-profile.json"), - JSON.stringify( - validProfile({ - applications: { - frontend: { - workingDirectory: ".", - command: "node frontend.js", - port, - healthUrl: `http://127.0.0.1:${port}` - } - } - }) - ) - ); - writeFileSync( - join(repository, "frontend.js"), - `require("node:http").createServer((q, s) => s.end("ok")).listen(${port}, "127.0.0.1");` - ); - const { dashboard, url } = await startDashboard(repository, () => ({ - completion: new Promise(() => {}), - terminate() {} - })); - - try { - const initial = await jsonRequest(url, "/api/frontend/status"); - assert.equal(initial.body.configured, true); - assert.equal(initial.body.healthy, false); - - const started = await jsonRequest(url, "/api/frontend/start", { - method: "POST" - }); - assert.equal(started.response.status, 202); - assert.equal(started.body.started, true); - - let status; - for (let attempt = 0; attempt < 80; attempt += 1) { - status = await jsonRequest(url, "/api/frontend/status"); - if (status.body.healthy) { - break; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - assert.equal(status.body.healthy, true); - assert.equal(status.body.owned, true); - - const again = await jsonRequest(url, "/api/frontend/start", { - method: "POST" - }); - assert.equal(again.response.status, 409); - - const stopped = await jsonRequest(url, "/api/frontend/stop", { - method: "POST" - }); - assert.equal(stopped.body.stopped, true); - for (let attempt = 0; attempt < 50; attempt += 1) { - status = await jsonRequest(url, "/api/frontend/status"); - if (!status.body.healthy) { - break; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - assert.equal(status.body.healthy, false); - } finally { - await dashboard.close(); - rmSync(repository, { recursive: true, force: true }); - } -}); - -test("dashboard does not restart a frontend that is already healthy", async () => { - const repository = createRepository(); - const port = await availablePort(); - writeFileSync( - join(repository, ".github", "fixlab", "repository-profile.json"), - JSON.stringify( - validProfile({ - applications: { - frontend: { - workingDirectory: ".", - command: "node does-not-exist.js", - port, - healthUrl: `http://127.0.0.1:${port}` - } - } - }) - ) - ); - const external = await new Promise((resolve) => { - const instance = createHttpServer((request, response) => - response.end("ok") - ); - instance.listen(port, "127.0.0.1", () => resolve(instance)); - }); - const { dashboard, url } = await startDashboard(repository, () => ({ - completion: new Promise(() => {}), - terminate() {} - })); - - try { - const result = await jsonRequest(url, "/api/frontend/start", { - method: "POST" - }); - assert.equal(result.response.status, 200); - assert.equal(result.body.started, false); - assert.equal(result.body.healthy, true); - assert.equal(result.body.owned, false); - } finally { - await dashboard.close(); - await new Promise((resolve) => external.close(resolve)); - rmSync(repository, { recursive: true, force: true }); - } -}); - test("zero exit with missing terminal markers fails as incomplete", async () => { const repository = createRepository(); const executor = ({ onOutput }) => {