visionA 雲端版前端 — 沿用 local-tool 既有 UI(原則 4:先抄 local-tool)+
新增雲端特有的登入 / 配對 / 設定流程,含以下整合階段:
- Phase 0:13 頁 + 30+ 元件 + 雛形 banner
- dashboard / devices / models / workspace / clusters / settings 等頁
- AppShell + Sidebar + Header + tokens + i18n(中英雙語 96 keys)
- API client + 5 stores + 3 hooks
- Phase 0.6:OIDC redirect 改造
- login 頁改為 OIDC redirect(`window.location.href = /api/auth/login`)
- register 改說明頁、account 改唯讀(user 資料來源是 MC)
- api client 改 cookie session(credentials: include)+ 完全清掉 localStorage
- Phase 0.7:stage 部署 + nil guard
- getApiBaseUrl() 修:browser 環境視為 same-origin(與 login 頁一致)
- login 頁加「已登入 → router.replace('/')」effect
- User type email/name 改 optional(MC id_token 不一定回 email/name claim)
- header.tsx UserMenu displayName 4 層 fallback:name → email → id → i18n
- 雛形 banner 文案更新(已接 Innovedus 帳號中心)+ 版號 Phase 0.7
驗證:pnpm lint / test (125/125) / build 全綠
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
/**
|
||
* Sidebar 單元測試
|
||
*
|
||
* 重點:
|
||
* 1. isNavActive() 純函式行為(edge cases:根路徑嚴格比對、子路徑 startsWith)
|
||
* 2. 當前路徑的 NavItem 帶 aria-current="page"
|
||
* 3. 非當前路徑的 NavItem 沒有 aria-current
|
||
* 4. 導航連結數量符合 pages.md 總覽(6 項)
|
||
*
|
||
* 需要 mock next/navigation 的 usePathname,否則 jsdom 環境會拋錯。
|
||
*/
|
||
|
||
import { render, screen } from "@testing-library/react";
|
||
import { describe, expect, it, vi } from "vitest";
|
||
|
||
import { LocaleProvider } from "@/lib/i18n/context";
|
||
|
||
import { Sidebar, isNavActive } from "./sidebar";
|
||
|
||
// mock usePathname — 每個測試自行控制回傳值
|
||
vi.mock("next/navigation", () => ({
|
||
usePathname: vi.fn(),
|
||
}));
|
||
|
||
// 取得可控制的 mock 引用
|
||
import { usePathname } from "next/navigation";
|
||
const usePathnameMock = vi.mocked(usePathname);
|
||
|
||
describe("isNavActive", () => {
|
||
it("根路徑 '/' 嚴格比對(不應被任何子路徑啟用)", () => {
|
||
expect(isNavActive("/", "/")).toBe(true);
|
||
expect(isNavActive("/", "/devices")).toBe(false);
|
||
expect(isNavActive("/", "/devices/pair")).toBe(false);
|
||
});
|
||
|
||
it("非根路徑:完全相等或子路徑都 active", () => {
|
||
expect(isNavActive("/devices", "/devices")).toBe(true);
|
||
expect(isNavActive("/devices", "/devices/pair")).toBe(true);
|
||
expect(isNavActive("/devices", "/devices/123")).toBe(true);
|
||
});
|
||
|
||
it("prefix 相同但非子路徑不應 active(避免 /device 啟用 /devices)", () => {
|
||
// '/dev' 不應啟用 '/devices' 項目
|
||
expect(isNavActive("/devices", "/dev")).toBe(false);
|
||
// '/devicesxyz' 也不是 /devices 的子路徑(沒有 / 接續)
|
||
expect(isNavActive("/devices", "/devicesxyz")).toBe(false);
|
||
});
|
||
|
||
it("不相關的路徑應為 false", () => {
|
||
expect(isNavActive("/models", "/devices")).toBe(false);
|
||
expect(isNavActive("/settings", "/")).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("<Sidebar />", () => {
|
||
it("在 /devices/pair 時,Devices 項目被標記為 aria-current=page", () => {
|
||
usePathnameMock.mockReturnValue("/devices/pair");
|
||
|
||
render(
|
||
<LocaleProvider>
|
||
<Sidebar />
|
||
</LocaleProvider>,
|
||
);
|
||
|
||
const devicesLink = screen.getByRole("link", { name: /裝置/ });
|
||
expect(devicesLink).toHaveAttribute("aria-current", "page");
|
||
|
||
// Dashboard 不該 active
|
||
const dashboardLink = screen.getByRole("link", { name: /儀表板/ });
|
||
expect(dashboardLink).not.toHaveAttribute("aria-current");
|
||
});
|
||
|
||
it("在根路徑 '/' 時僅 Dashboard active", () => {
|
||
usePathnameMock.mockReturnValue("/");
|
||
|
||
render(
|
||
<LocaleProvider>
|
||
<Sidebar />
|
||
</LocaleProvider>,
|
||
);
|
||
|
||
const dashboardLink = screen.getByRole("link", { name: /儀表板/ });
|
||
expect(dashboardLink).toHaveAttribute("aria-current", "page");
|
||
|
||
// 任一非根項目都不 active
|
||
expect(screen.getByRole("link", { name: /裝置/ })).not.toHaveAttribute(
|
||
"aria-current",
|
||
);
|
||
});
|
||
|
||
it("包含 pages.md 總覽規定的 6 個主導航項目", () => {
|
||
usePathnameMock.mockReturnValue("/");
|
||
|
||
render(
|
||
<LocaleProvider>
|
||
<Sidebar />
|
||
</LocaleProvider>,
|
||
);
|
||
|
||
// nav 內部的 link 數 = 6(不含品牌 logo link)
|
||
const nav = screen.getByRole("navigation");
|
||
const links = nav.querySelectorAll("a");
|
||
expect(links).toHaveLength(6);
|
||
|
||
// 抽樣:確保 clusters 已納入(雲端版新增)
|
||
expect(screen.getByRole("link", { name: /叢集/ })).toBeInTheDocument();
|
||
});
|
||
});
|