// @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); }); });