Files
console-dev/tests/review-ui-foundation.e2e.test.mjs
T
cieanyau 1c0c0f16bc
前端验证 / verify (push) Failing after 3m34s
修复布局:同步生成表格标签并移除跨帧补写
通过表格原生渲染统一生成手机布局标签,删除全页监听与后补扫描。动态等级列使用单一标题并保留颜色。

补齐首次插入、深链刷新、动态表头与首页延迟响应几何回归。全量214项通过,最终专项16项通过;不声明全站所有场景零位移。
2026-11-07 06:21:31 +08:00

336 lines
12 KiB
JavaScript

/** OpenVue/Deni 迁移的实际导航、移动表格与弹窗行为,不断言包名或源码形状。 */
import assert from "node:assert/strict";
import { test } from "node:test";
import {
backendOrigin,
eventually,
login,
scenario,
} from "./helpers/console-browser.mjs";
const navigation = (page) =>
page.getByRole("navigation", { name: "会员中心导航", exact: true });
async function assertNavigationCannotTakeFocus(page) {
const links = page.locator('nav[aria-label="会员中心导航"] a[href]');
await links.first().evaluate((element) => element.focus());
assert.equal(
await links
.first()
.evaluate((element) =>
element.closest("nav").contains(document.activeElement),
),
false,
"The closed navigation cannot receive keyboard focus",
);
}
test(
"mobile navigation traps focus, closes on Escape and returns to its opener",
{ timeout: 30000 },
(t) =>
scenario(t, {}, async (page) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.emulateMedia({ reducedMotion: "reduce" });
await login(page, "/security/ssh-keys");
await page.getByText("audit-01", { exact: true }).waitFor();
const opener = page.getByRole("button", {
name: "打开菜单",
exact: true,
});
await assertNavigationCannotTakeFocus(page);
await opener.click();
const drawer = page.getByRole("dialog", { name: "主导航", exact: true });
const close = drawer.getByRole("button", {
name: "关闭菜单",
exact: true,
});
await close.waitFor();
await eventually(
() => close.evaluate((element) => element === document.activeElement),
Boolean,
"Opening navigation focuses its close action",
);
const firstLink = drawer.getByRole("link", {
name: "破碎工坊首页",
exact: true,
});
const lastLink = drawer.getByRole("link").last();
await firstLink.focus();
await page.keyboard.press("Shift+Tab");
assert.equal(
await lastLink.evaluate(
(element) => element === document.activeElement,
),
true,
"Shift+Tab from the first item wraps to the last drawer action",
);
await page.keyboard.press("Tab");
assert.equal(
await firstLink.evaluate(
(element) => element === document.activeElement,
),
true,
"Tab from the last item wraps to the first drawer action",
);
await page.keyboard.press("Escape");
await drawer.waitFor({ state: "hidden" });
await eventually(
() => opener.evaluate((element) => element === document.activeElement),
Boolean,
"Closing navigation restores the opener",
);
await opener.click();
// 当前后台菜单把 SSH 放在独立入口;目标安全组位于尚未展开的安全分组。
await navigation(page)
.getByRole("button", { name: "安全", exact: true })
.click();
await navigation(page)
.getByRole("link", { name: "安全组", exact: true })
.click();
await page.waitForURL((url) => url.pathname === "/security/groups");
await page
.getByRole("heading", { name: "安全组", exact: true })
.waitFor();
await assertNavigationCannotTakeFocus(page);
assert.equal(
await page
.locator("body")
.evaluate(
(element) => getComputedStyle(element).overflow === "hidden",
),
false,
);
return {
focusContained: true,
escapeRestoresOpener: true,
routeClosesDrawer: true,
scrollRestored: true,
};
}),
);
test(
"navigation changes between overlay and static layout at the shared breakpoint",
{ timeout: 30000 },
(t) =>
scenario(t, {}, async (page) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.setViewportSize({ width: 1025, height: 900 });
await login(page, "/security/ssh-keys");
await navigation(page).waitFor();
assert.equal(
await page
.getByRole("button", { name: "打开菜单", exact: true })
.isVisible(),
false,
);
await page.setViewportSize({ width: 1024, height: 900 });
const opener = page.getByRole("button", {
name: "打开菜单",
exact: true,
});
await opener.waitFor();
await assertNavigationCannotTakeFocus(page);
await opener.click();
await navigation(page).waitFor();
await page.setViewportSize({ width: 1280, height: 900 });
await navigation(page).waitFor();
await eventually(
() =>
page
.locator("body")
.evaluate(
(element) => getComputedStyle(element).overflow !== "hidden",
),
Boolean,
"Resizing to desktop releases overlay scroll lock",
);
assert.equal(
await page.getByRole("dialog", { name: "主导航", exact: true }).count(),
0,
);
const link = navigation(page).getByRole("link", {
name: "SSH 密钥",
exact: true,
});
await link.focus();
assert.equal(
await link.evaluate((element) => element === document.activeElement),
true,
"Desktop navigation is focusable after the overlay closes",
);
return {
desktopAt1025: true,
overlayAt1024: true,
desktopResizeReleasesOverlay: true,
};
}),
);
test(
"mobile table retains field labels and an editable dialog without page overflow",
{ timeout: 30000 },
(t) =>
scenario(t, {}, async (page) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.emulateMedia({ reducedMotion: "reduce" });
await page.addInitScript(() => {
// 读取浏览器收到的首批单元格,不等待后续帧补写属性,也不改动产品 DOM。
window.initialTableCells = null;
const observer = new MutationObserver(() => {
const cells = document.querySelectorAll(
".p-datatable-tbody > tr:not(.p-datatable-empty-message) > td",
);
if (!cells.length) return;
window.initialTableCells = [...cells].map((cell) => ({
label: cell.getAttribute("data-label"),
visible: getComputedStyle(cell).display !== "none",
}));
observer.disconnect();
});
observer.observe(document, { childList: true, subtree: true });
});
await login(page, "/security/ssh-keys");
const name = page.getByText("audit-01", { exact: true });
await name.waitFor();
const label = await name.evaluate((element) => {
const cell = element.closest("td");
return {
value: cell?.getAttribute("data-label"),
displayed: cell ? getComputedStyle(cell, "::before").content : "none",
};
});
assert.equal(label.value, "名称");
assert.equal(label.displayed.includes("名称"), true);
const initial = await page.evaluate(() => window.initialTableCells);
assert.ok(initial?.length > 0);
assert.equal(
initial.every((cell) => cell.label !== null),
true,
"Mobile cells must carry layout labels when inserted, without another frame",
);
assert.ok(
initial.some((cell) => cell.label === "名称" && cell.visible),
"The primary field must be visible in the initial card geometry",
);
await page.reload();
await name.waitFor();
const refreshed = await page.evaluate(() => window.initialTableCells);
assert.deepEqual(
refreshed,
initial,
"A deep-link refresh must insert the same complete mobile card geometry",
);
await page
.getByRole("button", { name: "编辑", exact: true })
.first()
.click();
const dialog = page.getByRole("dialog");
const field = dialog.getByRole("textbox").first();
await field.fill("mobile-key");
const box = await dialog.boundingBox();
assert.ok(
box.x >= -1 && box.x + box.width <= 376,
"The editor fits the phone viewport",
);
await dialog.getByRole("button", { name: "取消", exact: true }).click();
await dialog.waitFor({ state: "hidden" });
const fits = await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth + 1,
);
assert.equal(fits, true);
return {
tableFieldLabels: true,
initialTableGeometryReady: true,
refreshKeepsInitialTableGeometry: true,
editableDialogFits: true,
pageHasNoHorizontalOverflow: true,
};
}),
);
test(
"dynamic columns share a single colored title and mobile field label",
{ timeout: 30000 },
(t) =>
scenario(t, {}, async (page) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.emulateMedia({ reducedMotion: "reduce" });
await page.route(`${backendOrigin}/console/v1/**`, async (route) => {
const path = new URL(route.request().url()).pathname;
if (path.endsWith("/account")) {
const response = await route.fetch();
const json = await response.json();
json.data.account.customfield = {
idcsmart_client_level: { id: 1, name: "本地会员" },
};
return route.fulfill({ response, json });
}
if (path.endsWith("/country"))
return route.fulfill({
json: { status: 200, msg: "", data: { list: [] } },
});
if (!path.endsWith("/client_level")) return route.fallback();
return route.fulfill({
json: {
status: 200,
msg: "",
data: {
level_id: 1,
levels: [
{
id: 1,
name: "本地会员",
background_color: "#0284c7",
discount_list: [
{ product_name: "本地云产品", discount_percent: 95 },
],
},
],
},
},
});
});
await login(page, "/account");
await page.getByRole("button", { name: "本地会员", exact: true }).click();
const dialog = page.getByRole("dialog", {
name: "等级策略",
exact: true,
});
// 手机字段名来自 ::before,参与无障碍名称;按实际折扣文字定位单元格再核对标签。
const discount = dialog.locator("td").filter({ hasText: /^95%$/ });
await discount.waitFor();
assert.equal(await discount.getAttribute("data-label"), "本地会员");
// 手机表头隐藏,切回桌面核对用户实际看到的自定义标题,而非隐藏元素的无障碍名称。
await page.setViewportSize({ width: 1440, height: 900 });
const header = dialog.getByRole("columnheader", {
name: "本地会员",
exact: true,
});
await header.waitFor();
assert.equal(
await header.count(),
1,
"Dynamic columns must display a single title",
);
assert.equal(
await header.evaluate((element) => getComputedStyle(element).color),
"rgb(2, 132, 199)",
"The server-provided level color must remain visible",
);
assert.equal(
await dialog
.locator("td")
.filter({ hasText: /^本地云产品$/ })
.getAttribute("data-label"),
"商品",
);
return {
dynamicHeaderLabel: true,
singleTitle: true,
levelColorPreserved: true,
};
}),
);