feat(components): extract MoversPanel + MoversSection from App
This commit is contained in:
31
react-app/src/components/MoversPanel.jsx
Normal file
31
react-app/src/components/MoversPanel.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { Box, Paper, Typography, Stack } from '@mui/material'
|
||||
import { pctLabel } from '../utils/format.js'
|
||||
|
||||
export function MoversPanel({ title, units, accent, onSelectUnit, showFaction }) {
|
||||
return (
|
||||
<Paper sx={{ flex: 1, p: { xs: 0.5, sm: 2 }, borderRadius: { xs: 1, sm: 2 }, borderLeft: 3, borderColor: `${accent}.main`, minWidth: 0 }}>
|
||||
<Typography variant="subtitle2" fontWeight={700} sx={{ mb: 0.25, fontSize: { xs: '0.65rem', sm: '0.8rem' }, color: `${accent}.main` }}>{title}</Typography>
|
||||
<Stack spacing={0}>
|
||||
{units.map((u, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
data-testid="mover-row"
|
||||
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer', py: 0.15, px: 0.25, borderRadius: 0.5, '&:hover': { bgcolor: accent === 'success' ? 'rgba(63,185,80,0.08)' : 'rgba(248,81,73,0.08)' } }}
|
||||
onClick={() => onSelectUnit(u)}
|
||||
>
|
||||
<Box sx={{ minWidth: 0, overflow: 'hidden', flex: 1 }}>
|
||||
<Typography variant="body2" fontWeight={600} sx={{ fontSize: { xs: '0.6rem', sm: '0.8rem' }, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', '&:hover': { color: 'primary.main' } }}>{u.name}</Typography>
|
||||
{showFaction && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: { xs: '0.55rem', sm: '0.7rem' }, display: { xs: 'none', sm: 'block' } }}>{u.faction_name}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ fontFamily: 'monospace', color: `${accent}.main`, fontWeight: 700, fontSize: { xs: '0.55rem', sm: '0.8rem' }, whiteSpace: 'nowrap', ml: 0.5 }}>
|
||||
{u.original}→{u.new} ({pctLabel(u.change_pct)})
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
26
react-app/src/components/MoversSection.jsx
Normal file
26
react-app/src/components/MoversSection.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { Box, Container } from '@mui/material'
|
||||
import { MoversPanel } from './MoversPanel.jsx'
|
||||
|
||||
export function MoversSection({ movers, onSelectUnit, showFaction }) {
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ py: { xs: 0.5, sm: 2 }, px: { xs: 1, sm: 2 } }}>
|
||||
<Box sx={{ display: 'flex', gap: { xs: 0.5, sm: 2 }, flexDirection: 'row' }}>
|
||||
<MoversPanel
|
||||
title="↓ Cheaper"
|
||||
units={movers.drops}
|
||||
accent="success"
|
||||
onSelectUnit={onSelectUnit}
|
||||
showFaction={showFaction}
|
||||
/>
|
||||
<MoversPanel
|
||||
title="↑ Costlier"
|
||||
units={movers.rises}
|
||||
accent="error"
|
||||
onSelectUnit={onSelectUnit}
|
||||
showFaction={showFaction}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
71
react-app/src/test/MoversSection.test.jsx
Normal file
71
react-app/src/test/MoversSection.test.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import fixture from './fixtures/data.json'
|
||||
import { MoversSection } from '../components/MoversSection.jsx'
|
||||
|
||||
const palatine = fixture.units.find(u => u.name === 'Palatine')
|
||||
const intercessor = fixture.units.find(u => u.name === 'Intercessor Squad')
|
||||
const dread = fixture.units.find(u => u.name === 'Redemptor Dreadnought')
|
||||
|
||||
describe('MoversSection', () => {
|
||||
it('renders both panel titles', () => {
|
||||
render(
|
||||
<MoversSection
|
||||
movers={{ drops: [palatine], rises: [dread] }}
|
||||
onSelectUnit={() => {}}
|
||||
showFaction={true}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('↓ Cheaper')).toBeInTheDocument()
|
||||
expect(screen.getByText('↑ Costlier')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('lists units in the drops panel under "Cheaper"', () => {
|
||||
render(
|
||||
<MoversSection
|
||||
movers={{ drops: [palatine, intercessor], rises: [dread] }}
|
||||
onSelectUnit={() => {}}
|
||||
showFaction={true}
|
||||
/>,
|
||||
)
|
||||
// Both drops should be present
|
||||
expect(screen.getByText('Palatine')).toBeInTheDocument()
|
||||
expect(screen.getByText('Intercessor Squad')).toBeInTheDocument()
|
||||
// The rises unit should also be in the document
|
||||
expect(screen.getByText('Redemptor Dreadnought')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides faction name in movers when showFaction is false', () => {
|
||||
render(
|
||||
<MoversSection
|
||||
movers={{ drops: [palatine], rises: [] }}
|
||||
onSelectUnit={() => {}}
|
||||
showFaction={false}
|
||||
/>,
|
||||
)
|
||||
// The unit name still shows
|
||||
expect(screen.getByText('Palatine')).toBeInTheDocument()
|
||||
// The faction name should NOT show in the mover row
|
||||
// Note: it might still appear elsewhere on the page in a real App, but in this
|
||||
// isolated render of MoversSection, the only place it would appear is inside
|
||||
// the panel rows. So we expect it not to be present.
|
||||
const rows = screen.queryAllByText(/Adepta Sororitas/)
|
||||
expect(rows.length).toBe(0)
|
||||
})
|
||||
|
||||
it('calls onSelectUnit with the clicked row', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
render(
|
||||
<MoversSection
|
||||
movers={{ drops: [palatine], rises: [] }}
|
||||
onSelectUnit={onSelect}
|
||||
showFaction={true}
|
||||
/>,
|
||||
)
|
||||
await user.click(screen.getByText('Palatine'))
|
||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||
expect(onSelect).toHaveBeenCalledWith(palatine)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user