/** 首屏回归:延迟真实入口脚本时仍显示稳定占位,并在业务代码执行前恢复用户主题。 */ import assert from "node:assert/strict"; import { test } from "node:test"; import { appOrigin, backendOrigin, login, scenario, } from "./helpers/console-browser.mjs"; for (const preference of [ { stored: "dark", system: "light", dark: true }, { stored: "light", system: "dark", dark: false }, { stored: "auto", system: "dark", dark: true }, { stored: "auto", system: "light", dark: false }, { stored: "unknown", system: "dark", dark: true }, ]) { test( `cold refresh keeps a styled loading shell before scripts: ${preference.stored}/${preference.system}`, { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { await page.emulateMedia({ colorScheme: preference.system }); await page.addInitScript((mode) => { localStorage.setItem("theme", mode); }, preference.stored); let releaseScripts; const scriptsHeld = new Promise((resolve) => { releaseScripts = resolve; }); let markRequested; const requested = new Promise((resolve) => { markRequested = resolve; }); await page.route(`${appOrigin}/_nuxt/*.js`, async (route) => { markRequested(); await scriptsHeld; await route.continue(); }); let before; try { await page.goto(`${appOrigin}/login`, { waitUntil: "commit" }); await requested; await page.getByRole("status", { name: "页面加载中" }).waitFor(); before = await page.evaluate(() => ({ dark: document.documentElement.classList.contains("app-dark"), colorScheme: document.documentElement.style.colorScheme, rootChildren: document.getElementById("__nuxt").children.length, viewport: innerWidth, scrollWidth: document.documentElement.scrollWidth, loaderBackground: getComputedStyle( document.getElementById("console-startup"), ).backgroundColor, loaderPosition: getComputedStyle( document.getElementById("console-startup"), ).position, })); assert.equal(before.dark, preference.dark); assert.equal(before.colorScheme, preference.dark ? "dark" : "light"); assert.equal(before.rootChildren, 0); assert.equal(before.scrollWidth, before.viewport); assert.equal(before.loaderPosition, "fixed"); assert.equal( before.loaderBackground, preference.dark ? "rgb(24, 24, 27)" : "rgb(248, 250, 252)", ); } finally { releaseScripts(); } await page.locator(".blank-layout").waitFor(); await page.getByRole("status", { name: "页面加载中" }).waitFor({ state: "detached", }); assert.equal( await page .locator("html") .evaluate((html) => html.classList.contains("app-dark")), preference.dark, ); await page.getByText("密码登录", { exact: true }).click(); await page.locator("input[type=password]").waitFor(); return { themeBeforeScripts: before.dark, styledShellBeforeScripts: true, loadingShellRemoved: true, loginInteractive: true, }; }), ); } for (const boundary of ["bootstrap", "route-css"]) { test( `mobile refresh keeps the shell until ${boundary} is ready`, { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { await page.setViewportSize({ width: 390, height: 844 }); await page.emulateMedia({ colorScheme: "dark", reducedMotion: "reduce", }); await page.addInitScript(() => { window.startupFrames = []; // 在浏览器实际绘制边界采样占位退场后的前三帧,不向产品代码加入测试状态。 function sample() { const shell = document.querySelector(".auth-shell"); if (shell && !document.getElementById("__nuxt-loader")) { const style = getComputedStyle(shell); window.startupFrames.push({ boxSizing: style.boxSizing, display: style.display, width: shell.getBoundingClientRect().width, viewport: innerWidth, scrollWidth: document.documentElement.scrollWidth, dark: document.documentElement.classList.contains("app-dark"), }); } if (window.startupFrames.length < 3) requestAnimationFrame(sample); } requestAnimationFrame(sample); }); let release; const held = new Promise((resolve) => { release = resolve; }); let markRequested; const requested = new Promise((resolve) => { markRequested = resolve; }); const url = boundary === "bootstrap" ? `${backendOrigin}/console/v1/common` : `${appOrigin}/_nuxt/login.*.css`; await page.route(url, async (route) => { markRequested(); await held; await route.continue(); }); try { await page.goto(`${appOrigin}/login`, { waitUntil: "commit" }); await requested; await page.getByRole("status", { name: "页面加载中" }).waitFor(); assert.equal( await page .locator(".startup-spinner") .evaluate((element) => getComputedStyle(element).animationName), "none", ); } finally { release(); } await page.locator(".auth-shell").waitFor(); await page.getByRole("status", { name: "页面加载中" }).waitFor({ state: "detached", }); await page.waitForFunction(() => window.startupFrames.length === 3); const frames = await page.evaluate(() => window.startupFrames); for (const appearance of frames) { assert.equal(appearance.boxSizing, "border-box"); assert.equal(appearance.display, "grid"); assert.equal(appearance.dark, true); assert.ok(appearance.width <= appearance.viewport); assert.equal(appearance.scrollWidth, appearance.viewport); } assert.deepEqual(frames[0], frames[2]); return { boundary, shellDuringWait: true, shellRemoved: true, firstVisibleFrames: frames, }; }), ); } test( "delayed font downloads do not keep the startup shell over a usable page", { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { let release; const held = new Promise((resolve) => { release = resolve; }); let markRequested; const requested = new Promise((resolve) => { markRequested = resolve; }); await page.route(/\.woff2(?:\?|$)/, async (route) => { markRequested(); await held; await route.continue(); }); try { await page.goto(`${appOrigin}/login`, { waitUntil: "commit" }); await requested; await page.getByLabel("密码", { exact: true }).waitFor(); await page.getByRole("status", { name: "页面加载中" }).waitFor({ state: "detached", }); await page .getByLabel("账号", { exact: true }) .fill("audit@example.invalid"); assert.equal( await page.getByLabel("账号", { exact: true }).inputValue(), "audit@example.invalid", ); } finally { release(); } return { interactiveBeforeFonts: true, loadingShellRemoved: true }; }), ); test( "bootstrap failure releases the shell and allows configuration recovery", { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { let configurationAvailable = false; await page.route(`${backendOrigin}/console/v1/common`, async (route) => { if (configurationAvailable) return route.continue(); return route.fulfill({ json: { status: 500, msg: "站点配置暂不可用", data: {} }, }); }); await page.goto(`${appOrigin}/login`); const retry = page.getByRole("button", { name: "重新加载", exact: true }); await retry.waitFor(); assert.equal(await page.locator("#__nuxt-loader").count(), 0); configurationAvailable = true; await retry.click(); await retry.waitFor({ state: "detached" }); await page.getByText("验证码登录", { exact: true }).click(); await page .getByRole("button", { name: "获取验证码", exact: true }) .waitFor(); return { failureDoesNotTrapShell: true, configurationRecovered: true }; }), ); test( "member deep-link refresh restores layout without a second loading shell on navigation", { timeout: 30000 }, (t) => scenario(t, {}, async (page) => { await page.setViewportSize({ width: 390, height: 844 }); await page.emulateMedia({ colorScheme: "light" }); await login(page); await page.goto(`${appOrigin}/security/ssh-keys`); await page .getByRole("heading", { name: "SSH 密钥", exact: true }) .waitFor(); await page.reload(); await page .getByRole("heading", { name: "SSH 密钥", exact: true }) .waitFor(); await page .getByRole("status", { name: "页面加载中" }) .waitFor({ state: "detached" }); const layout = await page.locator(".deni-layout").evaluate((element) => ({ boxSizing: getComputedStyle(element).boxSizing, viewport: innerWidth, scrollWidth: document.documentElement.scrollWidth, })); assert.equal(layout.boxSizing, "border-box"); assert.equal(layout.scrollWidth, layout.viewport); await page.getByRole("button", { name: "打开菜单", exact: true }).click(); const navigation = page.getByRole("navigation", { name: "会员中心导航", exact: true, }); await navigation .getByRole("button", { name: "安全", exact: true }) .click(); await navigation .getByRole("link", { name: "安全组", exact: true }) .click(); await page .getByRole("heading", { name: "安全组", exact: true }) .waitFor(); assert.equal(await page.locator("#__nuxt-loader").count(), 0); return { memberRefreshStyled: true, loadingShellOnlyAtStartup: true }; }), );