Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/pages/CustomerMileagePage.test.tsx
T
lingniu d6b957dfc8 feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
2026-10-26 18:05:59 +08:00

178 lines
16 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, within } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { api } from '../../api/client';
import type { CustomerMileageResponse } from '../../api/customerMileage';
import CustomerMileagePage from './CustomerMileagePage';
afterEach(() => { cleanup(); vi.restoreAllMocks(); });
function fixture(): CustomerMileageResponse {
return { rules: { version: 3, lowMileageKm: 50, targetMileageKm: 200, deviationKm: 30, deviationPercent: 20, minimumGpsKm: 50, includeToday: false }, canConfigure: true, assignmentSource: 'oneos-live', assignmentObservedAt: '2026-09-30T01:00:00Z', assignmentStale: false, excludedUnassignedCount: 3, dates: ['2026-09-29'], groups: [{ customer: '客户甲', vehicleCount: 2, departments: ['业务一部'], responsiblePeople: ['负责人甲'], days: [{ date: '2026-09-29', vehicleCount: 2, gpsVehicleCount: 2, hydrogenVehicleCount: 1, gpsKm: 250, hydrogenKm: 100, suspectedChargingCount: 1, dataIssueCount: 1, lowMileageCount: 0, provisional: false }], vehicles: [{ vin: 'LNX00000000000001', plate: '沪A12345', customer: '客户甲', department: '业务一部', responsiblePerson: '负责人甲', date: '2026-09-29', gpsKm: 200, hydrogenKm: 100, gpsStatus: 'on-target', hydrogenStatus: 'below-target', differenceKm: 100, differencePercent: 50, alert: 'suspected-charging', provisional: false }, { vin: 'LNX00000000000002', plate: '沪A67890', customer: '客户甲', department: '', responsiblePerson: '', date: '2026-09-29', gpsKm: 50, hydrogenKm: null, gpsStatus: 'below-target', hydrogenStatus: 'missing', differenceKm: null, differencePercent: null, alert: 'missing-data', provisional: false }] }] };
}
function mount() { return render(<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })}><CustomerMileagePage /></QueryClientProvider>); }
test('preserves missing measurements and filters per-vehicle alerts', async () => {
vi.spyOn(api, 'customerMileage').mockResolvedValue(fixture()); mount(); fireEvent.click(await screen.findByRole('button', { name: '查看客户甲 2026-09-29车辆明细' })); await screen.findByText('沪A12345');
const row = screen.getByText('沪A67890').closest('tr')!; expect(row).toHaveTextContent('无数据'); expect(within(row).getAllByText('—').length).toBe(2); expect(within(screen.getByRole('dialog', { name: '客户甲' })).getByText('覆盖 2 / 1 / 2')).toBeInTheDocument();
fireEvent.change(screen.getByRole('combobox', { name: '筛选预警' }), { target: { value: 'suspected-charging' } }); expect(screen.queryByText('沪A67890')).not.toBeInTheDocument(); expect(screen.getByText('沪A12345')).toBeInTheDocument();
});
test('submits dates and customer filters, rejecting inverted date range', async () => {
const request = vi.spyOn(api, 'customerMileage').mockResolvedValue(fixture()); mount(); fireEvent.click(await screen.findByRole('button', { name: '查看客户甲 2026-09-29车辆明细' })); await screen.findByText('沪A12345');
fireEvent.click(screen.getByRole('button', { name: '关闭车辆明细' })); fireEvent.change(screen.getByLabelText('开始日期'), { target: { value: '2026-01-10' } }); fireEvent.change(screen.getByLabelText('结束日期'), { target: { value: '2026-01-01' } }); fireEvent.click(screen.getByRole('button', { name: '查询' })); expect(screen.getByRole('alert')).toHaveTextContent('有效日期范围'); expect(request).toHaveBeenCalledTimes(1);
fireEvent.change(screen.getByLabelText('结束日期'), { target: { value: '2026-01-31' } }); fireEvent.change(screen.getByPlaceholderText('搜索客户名称'), { target: { value: '客户甲' } }); fireEvent.click(screen.getByRole('button', { name: '查询' })); await waitFor(() => expect(request).toHaveBeenCalledTimes(2)); expect(request.mock.calls[1][0].get('dateFrom')).toBe('2026-01-10'); expect(request.mock.calls[1][0].get('customer')).toBe('客户甲');
});
test('saves versioned rules then refetches evaluated data', async () => {
const request = vi.spyOn(api, 'customerMileage').mockResolvedValue(fixture()); const save = vi.spyOn(api, 'saveCustomerMileageRules').mockImplementation(async r => ({ ...r, version: 4 })); mount(); fireEvent.click(await screen.findByRole('button', { name: '查看客户甲 2026-09-29车辆明细' })); await screen.findByText('沪A12345'); fireEvent.click(screen.getByRole('button', { name: '关闭车辆明细' })); fireEvent.click(screen.getByRole('button', { name: '预警规则' })); fireEvent.change(await screen.findByLabelText('偏差绝对值下限(km)'), { target: { value: '80' } }); fireEvent.click(screen.getByRole('button', { name: '保存并重新计算' })); await waitFor(() => expect(save).toHaveBeenCalled()); expect(save.mock.calls[0][0]).toMatchObject({ version: 3, deviationKm: 80 }); await waitFor(() => expect(request).toHaveBeenCalledTimes(2)); expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
test('readers inspect rules without edit controls', async () => {
const response = fixture(); response.canConfigure = false; vi.spyOn(api, 'customerMileage').mockResolvedValue(response); mount(); fireEvent.click(await screen.findByRole('button', { name: '查看客户甲 2026-09-29车辆明细' })); await screen.findByText('沪A12345'); fireEvent.click(screen.getByRole('button', { name: '关闭车辆明细' })); fireEvent.click(screen.getByRole('button', { name: '查看规则' })); expect(await screen.findByLabelText('达标里程(km)')).toBeDisabled(); expect(screen.queryByRole('button', { name: '保存并重新计算' })).not.toBeInTheDocument();
});
test('GZEV source replaces telemetry jobs and calculation notices', async () => {
const response = fixture(); response.hydrogenJobs = [{ date: '2026-09-29', status: 'pending', attempts: 0, completedAt: '' }];
vi.spyOn(api, 'customerMileage').mockResolvedValue(response); mount();
await screen.findByRole('button', { name: '查看客户甲 2026-09-29车辆明细' });
expect(screen.queryByText(/正在后台计算/)).not.toBeInTheDocument();
expect(screen.getByText(/hyRunMileage/)).toBeInTheDocument();
});
test('compares customers before opening details and keeps the selected customer and date together', async () => {
const response = fixture();
response.dates.push('2026-09-28');
const first = response.groups[0];
first.days.push({ ...first.days[0], date: '2026-09-28', gpsKm: 20, suspectedChargingCount: 0 });
first.vehicles.push({ ...first.vehicles[0], date: '2026-09-28', plate: '沪A前一天' });
response.groups.push({ ...first, customer: '客户乙', days: first.days.map(d => ({ ...d, suspectedChargingCount: 0, dataIssueCount: 0 })), vehicles: first.vehicles.map(v => ({ ...v, plate: '沪B乙客户', customer: '客户乙' })) });
vi.spyOn(api, 'customerMileage').mockResolvedValue(response);
mount();
const cell = await screen.findByRole('button', { name: '查看客户甲 2026-09-29车辆明细' });
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.getByText('客户乙')).toBeInTheDocument();
fireEvent.click(screen.getByRole('radio', { name: '疑似充电' }));
expect(screen.queryByText('客户乙')).not.toBeInTheDocument();
fireEvent.click(cell);
expect(await screen.findByText('沪A12345')).toBeInTheDocument();
fireEvent.change(screen.getByRole('combobox', { name: '明细日期' }), { target: { value: '2026-09-28' } });
expect(screen.getByText('沪A前一天')).toBeInTheDocument();
expect(screen.queryByText('沪A12345')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '关闭车辆明细' }));
fireEvent.click(screen.getByRole('radio', { name: '全部客户' }));
expect(screen.getByText('客户乙')).toBeInTheDocument();
});
test('paginates the customer overview and sorts the complete result', async () => {
const response = fixture();
const first = response.groups[0];
response.groups = Array.from({ length: 16 }, (_, index) => ({ ...first, customer: `客户${String(index).padStart(2, '0')}`, vehicleCount: index + 1 }));
vi.spyOn(api, 'customerMileage').mockResolvedValue(response); mount();
await screen.findByText('客户00');
expect(screen.queryByText('客户15')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '下一页客户' }));
expect(screen.getByText('客户15')).toBeInTheDocument();
expect(screen.queryByText('客户00')).not.toBeInTheDocument();
fireEvent.change(screen.getByRole('combobox', { name: '客户排序' }), { target: { value: 'vehicles' } });
expect(screen.getByText('客户15')).toBeInTheDocument();
expect(screen.queryByText('客户00')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '上一页客户' })).toBeDisabled();
});
test('resets dates and all customer filters to the initial query', async () => {
const request = vi.spyOn(api, 'customerMileage').mockResolvedValue(fixture());
mount();
await screen.findByRole('button', { name: '查看客户甲 2026-09-29车辆明细' });
const initial = request.mock.calls[0][0];
fireEvent.change(screen.getByLabelText('客户'), { target: { value: '客户甲' } });
fireEvent.change(screen.getByLabelText('部门'), { target: { value: '业务一部' } });
fireEvent.change(screen.getByLabelText('业务负责人'), { target: { value: '负责人甲' } });
fireEvent.change(screen.getByLabelText('开始日期'), { target: { value: '2026-01-01' } });
fireEvent.click(screen.getByRole('button', { name: '查询' }));
await waitFor(() => expect(request).toHaveBeenCalledTimes(2));
await waitFor(() => expect(screen.getByRole('button', { name: '重置' })).toBeEnabled());
fireEvent.click(screen.getByRole('button', { name: '重置' }));
expect(screen.getByLabelText('客户')).toHaveValue('');
expect(screen.getByLabelText('部门')).toHaveValue('');
expect(screen.getByLabelText('业务负责人')).toHaveValue('');
expect(screen.getByLabelText('开始日期')).toHaveValue(initial.get('dateFrom'));
expect(screen.getByLabelText('结束日期')).toHaveValue(initial.get('dateTo'));
fireEvent.click(screen.getByRole('button', { name: '快捷日期' }));
expect(screen.getByRole('radio', { name: '近 7 天' })).toBeChecked();
});
test('shows deviation and below-target counts independently without counting missing data', async () => {
vi.spyOn(api, 'customerMileage').mockResolvedValue(fixture()); mount();
const cell = await screen.findByRole('button', {name:'查看客户甲 2026-09-29车辆明细'});
expect(within(cell).getByText('偏差 1')).toBeInTheDocument();
expect(within(cell).getByText('未达标 2')).toBeInTheDocument();
fireEvent.click(cell);
expect(screen.getByText('GPS 达标 · 纯氢 未达标')).toBeInTheDocument();
expect(screen.getByText('GPS 未达标 · 纯氢 无数据,无法判断')).toBeInTheDocument();
});
test('drills into interval totals and then daily details', async () => {
vi.spyOn(api, 'customerMileage').mockResolvedValue(fixture()); mount();
fireEvent.click(await screen.findByRole('button', {name:'查看客户甲区间累计明细'}));
expect(await screen.findByRole('table',{name:'车辆区间累计'})).toHaveTextContent('沪A12345');
expect(screen.getByRole('table',{name:'车辆区间累计'})).toHaveTextContent('有数据 0 天');
fireEvent.click(screen.getByRole('button',{name:'2026-09-29'}));
expect(await screen.findByRole('table',{name:'客户车辆里程明细'})).toBeInTheDocument();
});
test('filters customer interval GPS totals with greater less and equal', async () => {
vi.spyOn(api, 'customerMileage').mockResolvedValue(fixture()); mount();
await screen.findByRole('button',{name:'查看客户甲区间累计明细'});
fireEvent.click(screen.getByRole('button',{name:'区间里程条件'}));
fireEvent.change(screen.getByRole('spinbutton',{name:'区间 GPS 里程阈值'}),{target:{value:'250'}});
expect(screen.queryByRole('button',{name:'查看客户甲区间累计明细'})).not.toBeInTheDocument();
fireEvent.change(screen.getByRole('combobox',{name:'区间 GPS 里程比较'}),{target:{value:'eq'}});
expect(screen.getByRole('button',{name:'查看客户甲区间累计明细'})).toBeInTheDocument();
fireEvent.change(screen.getByRole('combobox',{name:'区间 GPS 里程比较'}),{target:{value:'lt'}});
fireEvent.change(screen.getByRole('spinbutton',{name:'区间 GPS 里程阈值'}),{target:{value:'251'}});
expect(screen.getByRole('button',{name:'查看客户甲区间累计明细'})).toBeInTheDocument();
fireEvent.click(screen.getByRole('button',{name:'清除'}));
expect(screen.getByRole('spinbutton',{name:'区间 GPS 里程阈值'})).toHaveValue(null);
});
test('filters individual vehicles by GPS mileage in interval and daily details', async () => {
vi.spyOn(api, 'customerMileage').mockResolvedValue(fixture()); mount();
fireEvent.click(await screen.findByRole('button',{name:'查看客户甲区间累计明细'}));
fireEvent.change(screen.getByRole('spinbutton',{name:'单车 GPS 里程阈值'}),{target:{value:'100'}});
expect(screen.getByText('沪A12345')).toBeInTheDocument();
expect(screen.queryByText('沪A67890')).not.toBeInTheDocument();
fireEvent.change(screen.getByRole('combobox',{name:'单车 GPS 里程比较'}),{target:{value:'lt'}});
expect(screen.queryByText('沪A12345')).not.toBeInTheDocument();
expect(screen.getByText('沪A67890')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button',{name:'2026-09-29'}));
fireEvent.change(screen.getByRole('spinbutton',{name:'单车 GPS 里程阈值'}),{target:{value:'100'}});
expect(screen.getByText('沪A12345')).toBeInTheDocument();
expect(screen.queryByText('沪A67890')).not.toBeInTheDocument();
fireEvent.change(screen.getByRole('combobox',{name:'单车 GPS 里程比较'}),{target:{value:'lt'}});
expect(screen.getByText('沪A67890')).toBeInTheDocument();
expect(screen.queryByText('沪A12345')).not.toBeInTheDocument();
});
test('offers searchable dropdowns, keeps all options after filtering, and submits partial names', async () => {
const response = fixture();
response.groups.push({ ...response.groups[0], customer: '客户乙', departments: ['业务二部'], responsiblePeople: ['负责人乙'] });
const request = vi.spyOn(api, 'customerMileage').mockImplementation(async params => ({ ...response, groups: params.get('customer') ? response.groups.slice(0, 1) : response.groups }));
mount(); await screen.findByText('客户乙');
fireEvent.click(screen.getByRole('button', { name: '展开客户选项' }));
expect(within(screen.getByRole('listbox', { name: '客户选项' })).getAllByRole('option').map(option => option.textContent)).toEqual(['客户甲', '客户乙']);
fireEvent.change(screen.getByRole('combobox', { name: '客户' }), { target: { value: '甲' } });
expect(within(screen.getByRole('listbox', { name: '客户选项' })).getAllByRole('option').map(option => option.textContent)).toEqual(['客户甲']);
fireEvent.keyDown(screen.getByRole('combobox', { name: '客户' }), { key: 'ArrowDown' });
fireEvent.keyDown(screen.getByRole('combobox', { name: '客户' }), { key: 'Enter' });
expect(screen.getByLabelText('客户')).toHaveValue('客户甲'); expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText('部门'), { target: { value: '一部' } });
expect(screen.getByRole('option', { name: '业务一部' })).toBeInTheDocument();
fireEvent.blur(screen.getByLabelText('部门'));
fireEvent.change(screen.getByLabelText('业务负责人'), { target: { value: '甲' } });
expect(screen.getByRole('option', { name: '负责人甲' })).toBeInTheDocument();
fireEvent.blur(screen.getByLabelText('业务负责人'));
fireEvent.click(screen.getByRole('button', { name: '查询' }));
await waitFor(() => expect(request).toHaveBeenCalledTimes(2));
expect(request.mock.calls[1][0].get('department')).toBe('一部'); expect(request.mock.calls[1][0].get('responsiblePerson')).toBe('甲');
fireEvent.click(screen.getByRole('button', { name: '展开客户选项' }));
expect(screen.getByRole('option', { name: '客户乙' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '清除客户' })); expect(screen.getByLabelText('客户')).toHaveValue('');
});