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');
});