import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, expect, test, vi } from 'vitest'; import UsersPage from './UsersPage'; const mocks = vi.hoisted(() => ({ adminUsers: vi.fn(), vehicleCoverage: vi.fn(), createCustomerUser: vi.fn(), updateCustomerUser: vi.fn() })); const layout = vi.hoisted(() => ({ mobile: false })); vi.mock('../../api/client', () => ({ api: mocks })); vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: () => layout.mobile })); afterEach(() => { cleanup(); layout.mobile = false; Object.values(mocks).forEach((mock) => mock.mockReset()); }); test('deduplicates vehicle candidates by VIN and renders granted vehicles plate first', async () => { mocks.adminUsers.mockResolvedValue([{ id: 7, username: 'customer-east', displayName: '华东客户', userType: 'customer', status: 'enabled', customerRef: '', tenantRef: '', authProvider: 'local', menuKeys: ['monitor'], vehicleVins: ['VIN001'], vehicles: [{ vin: 'VIN001', plate: '粤A11111', validFrom: '2026-06-05T00:00:00+08:00', sourceSystem: 'manual', grantedBy: '平台管理员' }], grantHistory: [{ id: 1, vin: 'VIN001', plate: '粤A11111', validFrom: '2026-06-05T00:00:00+08:00', sourceSystem: 'manual', grantedBy: '平台管理员', revokedBy: '', createdAt: '2026-07-16T00:00:00Z', updatedAt: '2026-07-16T00:00:00Z' }], createdAt: '2026-07-16T00:00:00Z', updatedAt: '2026-07-16T00:00:00Z' }]); const duplicatedVehicle = { vin: 'VIN002', plate: '粤A22222', protocols: ['GB32960', 'JT808'] }; mocks.vehicleCoverage.mockResolvedValue({ items: [duplicatedVehicle, duplicatedVehicle], total: 1, limit: 20, offset: 0 }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); const customer = await screen.findByRole('button', { name: /选择客户 华东客户/ }); expect(screen.getByLabelText('账号管理操作')).toHaveTextContent('客户访问治理'); expect(screen.getByLabelText('账号管理操作')).toHaveTextContent('变更最多 30 秒生效'); expect(view.container.querySelector('.v2-page-heading')).not.toBeInTheDocument(); expect(screen.getByRole('heading', { name: '账号范围', level: 5 })).toBeInTheDocument(); expect(screen.getByText('按客户名称、登录账号、客户标识与启停状态查找')).toBeInTheDocument(); expect(screen.getAllByText('1 / 1 个账号')).toHaveLength(2); expect(view.container.querySelector('.v2-user-list.semi-card')).toBeInTheDocument(); expect(view.container.querySelector('.v2-user-filter-panel.v2-workspace-filter-panel.semi-card')).toBeInTheDocument(); expect(view.container.querySelector('.v2-customer-list.semi-list')).toBeInTheDocument(); expect(customer).toHaveClass('semi-button', 'v2-user-list-item'); expect(customer.closest('.semi-list-item')).toHaveClass('v2-user-list-row'); expect(customer).toHaveAttribute('aria-pressed', 'false'); expect(customer).toHaveAttribute('aria-expanded', 'false'); expect(view.container.querySelector('.v2-user-editor.semi-card')).not.toBeInTheDocument(); fireEvent.click(customer); await waitFor(() => expect(customer).toHaveAttribute('aria-expanded', 'true')); expect(view.container.querySelector('.v2-user-editor.semi-card')).toBeInTheDocument(); expect(view.container.querySelector('.v2-user-editor-tabs.semi-tabs')).toBeInTheDocument(); expect(screen.getAllByRole('tab')).toHaveLength(3); expect(view.container.querySelector('.v2-user-vehicle-section.semi-card')).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: /菜单权限/ })); expect(screen.getByText('客户开放菜单').closest('.semi-tag')).toBeInTheDocument(); fireEvent.click(screen.getByRole('tab', { name: /车辆权限/ })); expect(document.querySelector('.v2-user-vehicle-section .v2-user-section-title .semi-tag')).toHaveTextContent('1 辆'); const granted = await screen.findByRole('button', { name: '移除 粤A11111' }); expect(granted).toHaveClass('semi-button'); expect(granted.closest('.v2-assigned-vehicle-card')).toHaveClass('semi-card'); expect(granted.closest('.v2-assigned-vehicle-card')).toHaveTextContent('粤A11111VIN001'); expect(screen.getByRole('button', { name: '调整 粤A11111 有效期' })).toHaveAttribute('aria-haspopup', 'dialog'); expect(document.querySelectorAll('input[type="datetime-local"]')).toHaveLength(0); fireEvent.click(screen.getByRole('button', { name: '调整 粤A11111 有效期' })); expect(await screen.findByLabelText('粤A11111 启用时间')).toHaveValue('2026-06-05T00:00'); expect(document.querySelectorAll('input[type="datetime-local"]')).toHaveLength(2); expect(document.querySelector('.v2-user-grant-sidesheet .semi-sidesheet-inner')).toHaveAttribute('aria-label', '车辆授权有效期'); fireEvent.click(screen.getByRole('button', { name: '关闭车辆授权有效期' })); expect(document.querySelectorAll('input[type="datetime-local"]')).toHaveLength(0); expect(document.querySelector('.v2-user-avatar')).toHaveClass('semi-avatar'); expect(document.querySelector('.v2-user-status-tag')).toHaveClass('semi-tag'); expect(document.querySelector('.v2-grant-history')).toHaveClass('semi-collapse'); expect(document.querySelector('.v2-grant-history-title .semi-tag')).toHaveTextContent('1 条'); fireEvent.change(screen.getByRole('textbox', { name: '按车牌或 VIN 搜索' }), { target: { value: '粤A22222' } }); await waitFor(() => expect(mocks.vehicleCoverage).toHaveBeenCalled()); const candidates = await screen.findAllByRole('option', { name: /粤A22222.*VIN002.*选择/ }); expect(candidates).toHaveLength(1); expect((mocks.vehicleCoverage.mock.calls[0][0] as URLSearchParams).get('keyword')).toBe('粤A22222'); fireEvent.click(candidates[0]); expect((await screen.findByRole('button', { name: '移除 粤A22222' })).closest('.v2-assigned-vehicle-card')).toHaveTextContent('粤A22222VIN002'); }); test('pages and filters large vehicle grants without nesting a vehicle-list scrollbar', async () => { const vehicles = Array.from({ length: 12 }, (_, index) => ({ vin: `VIN${String(index + 1).padStart(3, '0')}`, plate: `粤A${String(index + 1).padStart(5, '0')}`, validFrom: '2026-06-05T00:00:00+08:00', sourceSystem: 'manual', grantedBy: '平台管理员' })); mocks.adminUsers.mockResolvedValue([{ id: 7, username: 'customer-east', displayName: '华东客户', userType: 'customer', status: 'enabled', customerRef: '', tenantRef: '', authProvider: 'local', menuKeys: ['monitor', 'vehicles', 'tracks', 'statistics'], vehicleVins: vehicles.map((vehicle) => vehicle.vin), vehicles, grantHistory: [], createdAt: '2026-07-16T00:00:00Z', updatedAt: '2026-07-16T00:00:00Z' }]); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); fireEvent.click(await screen.findByRole('button', { name: /选择客户 华东客户/ })); expect(screen.getAllByRole('button', { name: /^移除 / })).toHaveLength(10); expect(screen.getByText('第 1–10 条,共 12 辆')).toBeInTheDocument(); expect(view.container.querySelector('.v2-assigned-pagination')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Next' })); expect(await screen.findByRole('button', { name: '移除 粤A00012' })).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: /^移除 / })).toHaveLength(2); fireEvent.change(screen.getByRole('textbox', { name: '筛选已授权车辆' }), { target: { value: '粤A00012' } }); expect(screen.getAllByRole('button', { name: /^移除 / })).toHaveLength(1); expect(screen.getByText('1 条匹配结果')).toBeInTheDocument(); expect(screen.getByText('第 1–1 条,共 1 辆')).toBeInTheDocument(); }); test('keeps the customer directory visible on mobile and opens details on demand', async () => { layout.mobile = true; mocks.adminUsers.mockResolvedValue([{ id: 7, username: 'customer-east', displayName: '华东客户', userType: 'customer', status: 'enabled', customerRef: '', tenantRef: '', authProvider: 'local', menuKeys: ['monitor'], vehicleVins: [], vehicles: [], grantHistory: [], createdAt: '2026-07-16T00:00:00Z', updatedAt: '2026-07-16T00:00:00Z' }]); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); expect(await screen.findByText('客户权限目录')).toBeInTheDocument(); const customer = await screen.findByRole('button', { name: /选择客户 华东客户/ }); expect(view.container.querySelector('.v2-user-mobile-picker')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-customer-list.semi-list')).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: '搜索客户账号' })).toBeInTheDocument(); const filterToggle = screen.getByRole('button', { name: '修改账号范围:全部状态' }); expect(filterToggle).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(filterToggle); expect(screen.getByRole('button', { name: '收起账号范围:全部状态' })).toHaveAttribute('aria-expanded', 'true'); expect(view.container.querySelector('.v2-user-editor')).not.toBeInTheDocument(); fireEvent.click(customer); expect(await screen.findByRole('button', { name: '关闭账号详情' })).toBeInTheDocument(); expect(customer).toHaveAttribute('aria-expanded', 'true'); fireEvent.click(screen.getByRole('button', { name: '关闭账号详情' })); expect(view.container.querySelector('.v2-user-editor')).not.toBeInTheDocument(); expect(customer).toHaveAttribute('aria-expanded', 'false'); expect(screen.getAllByRole('button', { name: '新建客户账号' })).toHaveLength(1); }); test('filters the customer directory by status and exposes the active result scope', async () => { mocks.adminUsers.mockResolvedValue([ { id: 7, username: 'customer-east', displayName: '华东客户', userType: 'customer', status: 'enabled', customerRef: 'east', tenantRef: '', authProvider: 'local', menuKeys: ['monitor'], vehicleVins: [], vehicles: [], grantHistory: [], createdAt: '2026-07-16T00:00:00Z', updatedAt: '2026-07-16T00:00:00Z' }, { id: 8, username: 'customer-west', displayName: '西部客户', userType: 'customer', status: 'disabled', customerRef: 'west', tenantRef: '', authProvider: 'local', menuKeys: ['monitor'], vehicleVins: [], vehicles: [], grantHistory: [], createdAt: '2026-07-16T00:00:00Z', updatedAt: '2026-07-16T00:00:00Z' } ]); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); expect(await screen.findByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument(); fireEvent.click(screen.getByRole('combobox', { name: '账号状态' })); const disabledOption = await waitFor(() => { const option = [...document.querySelectorAll('.semi-select-option')] .find((item) => item.textContent?.includes('停用账号')); expect(option).toBeInTheDocument(); return option!; }); fireEvent.click(disabledOption); await waitFor(() => expect(screen.queryByRole('button', { name: /选择客户 华东客户/ })).not.toBeInTheDocument()); expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument(); expect(screen.getAllByText('1 / 2 个账号')).toHaveLength(2); expect(screen.getByText('启用账号').nextSibling).toHaveTextContent('0'); fireEvent.click(screen.getByRole('button', { name: '清空账号筛选' })); await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument()); expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument(); }); test('uses a standard Semi empty state when no customer account exists', async () => { mocks.adminUsers.mockResolvedValue([]); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const view = render(); expect(await screen.findByText('还没有客户账号')).toBeInTheDocument(); expect(view.container.querySelector('.v2-user-list-empty.semi-empty')).toBeInTheDocument(); expect(view.container.querySelector('.v2-user-editor')).not.toBeInTheDocument(); expect(screen.getAllByRole('button', { name: '创建第一个客户账号' })).toHaveLength(1); }); test('submits per-vehicle authorization interval instead of only a VIN list', async () => { mocks.adminUsers.mockResolvedValue([{ id: 7, username: 'customer-east', displayName: '华东客户', userType: 'customer', status: 'enabled', customerRef: '', tenantRef: '', authProvider: 'local', menuKeys: ['statistics'], vehicleVins: ['VIN001'], vehicles: [{ vin: 'VIN001', plate: '粤A11111', validFrom: '2026-07-16T14:08:18+08:00', sourceSystem: 'manual', grantedBy: '平台管理员' }], grantHistory: [], createdAt: '2026-07-16T00:00:00Z', updatedAt: '2026-07-16T00:00:00Z' }]); mocks.updateCustomerUser.mockResolvedValue({ id: 7 }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); render(); fireEvent.click(await screen.findByRole('button', { name: /选择客户 华东客户/ })); expect(document.querySelectorAll('input[type="datetime-local"]')).toHaveLength(0); fireEvent.click(screen.getByRole('button', { name: '调整 粤A11111 有效期' })); fireEvent.change(await screen.findByLabelText('粤A11111 启用时间'), { target: { value: '2026-06-05T00:00' } }); fireEvent.click(screen.getByRole('button', { name: '完成' })); fireEvent.click(screen.getByRole('button', { name: '保存权限' })); await waitFor(() => expect(mocks.updateCustomerUser).toHaveBeenCalled()); expect(mocks.updateCustomerUser.mock.calls[0][1]).toMatchObject({ vehicleVins: ['VIN001'], vehicleGrants: [{ vin: 'VIN001', validFrom: '2026-06-05T00:00', validTo: '' }] }); });