diff --git a/frontend/src/pages/Reports.tsx b/frontend/src/pages/Reports.tsx
index b690b787..bfe1de6d 100644
--- a/frontend/src/pages/Reports.tsx
+++ b/frontend/src/pages/Reports.tsx
@@ -488,7 +488,24 @@ export default function Reports() {
))}
- {filteredReports.length === 0 && (
+ {filteredReports.length === 0 && reports.length === 0 && (
+
diff --git a/frontend/testing/unit/pages/Reports.onboarding.test.tsx b/frontend/testing/unit/pages/Reports.onboarding.test.tsx
new file mode 100644
index 00000000..7bdd0e1a
--- /dev/null
+++ b/frontend/testing/unit/pages/Reports.onboarding.test.tsx
@@ -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(
+
+
+ ,
+ )
+}
+
+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()
+ })
+})
\ No newline at end of file
diff --git a/frontend/testing/unit/pages/Reports.test.tsx b/frontend/testing/unit/pages/Reports.test.tsx
index 3b3ec230..0ee808f2 100644
--- a/frontend/testing/unit/pages/Reports.test.tsx
+++ b/frontend/testing/unit/pages/Reports.test.tsx
@@ -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 () => {