From 5c2342d4c5165a096032bb47283f499112090525 Mon Sep 17 00:00:00 2001 From: CieanYau Date: Sat, 7 Nov 2026 02:20:34 +0800 Subject: [PATCH] cover Deni responsive navigation and retain upstream notices --- THIRD_PARTY_NOTICES.md | 42 ++++++ scripts/reconstruct.mjs | 3 + tests/review-ui-foundation.e2e.test.mjs | 187 ++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 tests/review-ui-foundation.e2e.test.mjs diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..772d9ef --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,42 @@ +# Third-party source attribution + +## Deni + +The console layout and shared presentation styles are adapted from Deni, +revision `72156473d5f73a26f97d35aa5fed3a96059779ef`, maintained by the +OpenVue Foundation. Upstream: `https://github.com/openvi-foundation/deni`. + +Adapted sources: `src/layout/AppLayout.vue`, `AppSidebar.vue`, `AppTopbar.vue`, +`AppMenuItem.vue`, and `src/assets/styles/_base.scss`, `_layout.scss`, +`_sidebar.scss`, `_topbar.scss`, `_page.scss`. The adaptation uses Nuxt slots, +the console's existing authentication and authorised menu, local Lucide icons, +Chinese labels, measured maintenance offsets and three-mode theme handling. +The upstream demo router, data, analytics and configurator are not included. + +The MIT License (MIT) + +Copyright (c) 2026 OpenVue Foundation and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## OpenVue packages + +`openvue`, `@openvue/forms` and `@openvue/themes` are pinned to 1.1.1. +Their upstream licences and notices are distributed in the installed npm +packages. Upstream: `https://github.com/openvi-foundation/openvue`. diff --git a/scripts/reconstruct.mjs b/scripts/reconstruct.mjs index 004dec7..a48dd02 100644 --- a/scripts/reconstruct.mjs +++ b/scripts/reconstruct.mjs @@ -250,6 +250,9 @@ for (const [source, mod] of Object.entries(pc.modules)) { let npm = source.slice(source.lastIndexOf("/node_modules/") + 14); if (npm.startsWith("nuxt/") || npm.startsWith("@nuxt/icon")) continue; npm = npm.replace(/\/index\.mjs$/, ""); + // 可信旧发布元数据仍带原包名;恢复出的源码统一使用当前 OpenVue 依赖。 + npm = npm.replace(/^primevue(?=\/|$)/, "openvue"); + npm = npm.replace(/^@primevue\//, "@openvue/"); if (npm === "@novnc/novnc/core/rfb.js") npm = "@novnc/novnc"; const r = resolve(mod.file, "default"); if (!owners.has(r.file)) diff --git a/tests/review-ui-foundation.e2e.test.mjs b/tests/review-ui-foundation.e2e.test.mjs new file mode 100644 index 0000000..f07cbe7 --- /dev/null +++ b/tests/review-ui-foundation.e2e.test.mjs @@ -0,0 +1,187 @@ +/** 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, + ); + 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, + }; + }), +);