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( }>页面内容} /> ); 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( }>页面内容} /> ); 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( }>页面内容} /> ); const content = document.querySelector('.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('.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( }>页面内容} /> ); 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( }>页面内容} /> ); 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( }>页面内容} /> ); 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( }>页面内容} /> ); 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( }>页面内容} /> ); 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( }>页面内容} /> ); expect(screen.getByRole('link', { name: '全局监控' })).toHaveAttribute('title', '全局监控'); expect(screen.queryByRole('button', { name: '收起侧栏' })).not.toBeInTheDocument(); expect(document.querySelector('.v2-sidebar')).toHaveClass('is-collapsed'); });