feat(components): extract MoversPanel + MoversSection from App

This commit is contained in:
2026-06-23 16:06:57 -04:00
parent b87f5ea5aa
commit bd4ca78c1d
3 changed files with 128 additions and 0 deletions

View 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>
)
}

View 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>
)
}

View 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)
})
})