198 lines
6.5 KiB
JavaScript
198 lines
6.5 KiB
JavaScript
/** OpenVue/Deni 迁移的实际导航、移动表格与弹窗行为,不断言包名或源码形状。 */
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { 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",
|
|
);
|
|
await page.keyboard.press("Shift+Tab");
|
|
assert.equal(
|
|
await drawer.evaluate((element) =>
|
|
element.contains(document.activeElement),
|
|
),
|
|
true,
|
|
);
|
|
await page.keyboard.press("Tab");
|
|
assert.equal(
|
|
await drawer.evaluate((element) =>
|
|
element.contains(document.activeElement),
|
|
),
|
|
true,
|
|
);
|
|
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();
|
|
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 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);
|
|
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,
|
|
editableDialogFits: true,
|
|
pageHasNoHorizontalOverflow: true,
|
|
};
|
|
}),
|
|
);
|