// @vitest-environment jsdom import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import React from 'react'; import { afterEach, beforeAll, describe, expect, it } from 'vitest'; import { DetailModelParamsTab } from '../../src/prototypes/vehicle-management/components/DetailModelParamsTab'; import vehicles from '../../src/prototypes/vehicle-management/data/vehicles.json'; import type { VehicleRecord } from '../../src/prototypes/vehicle-management/types'; import { renderReact, type RenderReactResult } from './detailTestUtils'; let rendered: RenderReactResult | undefined; function findClosingBrace(source: string, openingBraceIndex: number): number { let depth = 0; for (let index = openingBraceIndex; index < source.length; index += 1) { if (source[index] === '{') { depth += 1; } else if (source[index] === '}') { depth -= 1; if (depth === 0) return index; } } return -1; } beforeAll(() => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; }); afterEach(async () => { await rendered?.unmount(); rendered = undefined; }); describe('DetailModelParamsTab', () => { it('renders the model parameters as semantic readonly descriptions', async () => { rendered = await renderReact( , ); const { container } = rendered; const tab = container.querySelector( 'section[aria-label="型号参数(只读)"]', ); const headings = Array.from(tab?.querySelectorAll('h3') ?? [], (heading) => ( heading.textContent?.trim() )); const grids = Array.from(tab?.querySelectorAll('.va-description-grid') ?? []); const items = Array.from(tab?.querySelectorAll('.va-description-item') ?? []); const labels = items.map((item) => item.querySelector('dt')?.textContent?.trim()); expect(tab).not.toBeNull(); expect(tab?.tagName).toBe('SECTION'); expect(headings).toEqual([ '基本情况', '轮胎情况', '电气情况', '供氢系统情况', '其他系统情况', '保养规则', ]); expect(grids.length).toBeGreaterThan(0); expect(grids.every((grid) => grid.tagName === 'DL')).toBe(true); expect(items.length).toBeGreaterThanOrEqual(10); expect(items.every((item) => ( item.tagName === 'DIV' && item.querySelector(':scope > dt') !== null && item.querySelector(':scope > dd') !== null ))).toBe(true); expect(labels).toEqual([ '品牌', '型号', '车辆类型', '公告型号', '燃料种类', '车牌颜色', '车辆长度(米)', '车辆宽度(米)', '车辆高度(米)', '轮胎数量', '轮胎磨损费用(元/mm)', '电池类型', '电池厂家', '储电量', '电续航里程(KM)', '氢瓶容量', '仪表盘显示模式', '氢续航里程(KM)', '冷机厂家', ]); expect(items[0].querySelector('dd')?.textContent).toBe('现代'); expect(tab?.querySelector('.va-model-param-item__value')).toBeNull(); expect(tab?.querySelector('input, select, textarea')).toBeNull(); expect(tab?.querySelector('.va-model-param-table')?.textContent).toContain('保养公里周期'); expect(tab?.querySelector('.va-model-param-table')?.textContent).toContain('变速器油'); }); it('associates the maintenance table with its heading and identifies column headers', async () => { rendered = await renderReact( , ); const heading = Array.from(rendered.container.querySelectorAll('h3')) .find((candidate) => candidate.textContent?.trim() === '保养规则'); const table = rendered.container.querySelector('.va-model-param-table'); const columnHeaders = Array.from(table?.querySelectorAll('th') ?? []); expect(heading?.id).toBeTruthy(); expect(table?.getAttribute('aria-labelledby')).toBe(heading?.id); expect(columnHeaders.length).toBeGreaterThan(0); expect(columnHeaders.every((header) => header.getAttribute('scope') === 'col')).toBe(true); }); it('renders empty parameter values as an em dash', async () => { rendered = await renderReact( , ); const brandItem = Array.from( rendered.container.querySelectorAll('.va-description-item'), ).find((item) => item.querySelector('dt')?.textContent?.trim() === '品牌'); expect(brandItem?.querySelector('dd')?.textContent).toBe('—'); }); it('uses the page mobile breakpoint for the single-column description grid', () => { const descriptionGridSelector = '.va-model-param-tab .va-description-grid'; const styles = readFileSync( resolve(process.cwd(), 'src/prototypes/vehicle-management/style.css'), 'utf8', ); const selectorIndexes: number[] = []; let selectorIndex = styles.indexOf(descriptionGridSelector); while (selectorIndex >= 0) { selectorIndexes.push(selectorIndex); selectorIndex = styles.indexOf(descriptionGridSelector, selectorIndex + 1); } expect(selectorIndexes).toHaveLength(2); const mobileSelectorIndexes = selectorIndexes.filter((index) => { const ruleOpeningBrace = styles.indexOf('{', index); const ruleClosingBrace = findClosingBrace(styles, ruleOpeningBrace); const rule = styles.slice(index, ruleClosingBrace + 1); return rule.includes('grid-template-columns: minmax(0, 1fr)'); }); expect(mobileSelectorIndexes).toHaveLength(1); const mobileSelectorIndex = mobileSelectorIndexes[0]; const nearestMediaIndex = styles.lastIndexOf('@media', mobileSelectorIndex); const nearestMediaBlockStart = styles.indexOf('{', nearestMediaIndex); const nearestMediaBlockEnd = findClosingBrace(styles, nearestMediaBlockStart); const nearestMediaHeader = styles .slice(nearestMediaIndex, nearestMediaBlockStart) .trim(); const mobileRuleStart = styles.indexOf('{', mobileSelectorIndex); const mobileRuleEnd = findClosingBrace(styles, mobileRuleStart); const mobileRule = styles.slice(mobileSelectorIndex, mobileRuleEnd + 1); expect(nearestMediaHeader).toBe('@media (max-width: 767px)'); expect(mobileSelectorIndex).toBeGreaterThan(nearestMediaBlockStart); expect(mobileSelectorIndex).toBeLessThan(nearestMediaBlockEnd); expect(mobileRuleEnd).toBeLessThan(nearestMediaBlockEnd); expect(mobileRule).toContain('grid-template-columns: minmax(0, 1fr)'); }); });