diff --git a/frontend/src/pages/Reports.tsx b/frontend/src/pages/Reports.tsx index 047fffec..7464dbde 100644 --- a/frontend/src/pages/Reports.tsx +++ b/frontend/src/pages/Reports.tsx @@ -16,7 +16,6 @@ import { } from '@hugeicons/core-free-icons' import { getDashboardSummary, getReports, API_BASE } from '../api' import { formatDateLong, isWithinDateRange, type DateRange } from '../utils/date' -import { usePreferredExportFormat } from '../hooks/usePreferredExportFormat' type Report = { id: string @@ -73,7 +72,10 @@ export default function Reports() { const [selectedDateRange, setSelectedDateRange] = useState('all') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const { preferred, savePreference } = usePreferredExportFormat() + const [preferredFormat, setPreferredFormat] = useState(null) + const latestReadyReport = [...reports] + .filter((report) => report.status === 'ready') + .sort((a, b) => new Date(b.generated_at).getTime() - new Date(a.generated_at).getTime())[0] const fetchReports = () => { setLoading(true) @@ -93,6 +95,8 @@ export default function Reports() { useEffect(() => { fetchReports() + const pref = localStorage.getItem('secuscan:preferred-export-format') + if (pref) setPreferredFormat(pref) }, []) const filteredReports = reports.filter((report) => @@ -118,6 +122,18 @@ export default function Reports() {
+ - {[...exportFormats].sort((a, b) => - a === preferred ? -1 : b === preferred ? 1 : 0 - ).map((format) => ( - - ))} + }} + disabled={report.status === 'generating'} + className={`border-4 border-black px-3 py-2 text-[9px] font-black uppercase tracking-widest transition-all disabled:opacity-30 disabled:cursor-not-allowed disabled:group-hover:text-silver/20 disabled:group-hover:bg-charcoal-dark ${ + format === preferredFormat + ? 'bg-rag-amber border-black text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]' + : 'bg-charcoal-dark text-silver/20 group-hover:text-silver-bright group-hover:bg-black' + }`} + title={report.status === 'generating' ? 'Export unavailable while report is generating' : `Download ${format.toUpperCase()}`} + > + {format} + + )) + })()}
diff --git a/frontend/testing/unit/pages/Reports.test.tsx b/frontend/testing/unit/pages/Reports.test.tsx index 3da4ecd5..3b3ec230 100644 --- a/frontend/testing/unit/pages/Reports.test.tsx +++ b/frontend/testing/unit/pages/Reports.test.tsx @@ -22,6 +22,12 @@ const readyReport = { generated_at: '2026-05-14T10:00:00Z', status: 'ready', findings: 7, assets: 3, pages: 12, } +const newerReadyReport = { + id: 'report-4', task_id: 'task-jkl-012', + name: 'Security Scan — docs.example.com', type: 'technical', + generated_at: '2026-05-14T11:30:00Z', status: 'ready', + findings: 2, assets: 1, pages: 4, +} const generatingReport = { id: 'report-2', task_id: 'task-def-456', name: 'Security Scan — staging.example.com', type: 'executive', @@ -168,6 +174,39 @@ describe('Reports — export buttons on a ready report', () => { }) }) +describe('Reports — header export button', () => { + beforeEach(() => { + vi.mocked(getDashboardSummary).mockResolvedValue(emptySummary) + openSpy.mockClear() + }) + + it('opens the newest ready report PDF from the header button', async () => { + const user = userEvent.setup() + vi.mocked(getReports).mockResolvedValue({ reports: [readyReport, generatingReport, newerReadyReport, failedReport] }) + + renderReports() + + await user.click(await screen.findByRole('button', { name: /download latest ready report pdf/i })) + + expect(openSpy).toHaveBeenCalledWith( + expect.stringContaining('/task/' + newerReadyReport.task_id + '/report/pdf'), '_blank') + expect(openSpy).not.toHaveBeenCalledWith(expect.stringContaining('/task/latest/report/pdf'), expect.anything()) + }) + + it('disables the header export button when there is no ready report', async () => { + const user = userEvent.setup() + vi.mocked(getReports).mockResolvedValue({ reports: [generatingReport, failedReport] }) + + renderReports() + + const button = await screen.findByRole('button', { name: /download latest ready report pdf/i }) + expect(button).toBeDisabled() + + await user.click(button) + expect(openSpy).not.toHaveBeenCalled() + }) +}) + // ── Export buttons — generating report ──────────────────────────────────────────────────────────── describe('Reports — export buttons on a generating report', () => {