// @vitest-environment jsdom
import React from 'react';
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
expectTypeOf,
it,
vi,
} from 'vitest';
import {
DetailRecordFilterBar,
DetailRecordFooter,
DetailRecordTable,
type DetailRecordColumn,
} from '../../src/prototypes/vehicle-management/components/DetailRecordPrimitives';
import { DetailInsuranceRecordsTab } from '../../src/prototypes/vehicle-management/components/DetailInsuranceRecordsTab';
import {
DetailAccidentRecordsTab,
DetailAnnualReviewRecordsTab,
DetailLeaseRecordsTab,
DetailMovementRecordsTab,
DetailTransferRecordsTab,
DetailViolationRecordsTab,
} from '../../src/prototypes/vehicle-management/components/DetailRecordTabs';
import { DetailFaultRecordsTab } from '../../src/prototypes/vehicle-management/components/DetailFaultRecordsTab';
import vehicles from '../../src/prototypes/vehicle-management/data/vehicles.json';
import type { VehicleRecord } from '../../src/prototypes/vehicle-management/types';
import {
buttonByName,
click,
renderReact,
type RenderReactResult,
} from './detailTestUtils';
interface FixtureRow {
id: string;
code: string;
status: string;
}
const renderCode = vi.fn((row: FixtureRow) => (
{`编号:${row.code}`}
));
const renderStatus = vi.fn((row: FixtureRow) => (
{`状态标签:${row.status || '—'}`}
));
const columns: DetailRecordColumn[] = [
{
key: 'code',
label: '编号',
width: 128,
className: 'code',
render: renderCode,
},
{
key: 'status',
label: '状态',
render: renderStatus,
},
];
let rendered: RenderReactResult | undefined;
const actEnvironment = globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean;
};
const originalActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
beforeAll(() => {
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
});
afterAll(() => {
if (originalActEnvironment === undefined) {
Reflect.deleteProperty(actEnvironment, 'IS_REACT_ACT_ENVIRONMENT');
} else {
actEnvironment.IS_REACT_ACT_ENVIRONMENT = originalActEnvironment;
}
});
afterEach(async () => {
await rendered?.unmount();
rendered = undefined;
vi.clearAllMocks();
});
describe('DetailRecordTable', () => {
it('keeps width numeric and render required in the column type', () => {
type Column = DetailRecordColumn;
type HasRequiredRenderer = Column extends {
render: (row: FixtureRow) => React.ReactNode;
} ? true : false;
expectTypeOf().toEqualTypeOf();
expectTypeOf().toEqualTypeOf<
(row: FixtureRow) => React.ReactNode
>();
expectTypeOf().toEqualTypeOf();
});
it('renders a named vm table with scoped headers, column widths, and cell renderers', async () => {
const rows: FixtureRow[] = [
{ id: 'row-1', code: 'REC-001', status: '正常' },
{ id: 'row-2', code: 'REC-002', status: '' },
];
rendered = await renderReact(
,
);
const table = rendered.container.querySelector('table');
const card = rendered.container.querySelector('.va-record-table-card');
const tableWrap = card?.querySelector('.va-table-wrap');
const headers = Array.from(table?.querySelectorAll('th') ?? []);
const bodyRows = Array.from(table?.querySelectorAll('tbody tr') ?? []);
expect(card?.classList.contains('vm-table-card')).toBe(true);
expect(tableWrap?.classList.contains('vm-table-wrap')).toBe(true);
expect(tableWrap?.classList.contains('vm-table-wrap--wide')).toBe(true);
expect(table?.classList.contains('vm-table')).toBe(true);
expect(table?.classList.contains('va-record-table')).toBe(true);
expect(table?.querySelector('caption')?.textContent).toBe('测试记录');
expect(headers.map((header) => header.textContent?.trim())).toEqual(['编号', '状态']);
expect(headers.every((header) => header.scope === 'col')).toBe(true);
expect(headers[0].style.minWidth).toBe('128px');
expect(bodyRows[0].querySelector('[data-record-code]')?.textContent).toBe('编号:REC-001');
expect(bodyRows[0].querySelector('[data-record-status]')?.textContent).toBe('状态标签:正常');
expect(bodyRows[1].querySelector('[data-record-status]')?.textContent).toBe('状态标签:—');
expect(bodyRows[0].querySelector('td')?.classList.contains('code')).toBe(true);
expect(renderCode).toHaveBeenCalledTimes(2);
expect(renderCode).toHaveBeenNthCalledWith(1, rows[0]);
expect(renderCode).toHaveBeenNthCalledWith(2, rows[1]);
expect(renderStatus).toHaveBeenCalledTimes(2);
});
it('renders a clear status fallback instead of a malformed table with no columns', async () => {
rendered = await renderReact(
title="动态记录"
columns={[]}
rows={[{ id: 'row-1', code: 'REC-001', status: '正常' }]}
/>,
);
const status = rendered.container.querySelector('[role="status"]');
expect(rendered.container.querySelector('table')).toBeNull();
expect(status?.textContent?.trim()).toBe('暂无可显示字段');
expect(status?.closest('.va-record-table-card')).not.toBeNull();
});
it('renders one spanning empty cell with the default text', async () => {
rendered = await renderReact(
,
);
const cells = rendered.container.querySelectorAll('tbody td');
const emptyCell = cells.item(0);
expect(cells).toHaveLength(1);
expect(emptyCell.colSpan).toBe(columns.length);
expect(emptyCell.textContent).toBe('暂无数据');
});
});
describe('DetailRecordFooter', () => {
it('wraps the common pagination with its total and named navigation', async () => {
rendered = await renderReact(
,
);
const footer = rendered.container.querySelector('.vm-table-footer--compact');
const navigation = footer?.querySelector(
'[role="navigation"][aria-label="表格分页"]',
);
expect(footer?.classList.contains('vm-table-footer')).toBe(true);
expect(footer?.classList.contains('va-record-footer')).toBe(true);
expect(navigation).not.toBeNull();
expect(navigation?.textContent?.replace(/\s+/g, ' ').trim()).toContain('共 21 条');
expect(buttonByName(navigation as HTMLElement, '上一页').disabled).toBe(true);
expect(buttonByName(navigation as HTMLElement, '下一页').disabled).toBe(false);
});
});
describe('DetailRecordFilterBar', () => {
it('is a named region with a visible header, V2 actions, and expanded controlled content', async () => {
rendered = await renderReact(
,
);
const region = rendered.container.querySelector(
'section[role="region"][aria-label="记录筛选"]',
);
const content = region?.querySelector('.va-record-filter__content');
const toggle = buttonByName(region as HTMLElement, '收起筛选');
const reset = buttonByName(region as HTMLElement, '重置');
const query = buttonByName(region as HTMLElement, '查询');
const input = region?.querySelector('input[name="status"]');
expect(region?.querySelector('.va-record-filter__header h3')?.textContent).toBe('记录筛选');
expect(region?.classList.contains('is-expanded')).toBe(true);
expect(content?.hidden).toBe(false);
expect(content?.id).toBeTruthy();
expect(toggle.getAttribute('aria-controls')).toBe(content?.id);
expect(toggle.getAttribute('aria-expanded')).toBe('true');
expect(toggle.classList.contains('v2-btn--ghost')).toBe(true);
expect(toggle.classList.contains('v2-btn--lg')).toBe(true);
expect(input).not.toBeNull();
expect(input?.labels?.item(0)?.textContent?.trim()).toBe('状态');
expect(reset.type).toBe('button');
expect(reset.classList.contains('v2-btn--secondary')).toBe(true);
expect(reset.classList.contains('v2-btn--lg')).toBe(true);
expect(query.type).toBe('button');
expect(query.classList.contains('v2-btn--primary')).toBe(true);
expect(query.classList.contains('v2-btn--lg')).toBe(true);
});
it('calls query before hiding the content and can reopen it', async () => {
let region: HTMLElement | null = null;
const onQuery = vi.fn(() => {
expect(region?.classList.contains('is-expanded')).toBe(true);
});
rendered = await renderReact(
,
);
region = rendered.container.querySelector('.va-record-filter');
const content = region?.querySelector('.va-record-filter__content');
const query = buttonByName(region as HTMLElement, '查询');
query.focus();
expect(document.activeElement).toBe(query);
await click(query);
expect(onQuery).toHaveBeenCalledOnce();
expect(region?.classList.contains('is-expanded')).toBe(false);
expect(content?.hidden).toBe(true);
const reopen = buttonByName(region as HTMLElement, '展开筛选');
expect(reopen.getAttribute('aria-expanded')).toBe('false');
expect(document.activeElement).toBe(reopen);
expect(reopen.closest('[hidden]')).toBeNull();
await click(reopen);
expect(region?.classList.contains('is-expanded')).toBe(true);
expect(content?.hidden).toBe(false);
expect(buttonByName(region as HTMLElement, '收起筛选').getAttribute('aria-expanded')).toBe('true');
});
it('calls reset before hiding the content in a fresh render', async () => {
let region: HTMLElement | null = null;
const onReset = vi.fn(() => {
expect(region?.classList.contains('is-expanded')).toBe(true);
});
rendered = await renderReact(
,
);
region = rendered.container.querySelector('.va-record-filter');
const content = region?.querySelector('.va-record-filter__content');
const reset = buttonByName(region as HTMLElement, '重置');
reset.focus();
expect(document.activeElement).toBe(reset);
await click(reset);
expect(onReset).toHaveBeenCalledOnce();
expect(region?.classList.contains('is-expanded')).toBe(false);
expect(content?.hidden).toBe(true);
const reopen = buttonByName(region as HTMLElement, '展开筛选');
expect(reopen.getAttribute('aria-expanded')).toBe('false');
expect(document.activeElement).toBe(reopen);
expect(reopen.closest('[hidden]')).toBeNull();
});
});
function tableHeaders(root: ParentNode): string[] {
return Array.from(root.querySelectorAll('th'), (cell) => cell.textContent?.trim() ?? '');
}
describe('vehicle detail record contracts', () => {
it('renders the approved headers and filters for lease through annual review', async () => {
const onToast = vi.fn();
const cases = [
{
element: (
),
headers: ['合同编码', '项目名称', '客户名称', '业务类型', '交车日期', '交车人', '还车日期', '还车人'],
filters: ['合同编码', '项目名称', '客户名称', '业务类型'],
},
{
element: (
),
headers: ['事故编码', '事故时间', '事故地点', '事故类型', '客户名称', '我方定损金额', '对方定损金额', '责任划分', '事故状态', '结案时间'],
filters: ['事故时间', '客户名称', '事故等级', '事故状态'],
},
{
element: (
),
headers: ['违法时间', '违法地点', '违法行为', '扣分', '罚款金额', '缴费状态', '是否处理', '采集单位'],
filters: ['违法时间', '客户名称', '缴费状态', '是否处理'],
},
{
element: (
),
headers: ['异动开始日期', '异动预计结束日期', '异动状态', '异动目的地', '目的地名称', '异动类型', '预计异动里程 (km)', '异动开始里程 (km)', '异动开始电量 (kWh)', '异动开始氢量', '异动结束里程 (km)', '异动结束电量 (kWh)', '异动结束氢量', '创建人', '创建时间'],
filters: ['异动开始日期', '异动目的地', '异动类型'],
},
{
element: (
),
headers: ['调拨日期', '调出人', '接收人', '出发区域', '接收区域', '调拨方式', '出发停车场', '接收停车场', '接收日期'],
filters: ['调拨日期', '调出人', '接收人'],
},
{
element: (
),
headers: ['检验有效期至', '检测服务站名称', '检测费用', '二保服务站名称', '二保费用', '整备服务站名称', '整备费用', '办理人', '完成时间'],
filters: ['完成时间', '检验有效期', '办理人'],
},
];
for (const recordCase of cases) {
rendered = await renderReact(recordCase.element);
expect(tableHeaders(rendered.container)).toEqual(recordCase.headers);
for (const label of recordCase.filters) {
expect(rendered.container.textContent).toContain(label);
}
expect(buttonByName(rendered.container, '查询')).toBeTruthy();
expect(buttonByName(rendered.container, '重置')).toBeTruthy();
await rendered.unmount();
rendered = undefined;
}
});
it('renders the insurance overview, reference history fields, and file actions', async () => {
const onToast = vi.fn();
rendered = await renderReact(
,
);
expect(
Array.from(rendered.container.querySelectorAll('.va-insurance-type strong'), (node) => node.textContent),
).toEqual(['交强险', '商业险', '超赔险', '驾意险', '货物险']);
expect(rendered.container.textContent).toContain('保单号');
expect(rendered.container.textContent).toContain('保险公司');
expect(rendered.container.textContent).toContain('到期日期');
expect(rendered.container.textContent).toContain('保费');
expect(tableHeaders(rendered.container)).toEqual([
'保险导入时间',
'操作人',
'类型',
'保单号',
'保险状态',
'保险公司',
'付款时间',
'到期日期',
'保单文件',
]);
expect(rendered.container.textContent).toContain('—');
await click(buttonByName(rendered.container, '预览'));
await click(buttonByName(rendered.container, '下载'));
expect(onToast).toHaveBeenNthCalledWith(1, '预览保单文件(原型演示):交强险保单.pdf');
expect(onToast).toHaveBeenNthCalledWith(2, '已开始下载:交强险保单.pdf(原型演示)');
});
it('renders the complete fault contract and global view/edit actions', async () => {
const onView = vi.fn();
const onEdit = vi.fn();
const record = vehicles[0] as VehicleRecord;
const row = {
id: 'fault-1',
plateNo: record.plateNo,
faultNo: 'GZ-001',
resolutionStatus: '已解决',
brand: record.brand,
model: record.model,
operateCompany: record.operateCompany,
faultLevel: 'L2',
faultType: '冷机故障',
faultDescription: '制冷异常',
reportedAt: '2026-01-01 10:00',
resolvedAt: '2026-01-01 12:00',
aiMatched: true,
lastOperator: '张三',
};
rendered = await renderReact(
,
);
expect(tableHeaders(rendered.container)).toEqual([
'故障编号',
'解决情况',
'车牌号',
'车辆品牌',
'车辆型号',
'运营公司',
'故障等级',
'故障类型',
'故障描述',
'上报时间',
'解决时间',
'AI匹配状态',
'最后操作人',
'操作',
]);
for (const label of ['上报时间', '故障编号', '故障等级', '故障类型', '解决情况', 'AI匹配状态']) {
expect(rendered.container.textContent).toContain(label);
}
await click(buttonByName(rendered.container, '查看'));
await click(buttonByName(rendered.container, '编辑'));
expect(onView).toHaveBeenCalledWith(record, row);
expect(onEdit).toHaveBeenCalledWith(record, row);
});
});