jim800121chen 99dea42239 feat(visionA-frontend): Phase 0 → 0.7 雲端前端(Next.js + OIDC redirect 流程)
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>
2026-05-01 11:21:36 +08:00

109 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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