完善视觉回归边界与超长通知验收说明

This commit is contained in:
cieanyau committed 2026-11-06 03:17:10 +08:00
1 parent 21821ec6fa
commit bbd1f3b963
3 files changed
+88 -32

No files matched your search

+2 -1
View File
@@ -125,7 +125,8 @@ CHROME_PATH=/absolute/path/to/chrome npm test
离线场景覆盖登录协议、短信倒计时、路由保护、会话失效、双标签页退出、SSH 密钥管理、 离线场景覆盖登录协议、短信倒计时、路由保护、会话失效、双标签页退出、SSH 密钥管理、
请求超时、支付期限及旧登录入口;还覆盖安全组批量规则、旧工单切换、镜像订单重试、 请求超时、支付期限及旧登录入口;还覆盖安全组批量规则、旧工单切换、镜像订单重试、
级联配置、商品字段校验、安全随机密码和三态主题。 级联配置、商品字段校验、安全随机密码和三态主题;补充维护布局、深色样式隔离、
上传控件名称、导航键盘焦点及监控图表减动回归。
测试产物保存在被忽略的 `tests/fixtures/runs/`。 测试产物保存在被忽略的 `tests/fixtures/runs/`。
详细使用方式见 [测试说明](tests/README.md)。 详细使用方式见 [测试说明](tests/README.md)。
+3
View File
@@ -38,6 +38,9 @@ CHROME_PATH=/absolute/path/to/chrome npm test
- `review-purchase.e2e.test.mjs`:镜像订单未付复用、取消后重建、已付授权延迟不重复收费;级联候选与默认、结算、购物车恢复一致。 - `review-purchase.e2e.test.mjs`:镜像订单未付复用、取消后重建、已付授权延迟不重复收费;级联候选与默认、结算、购物车恢复一致。
- `review-fields.e2e.test.mjs`:常见 ASCII 规则本地拦截、PCRE 专有规则交后端校验;实例随机密码实际提交及安全源失效、无偏采样边界。 - `review-fields.e2e.test.mjs`:常见 ASCII 规则本地拦截、PCRE 专有规则交后端校验;实例随机密码实际提交及安全源失效、无偏采样边界。
- `review-theme.e2e.test.mjs`:认证页三态主题文案、持久化与跟随系统变化。 - `review-theme.e2e.test.mjs`:认证页三态主题文案、持久化与跟随系统变化。
- `review-style-layout.e2e.test.mjs`:维护条随内容与视口变化、超长通知内部滚动、帮助/购物车/机型/配置摘要吸顶避让,退出布局后的偏移清理与导航单一键盘焦点。
- `review-style-fields.e2e.test.mjs`:深色商品说明对比度、提现样式隔离、实名认证与转账凭证上传名称;实际升级弹窗的窄屏网格。
- `review-style-motion.e2e.test.mjs`:实例监控 Canvas 初始/刷新/加载反馈跟随系统减动偏好,并验证偏好切换及离页后无运行时错误。
可针对单组执行,例如 `node --test tests/review-routes.e2e.test.mjs`;仍需先构建当前源码。 可针对单组执行,例如 `node --test tests/review-routes.e2e.test.mjs`;仍需先构建当前源码。
随机密码场景只记录性质与是否一致,不把密码或安全随机值写入验收附件。 随机密码场景只记录性质与是否一致,不把密码或安全随机值写入验收附件。
+83 -31
View File
@@ -17,7 +17,7 @@ const maintenanceMessage =
async function layoutBoundary( async function layoutBoundary(
page, page,
{ maintenance = true, content = "help" } = {}, { maintenance = true, content = "help", message = maintenanceMessage } = {},
) { ) {
await page await page
.context() .context()
@@ -33,21 +33,13 @@ async function layoutBoundary(
const body = await response.json(); const body = await response.json();
Object.assign(body.data, { Object.assign(body.data, {
maintenance_mode: Number(maintenance), maintenance_mode: Number(maintenance),
maintenance_mode_message: maintenanceMessage, maintenance_mode_message: message,
}); });
return route.fulfill({ response, json: body }); return route.fulfill({ response, json: body });
} }
if (path === "/menu") if (["/ticket/status", "/ticket/type"].includes(path))
return reply({ return reply({ list: [] });
menu: [ if (path === "/ticket/statistic") return reply({});
{
id: 10,
name: "查看官网",
menu_type: "custom",
url: "/official-site",
},
],
});
if (path === "/official_site_cms/content") return reply({ content: {} }); if (path === "/official_site_cms/content") return reply({ content: {} });
if (path === "/help") if (path === "/help")
return reply({ return reply({
@@ -202,26 +194,80 @@ test(
); );
sizes.push({ width, noticeFits: true, headerBelowNotice: true }); sizes.push({ width, noticeFits: true, headerBelowNotice: true });
} }
await page.getByRole("button", { name: "打开菜单", exact: true }).click(); // 关闭控件按既有设计只在键盘焦点时显现,因此从键盘入口验证它不被维护条遮挡。
const menuVisible = await page const open = page.getByRole("button", { name: "打开菜单", exact: true });
.getByRole("button", { name: "关闭菜单", exact: true }) await open.focus();
.evaluate((button) => { await page.keyboard.press("Enter");
const r = button.getBoundingClientRect(), await eventually(
hit = document.elementFromPoint( () =>
r.left + r.width / 2, page
r.top + r.height / 2, .getByRole("button", { name: "关闭菜单", exact: true })
); .evaluate((button) => {
return button.contains(hit); const r = button.getBoundingClientRect(),
}); hit = document.elementFromPoint(
assert.ok( r.left + r.width / 2,
menuVisible, r.top + r.height / 2,
"The notice must not cover the drawer close button", );
return button === document.activeElement && button.contains(hit);
}),
Boolean,
"The notice must not cover the focused drawer close button",
); );
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
return { sizes, drawerCloseReachable: true }; return { sizes, drawerCloseReachable: true };
}), }),
); );
test(
"oversized maintenance notices scroll without pushing navigation off screen",
{ timeout: 30000 },
(t) =>
scenario(t, {}, async (page) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.setViewportSize({ width: 320, height: 640 });
await layoutBoundary(page, { message: maintenanceMessage.repeat(30) });
await login(page, "/security/ssh-keys");
const banner = page.locator(".maintenance-banner");
await banner.waitFor();
const state = await eventually(
() =>
banner.evaluate((element) => {
const bounds = element.getBoundingClientRect(),
header = document
.querySelector(".topbar")
.getBoundingClientRect();
return {
height: bounds.height,
viewportHeight: innerHeight,
scrollable: element.scrollHeight > element.clientHeight,
belowBanner: header.top >= bounds.bottom - 0.5,
navigationVisible: header.bottom < innerHeight,
};
}),
(state) => state.belowBanner && state.navigationVisible,
"Even a long notice must leave navigation visible below it",
);
assert.ok(state.scrollable, "All notice text must remain scrollable");
assert.ok(state.height <= state.viewportHeight * 0.4 + 1);
const endReachable = await banner.evaluate((element) => {
element.scrollTop = element.scrollHeight;
const bounds = element.getBoundingClientRect(),
text = element.querySelector("span").firstChild,
end = document.createRange();
end.setStart(text, text.length - 1);
end.setEnd(text, text.length);
const last = end.getBoundingClientRect();
return (
element.scrollTop > 0 &&
last.top >= bounds.top &&
last.bottom <= bounds.bottom + 0.5
);
});
assert.ok(endReachable, "The end of a long notice must remain readable");
return { boundedHeight: true, navigationVisible: true, endReachable };
}),
);
for (const [name, path, selector] of [ for (const [name, path, selector] of [
["help", "/help/article?id=1", ".help-nav"], ["help", "/help/article?id=1", ".help-nav"],
["cart", "/cart", ".cart-foot"], ["cart", "/cart", ".cart-foot"],
@@ -243,6 +289,8 @@ for (const [name, path, selector] of [
.first() .first()
.click(); .click();
await page.locator(selector).waitFor(); await page.locator(selector).waitFor();
if (name === "help")
await page.locator(".content-html p").first().waitFor();
await page.evaluate(() => window.scrollTo(0, 500)); await page.evaluate(() => window.scrollTo(0, 500));
const state = await eventually( const state = await eventually(
() => () =>
@@ -279,7 +327,7 @@ for (const [name, path, selector] of [
} }
test( test(
"console layout releases its measured offsets when navigating to the official site", "console layout releases its measured offsets after logging out",
{ timeout: 30000 }, { timeout: 30000 },
(t) => (t) =>
scenario(t, {}, async (page) => { scenario(t, {}, async (page) => {
@@ -298,8 +346,11 @@ test(
(top) => top > outsideConsoleTop, (top) => top > outsideConsoleTop,
"Global notifications must clear the maintenance header", "Global notifications must clear the maintenance header",
); );
await page.getByRole("link", { name: "查看官网", exact: true }).click(); await page.getByRole("button", { name: "用户菜单", exact: true }).click();
await page.locator(".official-site").waitFor(); await page
.getByRole("menuitem", { name: "退出登录", exact: true })
.click();
await page.waitForURL((url) => url.pathname === "/login");
await eventually( await eventually(
toastTop, toastTop,
(top) => Math.abs(top - outsideConsoleTop) < 0.5, (top) => Math.abs(top - outsideConsoleTop) < 0.5,
@@ -307,7 +358,7 @@ test(
); );
return { return {
notificationClearedConsole: consoleTop > outsideConsoleTop, notificationClearedConsole: consoleTop > outsideConsoleTop,
officialLayoutRestored: true, blankLayoutRestored: true,
}; };
}), }),
); );
@@ -322,6 +373,7 @@ for (const [path, name] of [
{ timeout: 25000 }, { timeout: 25000 },
(t) => (t) =>
scenario(t, {}, async (page) => { scenario(t, {}, async (page) => {
await layoutBoundary(page, { maintenance: false });
await login(page, path); await login(page, path);
const links = page.getByRole("link", { name, exact: true }); const links = page.getByRole("link", { name, exact: true });
await links.first().waitFor(); await links.first().waitFor();