/** 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, }; }), );