From 78bcc111784663af8696f00189ce8646f681fe9a Mon Sep 17 00:00:00 2001 From: Naitik Date: Sun, 5 Jul 2026 10:43:47 +0530 Subject: [PATCH 1/2] fix(scans): replace setError banner with toast feedback for delete and rescan failures --- frontend/src/pages/Scans.tsx | 9 +- .../testing/unit/pages/Scans.polling.test.tsx | 7 +- frontend/testing/unit/pages/Scans.test.tsx | 14 +- .../testing/unit/pages/ScansPhases.test.tsx | 7 +- .../unit/pages/ScansToastErrors.test.tsx | 129 ++++++++++++++++++ 5 files changed, 151 insertions(+), 15 deletions(-) create mode 100644 frontend/testing/unit/pages/ScansToastErrors.test.tsx diff --git a/frontend/src/pages/Scans.tsx b/frontend/src/pages/Scans.tsx index 1f70ff153..de3cd26b3 100644 --- a/frontend/src/pages/Scans.tsx +++ b/frontend/src/pages/Scans.tsx @@ -9,6 +9,7 @@ import { formatLocaleTime, } from "../utils/date"; import { ConfirmModal } from "../components/ConfirmModal"; +import { useToast } from "../components/ToastContext"; import Pagination from "../components/Pagination"; interface Task { @@ -58,6 +59,7 @@ const itemVariants: Variants = { export default function Scans() { const navigate = useNavigate(); + const { addToast } = useToast(); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState("all"); @@ -193,6 +195,7 @@ export default function Scans() { } } catch (err) { console.error("Rescan failed:", err); + addToast("Rescan failed. Please try again.", "error"); } } @@ -211,7 +214,7 @@ export default function Scans() { setModalState(prev => ({ ...prev, isOpen: false })); } catch (err) { console.error("Failed to delete task:", err); - setError("Failed to delete task. It might still be running."); + addToast("Failed to delete task. It might still be running.", "error"); setModalState(prev => ({ ...prev, isOpen: false })); } }, @@ -234,7 +237,7 @@ export default function Scans() { setModalState(prev => ({ ...prev, isOpen: false })); } catch (err) { console.error("Failed to clear history:", err); - setError("Failed to clear history. Ensure no tasks are currently running."); + addToast("Failed to clear history. Ensure no tasks are currently running.", "error"); setModalState(prev => ({ ...prev, isOpen: false })); } }, @@ -257,7 +260,7 @@ export default function Scans() { setModalState(prev => ({ ...prev, isOpen: false })); } catch (err) { console.error("Bulk delete failed:", err); - setError("Failed to delete some tasks. Ensure they are not currently running."); + addToast("Failed to delete some tasks. Ensure they are not currently running.", "error"); setModalState(prev => ({ ...prev, isOpen: false })); } }, diff --git a/frontend/testing/unit/pages/Scans.polling.test.tsx b/frontend/testing/unit/pages/Scans.polling.test.tsx index f8914c83d..84480880c 100644 --- a/frontend/testing/unit/pages/Scans.polling.test.tsx +++ b/frontend/testing/unit/pages/Scans.polling.test.tsx @@ -2,6 +2,7 @@ import { render, act, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import Scans from '../../../src/pages/Scans'; +import { ToastProvider } from '../../../src/components/ToastContext' // ── Mocks ──────────────────────────────────────────────────────────────────── @@ -68,8 +69,10 @@ afterEach(() => { function renderScans() { return render( - - , + + + + ); } diff --git a/frontend/testing/unit/pages/Scans.test.tsx b/frontend/testing/unit/pages/Scans.test.tsx index da4baeab3..c863d17ee 100644 --- a/frontend/testing/unit/pages/Scans.test.tsx +++ b/frontend/testing/unit/pages/Scans.test.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { MemoryRouter } from 'react-router-dom' import Scans from '../../../src/pages/Scans' +import { ToastProvider } from '../../../src/components/ToastContext' // ── Mocks ──────────────────────────────────────────────────────────────────── @@ -83,7 +84,9 @@ function mockFetch(tasks: ReturnType[], total?: number) { function renderScans() { return render( - + + + ) } @@ -273,12 +276,7 @@ describe('Scans — task list', () => { expect(screen.getByText('nmap')).toBeInTheDocument() expect(screen.getByText('1')).toBeInTheDocument() - // Test that the error banner can be closed - const closeBtn = screen.getByRole('button', { name: /Close alert/i }) - await userEvent.click(closeBtn) - - await waitFor(() => { - expect(screen.queryByRole('alert')).not.toBeInTheDocument() - }) + // Error is shown via toast, not the banner — no Close alert button expected + expect(screen.queryByRole('button', { name: /Close alert/i })).not.toBeInTheDocument() }) }) diff --git a/frontend/testing/unit/pages/ScansPhases.test.tsx b/frontend/testing/unit/pages/ScansPhases.test.tsx index c14bead59..1c1046ec5 100644 --- a/frontend/testing/unit/pages/ScansPhases.test.tsx +++ b/frontend/testing/unit/pages/ScansPhases.test.tsx @@ -2,6 +2,7 @@ import { render, act, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import Scans from '../../../src/pages/Scans'; +import { ToastProvider } from '../../../src/components/ToastContext' vi.mock('../../../src/api', () => ({ API_BASE: 'http://localhost', @@ -64,8 +65,10 @@ async function tickTime(ms: number) { function renderScans() { return render( - - , + + + + ); } diff --git a/frontend/testing/unit/pages/ScansToastErrors.test.tsx b/frontend/testing/unit/pages/ScansToastErrors.test.tsx new file mode 100644 index 000000000..0791fee69 --- /dev/null +++ b/frontend/testing/unit/pages/ScansToastErrors.test.tsx @@ -0,0 +1,129 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter } from 'react-router-dom' +import { ToastProvider } from '../../../src/components/ToastContext' +import Scans from '../../../src/pages/Scans' +import { deleteTask, clearAllTasks, bulkDeleteTasks, startTask } from '../../../src/api' + +vi.mock('../../../src/api', async () => { + const actual: any = await vi.importActual('../../../src/api') + return { + ...actual, + deleteTask: vi.fn(), + clearAllTasks: vi.fn(), + bulkDeleteTasks: vi.fn(), + startTask: vi.fn(), + } +}) + +function makeTask(overrides = {}) { + return { + task_id: 'task-1', + plugin_id: 'nmap', + tool: 'nmap', + target: 'example.com', + status: 'completed', + created_at: '2026-05-14T10:00:00Z', + ...overrides, + } +} + +function mockFetch(tasks: any[]) { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ tasks, pagination: { total_items: tasks.length } }), + }), + ) +} + +function renderScans() { + return render( + + + + + , + ) +} + +beforeEach(() => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + mockFetch([makeTask()]) +}) + +afterEach(() => { + vi.useRealTimers() + vi.restoreAllMocks() +}) + +describe('Scans — toast error feedback', () => { + it('shows a toast when clear all tasks fails', async () => { + vi.mocked(clearAllTasks).mockRejectedValueOnce(new Error('clear failed')) + const user = userEvent.setup() + renderScans() + + await waitFor(() => expect(screen.getByText('nmap')).toBeInTheDocument()) + + await user.click(screen.getByRole('button', { name: /Purge_All_Records/i })) + const confirmBtn = await screen.findByRole('button', { name: /Confirm/i }) + await user.click(confirmBtn) + + await waitFor(() => { + expect(screen.getByText(/Failed to clear history/i)).toBeInTheDocument() + }) + }) + + it('shows a toast when bulk delete fails', async () => { + vi.mocked(bulkDeleteTasks).mockRejectedValueOnce(new Error('bulk failed')) + const user = userEvent.setup() + renderScans() + + await waitFor(() => expect(screen.getByText('nmap')).toBeInTheDocument()) + + await user.click(screen.getByRole('button', { name: /Select_All/i })) + await waitFor(() => expect(screen.getByText('1')).toBeInTheDocument()) + + await user.click(screen.getByRole('button', { name: /Prune_Selected_Records/i })) + const confirmBtn = await screen.findByRole('button', { name: /Confirm/i }) + await user.click(confirmBtn) + + await waitFor(() => { + expect(screen.getByText(/Failed to delete some tasks/i)).toBeInTheDocument() + }) + }) + + it('shows a toast when rescan fails', async () => { + vi.mocked(startTask).mockRejectedValueOnce(new Error('rescan failed')) + const user = userEvent.setup() + renderScans() + + await waitFor(() => expect(screen.getByText('nmap')).toBeInTheDocument()) + + const rescanBtn = screen.getByRole('button', { name: /Re-run nmap scan/i }) + await user.click(rescanBtn) + + await waitFor(() => { + expect(screen.getByText(/Rescan failed/i)).toBeInTheDocument() + }) + }) + + it('does not show the old error banner after a failure', async () => { + vi.mocked(clearAllTasks).mockRejectedValueOnce(new Error('clear failed')) + const user = userEvent.setup() + renderScans() + + await waitFor(() => expect(screen.getByText('nmap')).toBeInTheDocument()) + + await user.click(screen.getByRole('button', { name: /Purge_All_Records/i })) + const confirmBtn = await screen.findByRole('button', { name: /Confirm/i }) + await user.click(confirmBtn) + + await waitFor(() => { + expect(screen.getByText(/Failed to clear history/i)).toBeInTheDocument() + }) + + expect(screen.queryByRole('button', { name: /Close alert/i })).not.toBeInTheDocument() + }) +}) \ No newline at end of file From 4249d53e8e50f1c7760520e4c7562513f6b47130 Mon Sep 17 00:00:00 2001 From: Naitik Date: Tue, 7 Jul 2026 13:14:14 +0530 Subject: [PATCH 2/2] fix(scans): remove dead error banner state and setError no-ops, fix trailing newline --- frontend/src/pages/Scans.tsx | 35 ++++--------------- .../unit/pages/ScansToastErrors.test.tsx | 2 +- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/frontend/src/pages/Scans.tsx b/frontend/src/pages/Scans.tsx index de3cd26b3..f8850f2d7 100644 --- a/frontend/src/pages/Scans.tsx +++ b/frontend/src/pages/Scans.tsx @@ -68,7 +68,6 @@ export default function Scans() { const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const PAGE_LIMIT = 10; - const [error, setError] = useState(null); // Modal state for confirm dialogs const [modalState, setModalState] = useState<{ @@ -176,7 +175,7 @@ export default function Scans() { } function handleFilterChange(value: string) { - setError(null); + setFilter(value); setPage(1); } @@ -207,7 +206,7 @@ export default function Scans() { type: "danger", onConfirm: async () => { try { - setError(null); + await deleteTask(taskId); setTasks((prev) => prev.filter((t) => t.task_id !== taskId)); if (expandedId === taskId) setExpandedId(null); @@ -229,7 +228,7 @@ export default function Scans() { type: "danger", onConfirm: async () => { try { - setError(null); + await clearAllTasks(); setTasks([]); setSelectedIds([]); @@ -253,7 +252,7 @@ export default function Scans() { type: "danger", onConfirm: async () => { try { - setError(null); + await bulkDeleteTasks(selectedIds); setTasks((prev) => prev.filter((t) => !selectedIds.includes(t.task_id))); setSelectedIds([]); @@ -329,27 +328,7 @@ export default function Scans() { - {error && ( -
-
- error -
-

System_Alert

-

{error}

-
-
- -
- )} + {/* Filtration Block */}
@@ -740,11 +719,11 @@ export default function Scans() { limit={PAGE_LIMIT} loading={loading} onPrev={() => { - setError(null); + setPage((p) => p - 1); }} onNext={() => { - setError(null); + setPage((p) => p + 1); }} /> diff --git a/frontend/testing/unit/pages/ScansToastErrors.test.tsx b/frontend/testing/unit/pages/ScansToastErrors.test.tsx index 0791fee69..5a95e8c75 100644 --- a/frontend/testing/unit/pages/ScansToastErrors.test.tsx +++ b/frontend/testing/unit/pages/ScansToastErrors.test.tsx @@ -126,4 +126,4 @@ describe('Scans — toast error feedback', () => { expect(screen.queryByRole('button', { name: /Close alert/i })).not.toBeInTheDocument() }) -}) \ No newline at end of file +})