cover Deni responsive navigation and retain upstream notices
This commit is contained in:
1 parent
21692898b3
commit
5c2342d4c5
3 files changed
+232
No files matched your search
@@ -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`.
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
Reference in new issue
Block a user