Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx
2026-07-18 19:41:01 +08:00

359 lines
22 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 { 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(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
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(view.container.querySelector('.v2-user-discovery-shell')).toContainElement(screen.getByLabelText('账号管理操作'));
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(screen.getByRole('row', { name: '客户账号目录列' })).toHaveTextContent('客户账号菜单权限车辆权限最近登录访问状态');
expect(screen.getByText('权限就绪 1')).toBeInTheDocument();
expect(screen.getByText('车辆授权 1')).toBeInTheDocument();
expect(screen.getByText('权限就绪')).toBeInTheDocument();
expect(view.container.querySelector('.v2-user-directory-metrics')).not.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(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument();
fireEvent.click(customer);
await waitFor(() => expect(customer).toHaveAttribute('aria-expanded', 'true'));
expect(screen.getByLabelText('账号权限概览')).toHaveTextContent('@customer-east菜单 1车辆 1最近登录 尚未登录权限就绪');
const editorSheet = document.querySelector<HTMLElement>('.v2-user-editor-sidesheet .semi-sidesheet-inner');
expect(editorSheet).toHaveAttribute('aria-label', '客户账号详情');
expect(editorSheet).toHaveStyle({ width: 'min(840px, 100vw)' });
expect(document.querySelector('.v2-user-editor-sidesheet .v2-user-editor-tabs.semi-tabs')).toBeInTheDocument();
expect(document.querySelector('.v2-user-editor-form')).toHaveAttribute('id', 'v2-user-editor-form');
expect(screen.getAllByRole('tab')).toHaveLength(3);
expect(document.querySelector('.v2-user-vehicle-section.semi-card')).toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: '登录身份' }));
expect(screen.getByLabelText('登录身份摘要')).toHaveTextContent('登录账号customer-east不可修改');
expect(screen.getByLabelText('登录身份摘要')).toHaveTextContent('客户显示名称华东客户已设置');
expect(screen.getByRole('group', { name: '基础身份' })).toHaveTextContent('用于客户登录和平台内展示');
expect(screen.getByRole('group', { name: '安全与系统映射' })).toHaveTextContent('密码留空则保持不变');
fireEvent.click(screen.getByRole('tab', { name: /菜单权限/ }));
expect(screen.getByText('客户开放菜单').closest('.semi-tag')).toBeInTheDocument();
expect(screen.getByLabelText('已开放 1 个菜单,共 4 个')).toHaveTextContent('客户工作台按最小权限开放');
expect(screen.getAllByText('已开放')).toHaveLength(1);
expect(screen.getAllByText('未开放')).toHaveLength(3);
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(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
fireEvent.click(await screen.findByRole('button', { name: /选择客户 华东客户/ }));
expect(screen.getAllByRole('button', { name: /^移除 / })).toHaveLength(10);
expect(screen.getByText('第 110 条,共 12 辆')).toBeInTheDocument();
expect(document.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('第 11 条,共 1 辆')).toBeInTheDocument();
});
test('makes draft state explicit, protects bulk removal and preserves edits across a same-revision refresh', async () => {
const customer = {
id: 7,
username: 'customer-east',
displayName: '华东客户',
userType: 'customer',
status: 'enabled',
customerRef: '',
tenantRef: '',
authProvider: 'local',
menuKeys: ['monitor', 'vehicles', 'tracks', 'statistics'],
vehicleVins: ['VIN001'],
vehicles: [{ vin: 'VIN001', plate: '粤A11111', validFrom: '2026-06-05T00:00:00+08:00', sourceSystem: 'manual', grantedBy: '平台管理员' }],
grantHistory: [],
createdAt: '2026-07-16T00:00:00Z',
updatedAt: '2026-07-16T00:00:00Z'
};
mocks.adminUsers.mockResolvedValue([customer]);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
fireEvent.click(await screen.findByRole('button', { name: /选择客户 华东客户/ }));
expect(screen.getByText('已同步')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '保存权限' })).toBeDisabled();
fireEvent.click(screen.getByRole('tab', { name: '登录身份' }));
const displayName = screen.getByDisplayValue('华东客户');
fireEvent.change(displayName, { target: { value: '华东客户(更新)' } });
expect(screen.getByText('待保存')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '保存权限' })).toBeEnabled();
client.setQueryData(['admin-users'], [{ ...customer }]);
await waitFor(() => expect(screen.getByDisplayValue('华东客户(更新)')).toBeInTheDocument());
fireEvent.click(screen.getByRole('tab', { name: /车辆权限/ }));
fireEvent.click(screen.getByRole('button', { name: '清空全部 1 辆车辆权限' }));
expect(await screen.findByText('清空 1 辆车辆权限?')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '确认清空' }));
await waitFor(() => expect(screen.queryByRole('button', { name: '移除 粤A11111' })).not.toBeInTheDocument());
expect(screen.getByText('已从草稿移除 1 辆车,保存后生效')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '关闭账号详情' }));
expect(await screen.findByText('放弃未保存的更改?')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'cancel' }));
expect(document.querySelector('.v2-user-editor-form')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '保存权限' })).toBeEnabled();
});
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(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
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();
expect(screen.getByRole('button', { name: '新建客户账号' })).toHaveClass('v2-workspace-mobile-tool-button', 'is-primary');
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(document.querySelector('.v2-user-editor-sidesheet')).not.toBeInTheDocument();
fireEvent.click(customer);
expect(await screen.findByRole('button', { name: '关闭账号详情' })).toBeInTheDocument();
expect(customer).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(screen.getByRole('button', { name: '关闭账号详情' }));
await waitFor(() => expect(document.querySelector('.v2-user-editor-form')).not.toBeInTheDocument());
expect(customer).toHaveAttribute('aria-expanded', 'false');
expect(screen.getAllByRole('button', { name: '新建客户账号' })).toHaveLength(1);
});
test('keeps granted vehicles primary and reveals mobile assignment tools on demand', async () => {
layout.mobile = true;
const vehicles = Array.from({ length: 5 }, (_, 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 } } });
render(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
fireEvent.click(await screen.findByRole('button', { name: /选择客户 华东客户/ }));
const toggle = await screen.findByRole('button', { name: '添加授权车辆' });
expect(toggle).toHaveAttribute('aria-expanded', 'false');
expect(document.querySelector('.v2-user-vehicle-section')).toHaveClass('is-composer-collapsed');
expect(screen.queryByRole('textbox', { name: '按车牌或 VIN 搜索' })).not.toBeInTheDocument();
expect(screen.getAllByRole('button', { name: /^移除 / })).toHaveLength(4);
expect(screen.getByText('第 14 条,共 5 辆')).toBeInTheDocument();
expect(document.querySelectorAll('.v2-assigned-vehicle-card.is-mobile-compact')).toHaveLength(4);
expect(document.querySelector('.v2-assigned-vehicle-interval')).toHaveAttribute('aria-label', '授权有效期:启用 2026-06-05 00:00停用 持续有效');
expect(document.querySelector('.v2-user-editor-last-login')).toHaveTextContent('最近登录');
fireEvent.click(toggle);
expect(screen.getByRole('button', { name: '收起添加车辆' })).toHaveAttribute('aria-expanded', 'true');
expect(document.querySelector('.v2-user-vehicle-section')).not.toHaveClass('is-composer-collapsed');
expect(screen.getByRole('textbox', { name: '按车牌或 VIN 搜索' })).toBeInTheDocument();
expect(document.querySelector('#v2-user-vehicle-composer')).toBeInTheDocument();
});
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(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
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<HTMLElement>('.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('权限就绪 0')).toBeInTheDocument();
expect(screen.getByText('车辆授权 0')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '清空账号筛选' }));
await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument());
expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument();
fireEvent.click(screen.getByRole('combobox', { name: '访问状态' }));
const attentionOption = await waitFor(() => {
const option = [...document.querySelectorAll<HTMLElement>('.semi-select-option')]
.find((item) => item.textContent?.includes('待完善权限'));
expect(option).toBeInTheDocument();
return option!;
});
fireEvent.click(attentionOption);
await waitFor(() => expect(screen.queryByRole('button', { name: /选择客户 西部客户/ })).not.toBeInTheDocument());
expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument();
expect(screen.getByText('待分配车辆')).toBeInTheDocument();
expect(screen.getByText('待完善 1')).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(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
expect(await screen.findByText('还没有客户账号')).toBeInTheDocument();
expect(view.container.querySelector('.v2-user-list-empty.semi-empty')).toBeInTheDocument();
expect(document.querySelector('.v2-user-editor-sidesheet')).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(<QueryClientProvider client={client}><UsersPage /></QueryClientProvider>);
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: '' }]
});
});