feat(components): extract UnitTable from App

This commit is contained in:
2026-06-23 16:45:57 -04:00
parent 187aa4dfd6
commit 613a6dba80
3 changed files with 105 additions and 1 deletions

View File

@@ -0,0 +1,45 @@
import React from 'react'
import { Box, Container, Paper, Typography } from '@mui/material'
import { DataGrid } from '@mui/x-data-grid'
export function UnitTable({ rows, columns, filteredCount, onCellClick }) {
return (
<Container maxWidth="xl" sx={{ pb: 4, px: { xs: 1, sm: 2 } }}>
<Paper sx={{ borderRadius: 2, overflow: 'hidden' }}>
<Box sx={{ px: { xs: 1, sm: 2 }, py: 1, borderBottom: 1, borderColor: 'divider', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: { xs: '0.65rem', sm: '0.7rem' } }}>
<b style={{ color: 'text.primary' }}>{filteredCount.toLocaleString()}</b> units
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: { xs: '0.6rem', sm: '0.65rem' }, display: { xs: 'none', sm: 'block' } }}>
Click a unit for points history
</Typography>
</Box>
<DataGrid
rows={rows}
columns={columns}
getRowId={(row) => `${row.faction}|${row.name}`}
density="compact"
autoHeight
hideFooter
disableColumnMenu
onCellClick={(p) => {
if (p.field === 'size') return
onCellClick(p.row)
}}
sx={{
border: 'none',
width: '100%',
'& .MuiDataGrid-columnHeaders': { bgcolor: 'background.paper', borderBottom: 1, borderColor: 'divider' },
'& .MuiDataGrid-columnHeader': { fontSize: { xs: '0.6rem', sm: '0.75rem' }, textTransform: 'uppercase', fontWeight: 600 },
'& .MuiDataGrid-columnHeaderTitle': { fontSize: { xs: '0.6rem', sm: '0.75rem' } },
'& .MuiDataGrid-columnSeparator': { display: 'none' },
'& .MuiDataGrid-iconButtonContainer': { display: 'none' },
'& .MuiDataGrid-row': { cursor: 'pointer', '&:hover': { bgcolor: 'rgba(59,130,246,0.04)' } },
'& .MuiDataGrid-cell': { borderColor: 'divider', py: { xs: 0.5, sm: 0.5 }, display: 'flex', alignItems: 'center', overflow: 'hidden' },
'& .MuiDataGrid-virtualScroller': { overflowX: 'hidden' },
}}
/>
</Paper>
</Container>
)
}

View File

@@ -0,0 +1,59 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import fixture from './fixtures/data.json'
import { UnitTable } from '../components/UnitTable.jsx'
import { buildColumns } from '../utils/columns.jsx'
const columns = buildColumns({ isMobile: false, showFactionCol: true, onSelectSize: () => {} })
describe('UnitTable', () => {
it('renders the count header with the filtered count', () => {
render(<UnitTable rows={fixture.units} columns={columns} filteredCount={fixture.units.length} onCellClick={() => {}} />)
// "X units" is rendered (X being the filteredCount). The text is split across
// <b>{X}</b> and ' units' so a regex/getByText matcher can't find the combined
// text — instead find the <b>{X}</b> and check the parent for the "units" suffix.
const countEl = screen.getByText(`${fixture.units.length}`)
expect(countEl.parentElement).toHaveTextContent(new RegExp(`${fixture.units.length}\\s*units`))
})
it('renders all unit names in the grid', () => {
render(<UnitTable rows={fixture.units} columns={columns} filteredCount={fixture.units.length} onCellClick={() => {}} />)
for (const u of fixture.units) {
expect(screen.getByText(u.name)).toBeInTheDocument()
}
})
it('calls onCellClick with the clicked row when a non-size cell is clicked', async () => {
const user = userEvent.setup()
const onCellClick = vi.fn()
const palatine = fixture.units.find(u => u.name === 'Palatine')
render(<UnitTable rows={[palatine]} columns={columns} filteredCount={1} onCellClick={onCellClick} />)
// Click the Old cell for Palatine. Note: the brief said '60' but the fixture has
// Palatine.original = 50, so we use '50' (the actual Old value).
const oldCell = screen.getByText('50')
await user.click(oldCell)
expect(onCellClick).toHaveBeenCalledTimes(1)
expect(onCellClick).toHaveBeenCalledWith(expect.objectContaining({ name: 'Palatine' }))
})
it('does NOT call onCellClick when the size cell is clicked (size cell handles its own clicks)', async () => {
const user = userEvent.setup()
const onCellClick = vi.fn()
const arco = fixture.units.find(u => u.name === 'Arco-flagellants')
render(<UnitTable rows={[arco]} columns={columns} filteredCount={1} onCellClick={onCellClick} />)
// The size column shows a Select for multi-size units
const combobox = screen.getByRole('combobox')
await user.click(combobox)
// No onCellClick yet
expect(onCellClick).not.toHaveBeenCalled()
})
it('renders em-dash for new units (null original)', () => {
const newUnit = fixture.units.find(u => u.name === 'New Unit X')
render(<UnitTable rows={[newUnit]} columns={columns} filteredCount={1} onCellClick={() => {}} />)
// At least one em-dash should be in the document
const dashes = screen.getAllByText('—')
expect(dashes.length).toBeGreaterThan(0)
})
})

View File

@@ -22,7 +22,7 @@ export default defineConfig({
css: false,
server: {
deps: {
inline: ['@mui/material'],
inline: ['@mui/material', '@mui/x-data-grid'],
},
},
},