cover Deni responsive navigation and retain upstream notices

This commit is contained in:
cieanyau committed 2026-11-07 02:20:34 +08:00
1 parent 21692898b3
commit 5c2342d4c5
3 files changed
+232

No files matched your search

+42
View File
@@ -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`.
+3
View File
@@ -250,6 +250,9 @@ for (const [source, mod] of Object.entries(pc.modules)) {
let npm = source.slice(source.lastIndexOf("/node_modules/") + 14); let npm = source.slice(source.lastIndexOf("/node_modules/") + 14);
if (npm.startsWith("nuxt/") || npm.startsWith("@nuxt/icon")) continue; if (npm.startsWith("nuxt/") || npm.startsWith("@nuxt/icon")) continue;
npm = npm.replace(/\/index\.mjs$/, ""); 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"; if (npm === "@novnc/novnc/core/rfb.js") npm = "@novnc/novnc";
const r = resolve(mod.file, "default"); const r = resolve(mod.file, "default");
if (!owners.has(r.file)) if (!owners.has(r.file))
+187
View File
@@ -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,
};
}),
);