P2-9: TeacherSchedule 重复渲染优化 - 将移动端(lg:hidden)和桌面端(hidden lg:block)的双实例渲染改为单实例 - 使用 CSS flex order + grid col-start/row-start 实现响应式布局重排序 - 消除服务端 HTML 负载翻倍问题 P2-5: StudentTodayScheduleCard 时间过时修复 - 新增 useCurrentTime hook(src/shared/hooks/use-current-time.ts) - 每分钟自动更新当前时间,useMemo 依赖 [items, now] 确保徽章不过时 - SSR 安全:初始渲染用 new Date(),挂载后 setInterval 更新 P2-1: 流式/Suspense 架构改造 - 新增 getAdminDashboardStreams(streams.ts):返回各独立数据源的未解析 Promise - Admin dashboard:7 个分区组件用 React use() 独立消费 Promise,各 Suspense 边界独立流式渲染 - Teacher/Student/Parent dashboard:传入未解析 Promise,视图用 use() 消费,启用 Suspense 流式 - 页面外壳(标题 + 快捷操作)立即渲染,数据到达后各分区按各自速度填充 P2-7: 组件测试 + 路由测试修复 - 修复 dashboard-routing.test.ts:移除误导性的 permissions 字段(实际用 resolvePermissions(roles)) - 新增 fallback 路由测试(未知角色 → teacher dashboard) - 新增 DashboardSection 组件测试(6 个测试:骨架屏变体 + 错误边界 + 正常渲染) - 新增 useCurrentTime hook 测试(3 个测试:初始值 + 间隔更新 + 清理) 同步更新: - docs/architecture/005_architecture_data.json 新增 7 个流式组件 + useCurrentTime hook + getAdminDashboardStreams 条目
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||
|
||
const mocks = vi.hoisted(() => ({
|
||
authMock: vi.fn(),
|
||
redirectMock: vi.fn((target: string) => {
|
||
throw new Error(`REDIRECT:${target}`)
|
||
}),
|
||
}))
|
||
|
||
vi.mock("@/auth", () => ({
|
||
auth: mocks.authMock,
|
||
}))
|
||
|
||
vi.mock("next/navigation", () => ({
|
||
redirect: mocks.redirectMock,
|
||
}))
|
||
|
||
import DashboardPage from "@/app/(dashboard)/dashboard/page"
|
||
|
||
/**
|
||
* 仪表盘路由分发测试
|
||
*
|
||
* 注意:实际代码用 `resolvePermissions(roles)` 从角色推导权限,
|
||
* 而非读取 user.permissions 字段。因此 mock 中只需设置 roles,
|
||
* 无需(也不应)设置 permissions 字段,以免误导。
|
||
*/
|
||
describe("dashboard route dispatcher", () => {
|
||
beforeEach(() => {
|
||
vi.resetAllMocks()
|
||
mocks.redirectMock.mockImplementation((target: string) => {
|
||
throw new Error(`REDIRECT:${target}`)
|
||
})
|
||
})
|
||
|
||
it("redirects to login when session is missing", async () => {
|
||
mocks.authMock.mockResolvedValue(null)
|
||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/login")
|
||
})
|
||
|
||
it("redirects to login when user is missing", async () => {
|
||
mocks.authMock.mockResolvedValue({ user: null })
|
||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/login")
|
||
})
|
||
|
||
it("redirects admin to admin dashboard", async () => {
|
||
mocks.authMock.mockResolvedValue({
|
||
user: { id: "u_admin", roles: ["admin"] },
|
||
})
|
||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/admin/dashboard")
|
||
})
|
||
|
||
it("redirects student to student dashboard", async () => {
|
||
mocks.authMock.mockResolvedValue({
|
||
user: { id: "u_student", roles: ["student"] },
|
||
})
|
||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/student/dashboard")
|
||
})
|
||
|
||
it("redirects parent to parent dashboard", async () => {
|
||
mocks.authMock.mockResolvedValue({
|
||
user: { id: "u_parent", roles: ["parent"] },
|
||
})
|
||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/parent/dashboard")
|
||
})
|
||
|
||
it("redirects teacher to teacher dashboard", async () => {
|
||
mocks.authMock.mockResolvedValue({
|
||
user: { id: "u_teacher", roles: ["teacher"] },
|
||
})
|
||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/teacher/dashboard")
|
||
})
|
||
|
||
it("redirects unknown role to teacher dashboard (fallback)", async () => {
|
||
mocks.authMock.mockResolvedValue({
|
||
user: { id: "u_unknown", roles: [] },
|
||
})
|
||
await expect(DashboardPage()).rejects.toThrow("REDIRECT:/teacher/dashboard")
|
||
})
|
||
})
|