完善视觉回归边界与超长通知验收说明
This commit is contained in:
1 parent
21821ec6fa
commit
bbd1f3b963
3 files changed
+88
-32
No files matched your search
@@ -38,6 +38,9 @@ CHROME_PATH=/absolute/path/to/chrome npm test
|
||||
- `review-purchase.e2e.test.mjs`:镜像订单未付复用、取消后重建、已付授权延迟不重复收费;级联候选与默认、结算、购物车恢复一致。
|
||||
- `review-fields.e2e.test.mjs`:常见 ASCII 规则本地拦截、PCRE 专有规则交后端校验;实例随机密码实际提交及安全源失效、无偏采样边界。
|
||||
- `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`;仍需先构建当前源码。
|
||||
随机密码场景只记录性质与是否一致,不把密码或安全随机值写入验收附件。
|
||||
|
||||
@@ -17,7 +17,7 @@ const maintenanceMessage =
|
||||
|
||||
async function layoutBoundary(
|
||||
page,
|
||||
{ maintenance = true, content = "help" } = {},
|
||||
{ maintenance = true, content = "help", message = maintenanceMessage } = {},
|
||||
) {
|
||||
await page
|
||||
.context()
|
||||
@@ -33,21 +33,13 @@ async function layoutBoundary(
|
||||
const body = await response.json();
|
||||
Object.assign(body.data, {
|
||||
maintenance_mode: Number(maintenance),
|
||||
maintenance_mode_message: maintenanceMessage,
|
||||
maintenance_mode_message: message,
|
||||
});
|
||||
return route.fulfill({ response, json: body });
|
||||
}
|
||||
if (path === "/menu")
|
||||
return reply({
|
||||
menu: [
|
||||
{
|
||||
id: 10,
|
||||
name: "查看官网",
|
||||
menu_type: "custom",
|
||||
url: "/official-site",
|
||||
},
|
||||
],
|
||||
});
|
||||
if (["/ticket/status", "/ticket/type"].includes(path))
|
||||
return reply({ list: [] });
|
||||
if (path === "/ticket/statistic") return reply({});
|
||||
if (path === "/official_site_cms/content") return reply({ content: {} });
|
||||
if (path === "/help")
|
||||
return reply({
|
||||
@@ -202,26 +194,80 @@ test(
|
||||
);
|
||||
sizes.push({ width, noticeFits: true, headerBelowNotice: true });
|
||||
}
|
||||
await page.getByRole("button", { name: "打开菜单", exact: true }).click();
|
||||
const menuVisible = await page
|
||||
.getByRole("button", { name: "关闭菜单", exact: true })
|
||||
.evaluate((button) => {
|
||||
const r = button.getBoundingClientRect(),
|
||||
hit = document.elementFromPoint(
|
||||
r.left + r.width / 2,
|
||||
r.top + r.height / 2,
|
||||
);
|
||||
return button.contains(hit);
|
||||
});
|
||||
assert.ok(
|
||||
menuVisible,
|
||||
"The notice must not cover the drawer close button",
|
||||
// 关闭控件按既有设计只在键盘焦点时显现,因此从键盘入口验证它不被维护条遮挡。
|
||||
const open = page.getByRole("button", { name: "打开菜单", exact: true });
|
||||
await open.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await eventually(
|
||||
() =>
|
||||
page
|
||||
.getByRole("button", { name: "关闭菜单", exact: true })
|
||||
.evaluate((button) => {
|
||||
const r = button.getBoundingClientRect(),
|
||||
hit = document.elementFromPoint(
|
||||
r.left + r.width / 2,
|
||||
r.top + r.height / 2,
|
||||
);
|
||||
return button === document.activeElement && button.contains(hit);
|
||||
}),
|
||||
Boolean,
|
||||
"The notice must not cover the focused drawer close button",
|
||||
);
|
||||
await page.keyboard.press("Escape");
|
||||
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 [
|
||||
["help", "/help/article?id=1", ".help-nav"],
|
||||
["cart", "/cart", ".cart-foot"],
|
||||
@@ -243,6 +289,8 @@ for (const [name, path, selector] of [
|
||||
.first()
|
||||
.click();
|
||||
await page.locator(selector).waitFor();
|
||||
if (name === "help")
|
||||
await page.locator(".content-html p").first().waitFor();
|
||||
await page.evaluate(() => window.scrollTo(0, 500));
|
||||
const state = await eventually(
|
||||
() =>
|
||||
@@ -279,7 +327,7 @@ for (const [name, path, selector] of [
|
||||
}
|
||||
|
||||
test(
|
||||
"console layout releases its measured offsets when navigating to the official site",
|
||||
"console layout releases its measured offsets after logging out",
|
||||
{ timeout: 30000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
@@ -298,8 +346,11 @@ test(
|
||||
(top) => top > outsideConsoleTop,
|
||||
"Global notifications must clear the maintenance header",
|
||||
);
|
||||
await page.getByRole("link", { name: "查看官网", exact: true }).click();
|
||||
await page.locator(".official-site").waitFor();
|
||||
await page.getByRole("button", { name: "用户菜单", exact: true }).click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: "退出登录", exact: true })
|
||||
.click();
|
||||
await page.waitForURL((url) => url.pathname === "/login");
|
||||
await eventually(
|
||||
toastTop,
|
||||
(top) => Math.abs(top - outsideConsoleTop) < 0.5,
|
||||
@@ -307,7 +358,7 @@ test(
|
||||
);
|
||||
return {
|
||||
notificationClearedConsole: consoleTop > outsideConsoleTop,
|
||||
officialLayoutRestored: true,
|
||||
blankLayoutRestored: true,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -322,6 +373,7 @@ for (const [path, name] of [
|
||||
{ timeout: 25000 },
|
||||
(t) =>
|
||||
scenario(t, {}, async (page) => {
|
||||
await layoutBoundary(page, { maintenance: false });
|
||||
await login(page, path);
|
||||
const links = page.getByRole("link", { name, exact: true });
|
||||
await links.first().waitFor();
|
||||
|
||||
Reference in new issue
Block a user