244 lines
12 KiB
TypeScript
244 lines
12 KiB
TypeScript
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');
|
||
});
|