Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/layout/AppShell.test.tsx
2026-07-18 11:09:27 +08:00

244 lines
12 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.
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { ROUTER_FUTURE } from '../routing/routerConfig';
const routing = vi.hoisted(() => ({
preloadRoute: vi.fn(async () => undefined),
scheduleIdleRoutePreloads: vi.fn(() => vi.fn()),
shouldPreloadRouteOnIntent: vi.fn(() => true)
}));
const auth = vi.hoisted(() => ({
session: { name: 'test-user', username: 'test-user', role: 'viewer' as const } as any,
logout: vi.fn()
}));
const apiMocks = vi.hoisted(() => ({
changePassword: vi.fn()
}));
vi.mock('../routing/routeModules', () => routing);
vi.mock('../auth/AuthGate', () => ({
usePlatformSession: () => ({ session: auth.session, logout: auth.logout })
}));
vi.mock('../../api/client', () => ({ api: { changePassword: apiMocks.changePassword } }));
import { AppShell } from './AppShell';
afterEach(() => {
cleanup();
vi.restoreAllMocks();
vi.clearAllMocks();
auth.session = { name: 'test-user', username: 'test-user', role: 'viewer' };
apiMocks.changePassword.mockReset();
});
test('renders only explicitly assigned customer menus', () => {
auth.session = { name: '客户甲', role: 'customer', userType: 'customer', menuKeys: ['monitor', 'vehicles', 'tracks', 'statistics'] };
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
expect(screen.getByRole('link', { name: '全局监控' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '车辆查询' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '里程查询' })).toBeInTheDocument();
expect(screen.queryByRole('link', { name: '历史数据' })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: '告警中心' })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: '接入管理' })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: '账号管理' })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: '运维质量' })).not.toBeInTheDocument();
});
test('reschedules likely route preloads when the active module changes', async () => {
const firstCleanup = vi.fn();
const secondCleanup = vi.fn();
routing.scheduleIdleRoutePreloads
.mockReturnValueOnce(firstCleanup)
.mockReturnValueOnce(secondCleanup);
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
expect(routing.scheduleIdleRoutePreloads).toHaveBeenCalledWith({ activePathname: '/monitor' });
fireEvent.click(screen.getByRole('link', { name: '历史数据' }));
await waitFor(() => expect(routing.scheduleIdleRoutePreloads).toHaveBeenLastCalledWith({ activePathname: '/history' }));
expect(firstCleanup).toHaveBeenCalledTimes(1);
cleanup();
expect(secondCleanup).toHaveBeenCalledTimes(1);
});
test('marks the content as a full-height track workspace after sidebar navigation', async () => {
const scrollTo = vi.fn();
Object.defineProperty(HTMLElement.prototype, 'scrollTo', { configurable: true, value: scrollTo });
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
const content = document.querySelector<HTMLElement>('.v2-content')!;
expect(content).not.toHaveClass('is-track-workspace');
content.scrollTop = 240;
scrollTo.mockClear();
fireEvent.click(screen.getByRole('link', { name: '轨迹回放' }));
await waitFor(() => expect(document.querySelector('.v2-content')).toHaveClass('is-track-workspace'));
expect(document.querySelector<HTMLElement>('.v2-content')?.scrollTop).toBe(0);
expect(scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, behavior: 'auto' });
expect(screen.getByRole('heading', { name: '轨迹回放' })).toBeInTheDocument();
});
test('opens contextual help for the active module and closes it without navigating', () => {
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
const help = screen.getByRole('button', { name: '帮助' });
expect(help).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(help);
expect(screen.getByRole('dialog', { name: '全局监控' })).toHaveTextContent('筛选会自动生效');
expect(document.querySelector('.v2-help-overview.semi-card')).toBeInTheDocument();
expect(screen.getByText('建议操作')).toBeInTheDocument();
expect(help).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(screen.getByLabelText('关闭帮助'));
expect(help).toHaveAttribute('aria-expanded', 'false');
expect(document.body.style.overflow).not.toBe('hidden');
expect(screen.getByText('页面内容')).toBeInTheDocument();
});
test('uses one Semi account menu for identity, password and logout actions', async () => {
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
const trigger = screen.getByRole('button', { name: '账号菜单test-user' });
expect(trigger).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(trigger);
expect(await screen.findByText('@test-user')).toBeInTheDocument();
expect(screen.getByText('当前登录账号')).toBeInTheDocument();
await waitFor(() => expect(screen.getByRole('button', { name: '账号菜单test-user' })).toHaveAttribute('aria-expanded', 'true'));
fireEvent.click(screen.getByText('修改登录密码'));
expect(screen.getByRole('dialog', { name: '修改登录密码' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '取消' }));
fireEvent.click(trigger);
fireEvent.click(await screen.findByText('退出登录'));
expect(auth.logout).toHaveBeenCalledTimes(1);
});
test('validates password policy while typing and submits only a complete secure change', async () => {
apiMocks.changePassword.mockResolvedValue({ changed: true });
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
fireEvent.click(screen.getByRole('button', { name: '账号菜单test-user' }));
fireEvent.click(await screen.findByText('修改登录密码'));
const submit = screen.getByRole('button', { name: '确认修改' });
expect(submit).toBeDisabled();
expect(document.querySelector('.v2-password-dialog > .semi-banner-warning')).toHaveTextContent('当前设备和其他设备上的旧会话都会失效');
fireEvent.change(screen.getByLabelText('当前密码'), { target: { value: 'CurrentPass!1' } });
fireEvent.change(screen.getByLabelText('新密码'), { target: { value: 'StrongPass!1' } });
expect(screen.getByLabelText('新密码安全规则').querySelectorAll('.is-valid')).toHaveLength(2);
fireEvent.change(screen.getByLabelText('确认新密码'), { target: { value: 'different' } });
expect(screen.getByRole('status')).toHaveTextContent('两次输入的新密码不一致');
expect(submit).toBeDisabled();
fireEvent.change(screen.getByLabelText('确认新密码'), { target: { value: 'StrongPass!1' } });
expect(screen.getByRole('status')).toHaveTextContent('两次输入一致');
expect(submit).toBeEnabled();
fireEvent.click(submit);
await waitFor(() => expect(apiMocks.changePassword).toHaveBeenCalledWith({ currentPassword: 'CurrentPass!1', newPassword: 'StrongPass!1' }));
expect(auth.logout).toHaveBeenCalledTimes(1);
});
test('uses the compact mobile navigation and opens secondary modules in a Semi SideSheet', () => {
vi.spyOn(window, 'matchMedia').mockReturnValue({
matches: true,
media: '(max-width: 680px)',
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
});
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
expect(screen.getByRole('heading', { name: '全局监控' })).toHaveClass('v2-topbar-page-title');
expect(screen.getByRole('navigation', { name: '主导航' })).toHaveStyle({ '--v2-mobile-nav-count': '5' });
expect(screen.getByRole('link', { name: '全局监控' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '车辆查询' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '里程查询' })).toBeInTheDocument();
expect(screen.queryByRole('link', { name: '历史数据' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '更多功能' }));
const moreSheet = screen.getByRole('dialog', { name: '更多功能' });
expect(moreSheet).toHaveTextContent('数据分析与系统管理');
expect(screen.getByRole('link', { name: '历史数据' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '告警中心' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '接入管理' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '账号管理' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: '运维质量' })).toBeInTheDocument();
fireEvent.click(screen.getByLabelText('关闭更多功能'));
expect(screen.getByRole('button', { name: '更多功能' })).toHaveAttribute('aria-expanded', 'false');
expect(document.body.style.overflow).not.toBe('hidden');
});
test('lets customer mobile navigation use the full width when no more menu is needed', () => {
auth.session = { name: '客户甲', role: 'customer', userType: 'customer', menuKeys: ['monitor', 'vehicles', 'tracks', 'statistics'] };
vi.spyOn(window, 'matchMedia').mockReturnValue({
matches: true,
media: '(max-width: 680px)',
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
});
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/statistics']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
expect(screen.getByRole('heading', { name: '里程查询' })).toHaveClass('v2-topbar-page-title');
expect(screen.getByRole('navigation', { name: '主导航' })).toHaveStyle({ '--v2-mobile-nav-count': '4' });
expect(screen.queryByRole('button', { name: '更多功能' })).not.toBeInTheDocument();
expect(screen.getByRole('link', { name: '里程查询' })).toHaveAttribute('aria-current', 'page');
});
test('automatically collapses the Semi sidebar at tablet width', () => {
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
matches: query === '(max-width: 900px)',
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}));
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/monitor']}>
<Routes><Route element={<AppShell />}><Route path="*" element={<span></span>} /></Route></Routes>
</MemoryRouter>);
expect(screen.getByRole('link', { name: '全局监控' })).toHaveAttribute('title', '全局监控');
expect(screen.queryByRole('button', { name: '收起侧栏' })).not.toBeInTheDocument();
expect(document.querySelector('.v2-sidebar')).toHaveClass('is-collapsed');
});