Some checks failed
CI / check (pull_request) Failing after 2m14s
根因:RouteKeeper 保活缓存了首页 index 节点,DefaultRoute 中的声明式 <Navigate> 会随缓存节点在后续路由变化时反复触发 replace,导致所有导航 被强制拉回落地页(dashboard),刷新后因不再经过 / 而恢复正常。 修复:DefaultRoute 改为 effect 导航,仅在确实处于 / 且已算出落地页时 跳转一次;navigate 通过 ref 持有避免 effect 空转。新增集成测试覆盖 「首次进入 / → 跳转一次 → 后续导航不被劫持 → 回到 / 仍能重定向」。
106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
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();
|
||
});
|
||
});
|