fix(admin): 修复首次进入系统后点击任意导航被拉回 dashboard #67
105
apps/admin/src/components/DefaultRoute.integration.test.tsx
Normal file
105
apps/admin/src/components/DefaultRoute.integration.test.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { act } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { MemoryRouter, Route, Routes, useNavigate } from 'react-router';
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import DefaultRoute from './DefaultRoute';
|
||||||
|
import { RouteKeeper } from './RouteKeeper';
|
||||||
|
import { usePermissionStore } from '../store/permission/permissionStore';
|
||||||
|
import { useUserStore } from '../store/user/userStore';
|
||||||
|
|
||||||
|
let container: HTMLDivElement | null = null;
|
||||||
|
let root: ReturnType<typeof createRoot> | null = null;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
(
|
||||||
|
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }
|
||||||
|
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// 清掉可能由其他测试文件遗留的持久化数据,保证会话状态可控
|
||||||
|
Object.keys(localStorage).forEach((key) => localStorage.removeItem(key));
|
||||||
|
useUserStore.setState({ token: null, user: null });
|
||||||
|
usePermissionStore.setState({ permissions: [], status: 'unknown' });
|
||||||
|
useUserStore.getState().setSession('test-token', {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
roles: ['超级管理员'],
|
||||||
|
});
|
||||||
|
usePermissionStore.getState().writePermissions(['dashboard:view', 'student:view']);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (root) await act(async () => root?.unmount());
|
||||||
|
container?.remove();
|
||||||
|
root = null;
|
||||||
|
container = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageDashboard() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="page-dashboard">数据面板</div>
|
||||||
|
<button data-testid="go-students" onClick={() => navigate('/students')}>
|
||||||
|
去学生管理
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PageStudents() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="page-students">学生管理</div>
|
||||||
|
<button data-testid="go-home" onClick={() => navigate('/')}>
|
||||||
|
回首页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Harness() {
|
||||||
|
return (
|
||||||
|
<MemoryRouter initialEntries={['/']}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<RouteKeeper />}>
|
||||||
|
<Route index element={<DefaultRoute />} />
|
||||||
|
<Route path="dashboard" element={<PageDashboard />} />
|
||||||
|
<Route path="students" element={<PageStudents />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderHarness() {
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
await act(async () => {
|
||||||
|
root?.render(<Harness />);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DefaultRoute keep-alive regression', () => {
|
||||||
|
it('redirects to landing page once and does not hijack later navigations', async () => {
|
||||||
|
await renderHarness();
|
||||||
|
|
||||||
|
// 首次进入 '/' 应跳转到落地页 /dashboard,且只跳一次
|
||||||
|
expect(document.querySelector('[data-testid="page-dashboard"]')).not.toBeNull();
|
||||||
|
|
||||||
|
// 再导航到 /students,不应被保活的首页节点拉回 /dashboard
|
||||||
|
await act(async () => {
|
||||||
|
(document.querySelector('[data-testid="go-students"]') as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
expect(document.querySelector('[data-testid="page-students"]')).not.toBeNull();
|
||||||
|
|
||||||
|
// 回到 '/' 时仍应再次跳转到落地页
|
||||||
|
await act(async () => {
|
||||||
|
(document.querySelector('[data-testid="go-home"]') as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
expect(document.querySelector('[data-testid="page-dashboard"]')).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { Navigate } from 'react-router';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
import { Result, Spin } from 'antd';
|
import { Result, Spin } from 'antd';
|
||||||
import { usePermission } from '../hooks/usePermission';
|
import { usePermission } from '../hooks/usePermission';
|
||||||
import { findRoleAwareLandingPath } from '../auth/menu-policy';
|
import { findRoleAwareLandingPath } from '../auth/menu-policy';
|
||||||
@@ -8,14 +8,31 @@ import { useUserStore } from '../store/user/userStore';
|
|||||||
const DefaultRoute: React.FC = () => {
|
const DefaultRoute: React.FC = () => {
|
||||||
const { permissions, permissionsReady } = usePermission();
|
const { permissions, permissionsReady } = usePermission();
|
||||||
const roles = useUserStore((state) => state.user?.roles ?? []);
|
const roles = useUserStore((state) => state.user?.roles ?? []);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const firstPath = permissionsReady ? findRoleAwareLandingPath(roles, permissions) : null;
|
||||||
|
|
||||||
|
// 用 effect 导航替代声明式 <Navigate>:RouteKeeper 会把首页(index)节点保活缓存,
|
||||||
|
// 声明式 <Navigate> 在缓存节点随路由变化重渲染时会反复触发,导致首次进入系统后
|
||||||
|
// 点击任何按钮都被拉回 dashboard,必须刷新页面才能恢复。这里仅在确实处于首页
|
||||||
|
// 且已计算出落点时跳转;navigate 通过 ref 持有,避免其每次渲染变化导致 effect 空转。
|
||||||
|
const navigateRef = useRef(navigate);
|
||||||
|
navigateRef.current = navigate;
|
||||||
|
useEffect(() => {
|
||||||
|
if (pathname === '/' && firstPath) {
|
||||||
|
navigateRef.current(firstPath, { replace: true });
|
||||||
|
}
|
||||||
|
}, [pathname, firstPath]);
|
||||||
|
|
||||||
if (!permissionsReady) {
|
if (!permissionsReady) {
|
||||||
return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
|
return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
|
||||||
}
|
}
|
||||||
const firstPath = findRoleAwareLandingPath(roles, permissions);
|
if (!firstPath) {
|
||||||
if (firstPath) return <Navigate to={firstPath} replace />;
|
return (
|
||||||
return (
|
<Result status="403" title="暂无可访问功能" subTitle="请联系管理员为当前账号分配功能权限" />
|
||||||
<Result status="403" title="暂无可访问功能" subTitle="请联系管理员为当前账号分配功能权限" />
|
);
|
||||||
);
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DefaultRoute;
|
export default DefaultRoute;
|
||||||
|
|||||||
Reference in New Issue
Block a user