import { expect, test } from "@playwright/test"; import { expandSimulationConsole, prepareApp } from "./fixtures"; const report = { checkId: "backend-start-1", status: "running", stage: "compiling", message: "正在编译最小自检程序", durationMs: 100, compiler: "gcc", compilerVersion: "gcc test", sundialsRoot: "/native/sundials", error: null, failure: null, }; test("self-check progress does not block editing and completion is logged once", async ({ page }) => { await prepareApp(page); let completed = false; let requests = 0; await page.route("**/api/simulation/runtime-check", route => { requests++; return route.fulfill({ json: completed ? { ...report, status: "completed", stage: "complete", message: "仿真环境自检通过:最小程序预处理、编译、链接和运行均成功", durationMs: 1250 } : report }); }); await page.goto("/"); await expandSimulationConsole(page); const panel = page.getByRole("complementary", { name: "仿真控制台", exact: true }); await expect(panel).toContainText("正在检测仿真环境"); const name = page.getByRole("textbox", { name: "工程", exact: true }); await name.fill("检测期间仍可编辑"); await expect(name).toHaveValue("检测期间仍可编辑"); await expect.poll(() => requests).toBeGreaterThanOrEqual(3); await expect(panel.locator(".simulation-console-dock-entry-message").filter({ hasText: "正在检测仿真环境" })).toHaveCount(1); completed = true; await expect(panel).toContainText("仿真环境自检通过"); await expect(panel).toContainText("1.25 秒"); await page.evaluate(() => window.dispatchEvent(new Event("focus"))); await expect(panel.locator(".simulation-console-dock-entry-message").filter({ hasText: "仿真环境自检通过" })).toHaveCount(1); }); test("link failure shows diagnostics and a backend restart reports recovery", async ({ page }) => { await prepareApp(page); let recovered = false; await page.route("**/api/simulation/runtime-check", route => route.fulfill({ json: recovered ? { ...report, checkId: "backend-start-2", status: "completed", stage: "complete", message: "仿真环境自检通过" } : { ...report, status: "failed", stage: "linking", message: "仿真环境自检失败,编辑器仍可使用", error: "命令执行失败", failure: { command: ["gcc", "check.o", "-o", "model"], cwd: "/native/check", exitCode: 7, stderr: "cannot find library", stdout: "", errorType: "CalledProcessError" } } })); await page.goto("/"); await expandSimulationConsole(page); const panel = page.getByRole("complementary", { name: "仿真控制台", exact: true }); await expect(panel).toContainText("失败阶段:链接"); await expect(panel).toContainText("退出码:7"); await expect(panel).toContainText("cannot find library"); await expect(panel).toContainText('"gcc" "check.o" "-o" "model"'); await page.getByRole("textbox", { name: "工程", exact: true }).fill("失败后仍可编辑"); recovered = true; await page.evaluate(() => window.dispatchEvent(new Event("focus"))); await expect(panel).toContainText("仿真环境自检通过"); }); test("backend unavailable at page load is retried without duplicate warnings", async ({ page }) => { await prepareApp(page); let available = false; await page.route("**/api/simulation/runtime-check", route => available ? route.fulfill({ json: { ...report, status: "completed", message: "仿真环境自检通过" } }) : route.fulfill({ status: 503 })); await page.goto("/"); await expandSimulationConsole(page); const panel = page.getByRole("complementary", { name: "仿真控制台", exact: true }); await expect(panel).toContainText("暂时无法获取仿真环境自检状态"); available = true; await expect(panel).toContainText("仿真环境自检通过"); }); for (const finalStatus of ["completed", "failed"] as const) { test(`automatic retries suppress diagnostics until ${finalStatus}`, async ({ page }) => { await prepareApp(page); let attempt = 1; let finished = false; const failure = { command: ["gcc", "check.c"], cwd: "/native/check", exitCode: 1, stderr: "CreateProcess: No such file or directory", stdout: "", errorType: "CalledProcessError" }; await page.route("**/api/simulation/runtime-check", route => route.fulfill({ json: finished ? { ...report, status: finalStatus, attempt: 6, stage: finalStatus === "failed" ? "preprocessing" : "complete", message: finalStatus === "failed" ? "本次启动自检未通过:首次检测及 5 轮重试均失败;编辑器仍可使用" : "本次启动自检通过,仿真环境可用(第 5 轮重试成功)", error: finalStatus === "failed" ? "命令执行失败" : null, failure: finalStatus === "failed" ? failure : null } : { ...report, status: "retrying", attempt, stage: "retry-wait", error: null, failure: null, message: `本轮检测未通过,正在准备第 ${attempt}/5 轮重试(间隔 0.5 秒);编辑器可正常使用` } })); await page.goto("/"); await expandSimulationConsole(page); const panel = page.getByRole("complementary", { name: "仿真控制台", exact: true }); await expect(panel).toContainText("第 1/5 轮重试"); await expect(panel).not.toContainText("退出码"); await expect(panel).not.toContainText("CreateProcess"); await page.getByRole("textbox", { name: "工程", exact: true }).fill("重试时继续编辑"); attempt = 2; await expect(panel).toContainText("第 2/5 轮重试"); await expect(panel).not.toContainText("错误类型"); finished = true; if (finalStatus === "completed") { await expect(panel).toContainText("仿真环境可用(第 5 轮重试成功)"); await expect(panel).not.toContainText("CreateProcess"); await expect(panel.locator(".simulation-console-dock-entry.error")).toHaveCount(0); } else { await expect(panel).toContainText("首次检测及 5 轮重试均失败"); await expect(panel).toContainText("退出码:1"); await expect(panel).toContainText("CreateProcess: No such file or directory"); await expect(panel.locator(".simulation-console-dock-entry.error")).toHaveCount(1); } }); } test("finished startup report stops timed polling and is refreshed on focus", async ({ page }) => { await prepareApp(page); await page.clock.install(); let requests = 0; await page.route("**/api/simulation/runtime-check", route => { requests++; return route.fulfill({ json: { ...report, status: "completed", stage: "complete", message: "本次启动自检通过,仿真环境可用" } }); }); await page.goto("/"); await expandSimulationConsole(page); const panel = page.getByRole("complementary", { name: "仿真控制台", exact: true }); await expect(panel).toContainText("本次启动自检通过"); const completedRequests = requests; await page.clock.runFor(31000); expect(requests).toBe(completedRequests); await page.evaluate(() => window.dispatchEvent(new Event("focus"))); await expect.poll(() => requests).toBe(completedRequests + 1); });