287 lines
10 KiB
JavaScript
287 lines
10 KiB
JavaScript
/** 首屏回归:延迟真实入口脚本时仍显示稳定占位,并在业务代码执行前恢复用户主题。 */
|
|
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 };
|
|
}),
|
|
);
|