Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion frontend/src/pages/Reports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,24 @@ export default function Reports() {
</motion.div>
))}

{filteredReports.length === 0 && (
{filteredReports.length === 0 && reports.length === 0 && (
<div className="col-span-2 py-40 border-4 border-dashed border-rag-blue/10 text-center flex flex-col items-center gap-8 bg-charcoal/30">
<ReportIcon icon={Archive02Icon} size={120} className="text-rag-blue/15" aria-hidden="true" />
<div className="space-y-3 max-w-md">
<p className="text-xl font-black text-silver-bright uppercase tracking-[0.3em] italic">No Briefings Yet</p>
<p className="text-xs font-mono text-silver/30 uppercase tracking-widest leading-relaxed">
Run a scan from the Toolkit to generate your first report. Completed scans appear here automatically.
</p>
</div>
<Link
to={routes.toolkit}
className="bg-rag-blue border-4 border-black px-8 py-4 text-[10px] font-black uppercase tracking-[0.3em] text-black shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 transition-all"
>
Launch_First_Scan
</Link>
</div>
)}
{filteredReports.length === 0 && reports.length > 0 && (
<div className="col-span-2 py-40 border-4 border-dashed border-black/5 text-center flex flex-col items-center gap-8 bg-charcoal/30">
<ReportIcon icon={Archive02Icon} size={120} className="text-silver/5" aria-hidden="true" />
<div className="space-y-2">
Expand Down
83 changes: 83 additions & 0 deletions frontend/testing/unit/pages/Reports.onboarding.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter } from 'react-router-dom'
import Reports from '../../../src/pages/Reports'
import { getReports, getDashboardSummary } from '../../../src/api'

vi.mock('../../../src/api', () => ({
getReports: vi.fn(),
getDashboardSummary: vi.fn(),
API_BASE: 'http://127.0.0.1:8000',
}))

const readyReport = {
id: 'report-1',
task_id: 'task-abc-123',
name: 'Security Scan — example.com',
type: 'technical',
generated_at: '2026-05-14T10:00:00Z',
status: 'ready',
findings: 7,
assets: 3,
pages: 12,
}

const emptySummary = {
total_findings: 0,
total_assets: 0,
critical_findings: 0,
high_findings: 0,
total_attack_surface: 0,
}

function renderReports() {
return render(
<MemoryRouter>
<Reports />
</MemoryRouter>,
)
}

beforeEach(() => {
vi.mocked(getDashboardSummary).mockResolvedValue(emptySummary)
})

describe('Reports — onboarding empty state', () => {
it('shows the onboarding message when there are zero reports', async () => {
vi.mocked(getReports).mockResolvedValue({ reports: [] })
renderReports()

expect(await screen.findByText(/No Briefings Yet/i)).toBeInTheDocument()
expect(screen.getByText(/Run a scan from the Toolkit/i)).toBeInTheDocument()
})

it('shows a call-to-action link pointing to the toolkit route', async () => {
vi.mocked(getReports).mockResolvedValue({ reports: [] })
renderReports()

const cta = await screen.findByRole('link', { name: /launch_first_scan/i })
expect(cta).toHaveAttribute('href', '/toolkit')
})

it('does not show the onboarding message when reports exist but filters hide them all', async () => {
const user = userEvent.setup()
vi.mocked(getReports).mockResolvedValue({ reports: [readyReport] })
renderReports()

await screen.findByText(/Security Scan — example.com/i)
await user.click(screen.getByRole('button', { name: /executive briefings/i }))

expect(screen.queryByText(/No Briefings Yet/i)).not.toBeInTheDocument()
expect(await screen.findByText(/Archive Isolated/i)).toBeInTheDocument()
})

it('does not show the onboarding empty state once reports are loaded', async () => {
vi.mocked(getReports).mockResolvedValue({ reports: [readyReport] })
renderReports()

await screen.findByText(/Security Scan — example.com/i)

expect(screen.queryByText(/No Briefings Yet/i)).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /launch_first_scan/i })).not.toBeInTheDocument()
})
})
5 changes: 3 additions & 2 deletions frontend/testing/unit/pages/Reports.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ describe('Reports — empty state', () => {
vi.mocked(getDashboardSummary).mockResolvedValue(emptySummary)
})

it('shows Archive Isolated when there are no reports at all', async () => {
it('shows onboarding empty state when there are no reports at all', async () => {
renderReports()
expect(await screen.findByText(/Archive Isolated/i)).toBeInTheDocument()
expect(await screen.findByText(/No Briefings Yet/i)).toBeInTheDocument()
expect(screen.getByRole('link', { name: /launch_first_scan/i })).toBeInTheDocument()
})

it('shows Archive Isolated when filter returns no matching reports', async () => {
Expand Down
Loading