diff --git a/app/frontend/src/app/[locale]/campaigns/__tests__/page.test.tsx b/app/frontend/src/app/[locale]/campaigns/__tests__/page.test.tsx
new file mode 100644
index 00000000..1f0718c7
--- /dev/null
+++ b/app/frontend/src/app/[locale]/campaigns/__tests__/page.test.tsx
@@ -0,0 +1,225 @@
+/** @jest-environment jsdom */
+import '@testing-library/jest-dom';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import CampaignsPage from '../page';
+import { useCampaigns, useCreateCampaign } from '@/hooks/useCampaigns';
+import { useCampaignAction } from '@/hooks/useOptimisticCampaignMutations';
+import {
+ canManageCampaigns,
+ getUserRole,
+ getUserRoleLabel,
+} from '@/lib/user-role';
+import type { Campaign } from '@/types/campaign';
+
+const replace = jest.fn();
+let searchParams = new URLSearchParams();
+
+jest.mock('next/navigation', () => ({
+ useRouter: () => ({ replace }),
+ useSearchParams: () => searchParams,
+}));
+
+jest.mock('next/link', () => ({
+ __esModule: true,
+ default: ({
+ href,
+ children,
+ className,
+ }: {
+ href: string;
+ children: React.ReactNode;
+ className?: string;
+ }) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock('@/components/dashboard/ExportControls', () => ({
+ ExportControls: ({ context }: { context: string }) => (
+
+ ),
+}));
+
+jest.mock('@/hooks/useCampaigns', () => ({
+ useCampaigns: jest.fn(),
+ useCreateCampaign: jest.fn(),
+}));
+
+jest.mock('@/hooks/useOptimisticCampaignMutations', () => ({
+ useCampaignAction: jest.fn(),
+ useCampaignActions: jest.fn(),
+}));
+
+jest.mock('@/lib/user-role', () => ({
+ canManageCampaigns: jest.fn(),
+ getUserRole: jest.fn(),
+ getUserRoleLabel: jest.fn(),
+}));
+
+const mockUseCampaigns = useCampaigns as jest.MockedFunction<
+ typeof useCampaigns
+>;
+const mockUseCreateCampaign = useCreateCampaign as jest.MockedFunction<
+ typeof useCreateCampaign
+>;
+const mockUseCampaignAction = useCampaignAction as jest.MockedFunction<
+ typeof useCampaignAction
+>;
+const mockCanManageCampaigns = canManageCampaigns as jest.MockedFunction<
+ typeof canManageCampaigns
+>;
+const mockGetUserRole = getUserRole as jest.MockedFunction;
+const mockGetUserRoleLabel = getUserRoleLabel as jest.MockedFunction<
+ typeof getUserRoleLabel
+>;
+
+const activeCampaign: Campaign = {
+ id: 'campaign-1',
+ name: 'Flood Relief',
+ budget: 15000,
+ status: 'active',
+ metadata: { token: 'USDC', expiry: '2026-12-31T00:00:00.000Z' },
+};
+
+const pausedCampaign: Campaign = {
+ ...activeCampaign,
+ id: 'campaign-2',
+ name: 'Shelter Supplies',
+ status: 'paused',
+};
+
+const mutate = jest.fn();
+const mutateAsync = jest.fn();
+
+function mockPageState({
+ campaigns = [activeCampaign],
+ isLoading = false,
+ isError = false,
+ error = null,
+}: {
+ campaigns?: Campaign[];
+ isLoading?: boolean;
+ isError?: boolean;
+ error?: Error | null;
+} = {}) {
+ mockUseCampaigns.mockReturnValue({
+ data: campaigns,
+ isLoading,
+ isError,
+ error,
+ } as ReturnType);
+ mockUseCreateCampaign.mockReturnValue({
+ mutateAsync,
+ isPending: false,
+ } as ReturnType);
+ mockUseCampaignAction.mockReturnValue({
+ mutate,
+ isPending: false,
+ variables: undefined,
+ } as ReturnType);
+}
+
+describe('CampaignsPage', () => {
+ beforeEach(() => {
+ searchParams = new URLSearchParams();
+ replace.mockClear();
+ mutate.mockClear();
+ mutateAsync.mockResolvedValue(activeCampaign);
+ mutateAsync.mockClear();
+ mockCanManageCampaigns.mockReturnValue(true);
+ mockGetUserRole.mockReturnValue('admin');
+ mockGetUserRoleLabel.mockReturnValue('Admin');
+ mockPageState();
+ });
+
+ it('renders loading, error, and empty campaign states', () => {
+ mockPageState({ campaigns: [], isLoading: true });
+ const { rerender } = render();
+ expect(screen.getByText('Loading campaigns...')).toBeInTheDocument();
+
+ mockPageState({
+ campaigns: [],
+ isError: true,
+ error: new Error('Campaign API failed'),
+ });
+ rerender();
+ expect(
+ screen.getByText('Error fetching campaigns: Campaign API failed'),
+ ).toBeInTheDocument();
+
+ mockPageState({ campaigns: [] });
+ rerender();
+ expect(
+ screen.getByText('There are no active campaigns to review'),
+ ).toBeInTheDocument();
+ });
+
+ it('creates a campaign from the form payload', async () => {
+ render();
+
+ fireEvent.change(screen.getByLabelText('Name'), {
+ target: { value: 'Medical Supplies' },
+ });
+ fireEvent.change(screen.getByLabelText('Budget (USD)'), {
+ target: { value: '4200' },
+ });
+ fireEvent.change(screen.getByLabelText('Token'), {
+ target: { value: 'EURC' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Create campaign' }));
+
+ await waitFor(() => {
+ expect(mutateAsync).toHaveBeenCalledWith({
+ name: 'Medical Supplies',
+ budget: 4200,
+ status: 'active',
+ metadata: {
+ token: 'EURC',
+ expiry: undefined,
+ },
+ });
+ });
+ expect(
+ await screen.findByText('Campaign created successfully.'),
+ ).toBeInTheDocument();
+ });
+
+ it('filters campaigns by URL status and updates the URL when a preset is applied', () => {
+ searchParams = new URLSearchParams('status=paused');
+ mockPageState({ campaigns: [activeCampaign, pausedCampaign] });
+
+ render();
+
+ expect(screen.queryByText('Flood Relief')).not.toBeInTheDocument();
+ expect(screen.getByText('Shelter Supplies')).toBeInTheDocument();
+ });
+
+ it('dispatches pause, resume, and archive campaign actions', () => {
+ mockPageState({ campaigns: [activeCampaign, pausedCampaign] });
+
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Pause' }));
+ expect(mutate).toHaveBeenCalledWith({
+ id: 'campaign-1',
+ campaignName: 'Flood Relief',
+ action: { type: 'pause', targetStatus: 'paused' },
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Resume' }));
+ expect(mutate).toHaveBeenCalledWith({
+ id: 'campaign-2',
+ campaignName: 'Shelter Supplies',
+ action: { type: 'resume', targetStatus: 'active' },
+ });
+
+ fireEvent.click(screen.getAllByRole('button', { name: 'Archive' })[0]);
+ expect(mutate).toHaveBeenCalledWith({
+ id: 'campaign-1',
+ campaignName: 'Flood Relief',
+ action: { type: 'archive', targetStatus: 'archived' },
+ });
+ });
+});
diff --git a/app/frontend/src/components/__tests__/WalletConnect.test.tsx b/app/frontend/src/components/__tests__/WalletConnect.test.tsx
new file mode 100644
index 00000000..21b57589
--- /dev/null
+++ b/app/frontend/src/components/__tests__/WalletConnect.test.tsx
@@ -0,0 +1,167 @@
+/** @jest-environment jsdom */
+import '@testing-library/jest-dom';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import {
+ getAddress,
+ getNetworkDetails,
+ isConnected,
+ setAllowed,
+} from '@stellar/freighter-api';
+import { WalletConnect } from '../WalletConnect';
+import { useWalletStore } from '@/lib/walletStore';
+import { useToast } from '../ToastProvider';
+import { useNetworkGuard } from '@/hooks/useNetworkGuard';
+
+jest.mock('@stellar/freighter-api', () => ({
+ isConnected: jest.fn(),
+ setAllowed: jest.fn(),
+ getAddress: jest.fn(),
+ getNetworkDetails: jest.fn(),
+}));
+
+jest.mock('@/lib/walletStore', () => ({
+ useWalletStore: jest.fn(),
+}));
+
+jest.mock('../ToastProvider', () => ({
+ useToast: jest.fn(),
+}));
+
+jest.mock('@/hooks/useNetworkGuard', () => ({
+ useNetworkGuard: jest.fn(),
+}));
+
+jest.mock('@/lib/explorer', () => ({
+ buildExplorerUrl: (_type: string, identifier: string) =>
+ `https://stellar.expert/explorer/testnet/address/${identifier}`,
+}));
+
+const mockIsConnected = isConnected as jest.MockedFunction;
+const mockSetAllowed = setAllowed as jest.MockedFunction;
+const mockGetAddress = getAddress as jest.MockedFunction;
+const mockGetNetworkDetails = getNetworkDetails as jest.MockedFunction<
+ typeof getNetworkDetails
+>;
+const mockUseWalletStore = useWalletStore as unknown as jest.MockedFunction<
+ () => {
+ publicKey: string | null;
+ network: string | null;
+ setPublicKey: jest.Mock;
+ setNetwork: jest.Mock;
+ disconnect: jest.Mock;
+ }
+>;
+const mockUseToast = useToast as jest.MockedFunction;
+const mockUseNetworkGuard = useNetworkGuard as jest.MockedFunction<
+ typeof useNetworkGuard
+>;
+
+const toast = jest.fn();
+const setPublicKey = jest.fn();
+const setNetwork = jest.fn();
+const disconnect = jest.fn();
+let walletState = {
+ publicKey: null as string | null,
+ network: null as string | null,
+};
+
+function mockWallet() {
+ mockUseWalletStore.mockReturnValue({
+ ...walletState,
+ setPublicKey,
+ setNetwork,
+ disconnect,
+ });
+ mockUseToast.mockReturnValue({ toast } as ReturnType);
+ mockUseNetworkGuard.mockReturnValue({
+ isCorrectNetwork: true,
+ isMismatch: false,
+ walletNetwork: walletState.network,
+ expectedNetwork: 'TESTNET',
+ });
+}
+
+describe('WalletConnect', () => {
+ beforeEach(() => {
+ Object.defineProperty(window, 'FreighterApi', {
+ configurable: true,
+ value: {},
+ });
+ walletState = { publicKey: null, network: null };
+ toast.mockClear();
+ setPublicKey.mockClear();
+ setNetwork.mockClear();
+ disconnect.mockClear();
+ mockIsConnected.mockResolvedValue({ isConnected: false });
+ mockSetAllowed.mockResolvedValue(undefined);
+ mockGetAddress.mockResolvedValue({
+ address: 'GABC1234567890XYZ',
+ });
+ mockGetNetworkDetails.mockResolvedValue({ network: 'TESTNET' });
+ mockWallet();
+ });
+
+ it('connects Freighter and stores the returned address and network', async () => {
+ render();
+
+ fireEvent.click(
+ await screen.findByRole('button', { name: 'Connect Freighter Wallet' }),
+ );
+
+ await waitFor(() => {
+ expect(mockSetAllowed).toHaveBeenCalled();
+ expect(setPublicKey).toHaveBeenCalledWith('GABC1234567890XYZ');
+ expect(setNetwork).toHaveBeenCalledWith('TESTNET');
+ expect(toast).toHaveBeenCalledWith(
+ 'Wallet Connected',
+ 'Successfully connected to Freighter',
+ 'success',
+ );
+ });
+ });
+
+ it('renders connected wallet state and disconnects locally', async () => {
+ walletState = {
+ publicKey: 'GCONNECTED1234567890',
+ network: 'PUBLIC',
+ };
+ mockIsConnected.mockResolvedValue({ isConnected: true });
+ mockGetAddress.mockResolvedValue({ address: walletState.publicKey });
+ mockGetNetworkDetails.mockResolvedValue({ network: 'PUBLIC' });
+ mockWallet();
+
+ render();
+
+ expect(await screen.findByText('GCON...7890')).toBeInTheDocument();
+ expect(screen.getByText('PUBLIC')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Disconnect' }));
+
+ expect(disconnect).toHaveBeenCalled();
+ expect(toast).toHaveBeenCalledWith(
+ 'Disconnected',
+ 'Wallet has been disconnected',
+ 'info',
+ );
+ });
+
+ it('shows a retryable error when the user rejects connection', async () => {
+ mockSetAllowed.mockRejectedValue(new Error('User declined'));
+
+ render();
+
+ fireEvent.click(
+ await screen.findByRole('button', { name: 'Connect Freighter Wallet' }),
+ );
+
+ expect(
+ await screen.findByText('Connection cancelled by user.'),
+ ).toBeInTheDocument();
+ expect(setPublicKey).toHaveBeenCalledWith(null);
+ expect(toast).toHaveBeenCalledWith(
+ 'Connection Rejected',
+ 'Connection cancelled by user.',
+ 'error',
+ );
+ });
+});
diff --git a/app/frontend/src/components/dashboard/__tests__/FilteredPackageList.test.tsx b/app/frontend/src/components/dashboard/__tests__/FilteredPackageList.test.tsx
new file mode 100644
index 00000000..051521d4
--- /dev/null
+++ b/app/frontend/src/components/dashboard/__tests__/FilteredPackageList.test.tsx
@@ -0,0 +1,118 @@
+/** @jest-environment jsdom */
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import { FilteredPackageList } from '../FilteredPackageList';
+import { useAidPackages } from '@/hooks/useAidPackages';
+import { getAppUserRole } from '@/lib/app-role';
+import type { AidPackage } from '@/types/aid-package';
+
+jest.mock('@/hooks/useAidPackages', () => ({
+ useAidPackages: jest.fn(),
+}));
+
+jest.mock('@/lib/app-role', () => ({
+ getAppUserRole: jest.fn(),
+ isOperationsRole: (role: string) => role === 'operator',
+}));
+
+jest.mock('next/link', () => ({
+ __esModule: true,
+ default: ({
+ href,
+ children,
+ className,
+ }: {
+ href: string;
+ children: React.ReactNode;
+ className?: string;
+ }) => (
+
+ {children}
+
+ ),
+}));
+
+const mockUseAidPackages = useAidPackages as jest.MockedFunction<
+ typeof useAidPackages
+>;
+const mockGetAppUserRole = getAppUserRole as jest.MockedFunction<
+ typeof getAppUserRole
+>;
+
+const aidPackage: AidPackage = {
+ id: 'aid-001',
+ title: 'Winter Relief',
+ region: 'North District',
+ amount: '$2,500',
+ recipients: 14,
+ status: 'Active',
+ token: 'USDC',
+};
+
+describe('FilteredPackageList', () => {
+ beforeEach(() => {
+ mockGetAppUserRole.mockReturnValue('operator');
+ });
+
+ it('renders loading rows while aid packages are loading', () => {
+ mockUseAidPackages.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ error: null,
+ } as ReturnType);
+
+ const { container } = render();
+
+ expect(container.querySelectorAll('tbody tr')).toHaveLength(3);
+ expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(
+ 0,
+ );
+ });
+
+ it('renders an error state when the aid package query fails', () => {
+ mockUseAidPackages.mockReturnValue({
+ data: undefined,
+ isLoading: false,
+ error: new Error('Backend unavailable'),
+ } as ReturnType);
+
+ render();
+
+ expect(
+ screen.getByText('Error loading packages: Backend unavailable'),
+ ).toBeInTheDocument();
+ });
+
+ it('renders an empty state with filtered guidance when filters return no rows', () => {
+ mockUseAidPackages.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ } as ReturnType);
+
+ render();
+
+ expect(
+ screen.getAllByText('No aid packages match the current filters')[0],
+ ).toBeInTheDocument();
+ expect(screen.getAllByText('Reset dashboard filters')[0]).toHaveAttribute(
+ 'href',
+ '/dashboard',
+ );
+ });
+
+ it('renders package data in the desktop table and mobile cards', () => {
+ mockUseAidPackages.mockReturnValue({
+ data: [aidPackage],
+ isLoading: false,
+ error: null,
+ } as ReturnType);
+
+ render();
+
+ expect(screen.getAllByText('Winter Relief')).toHaveLength(2);
+ expect(screen.getAllByText('North District')).toHaveLength(2);
+ expect(screen.getAllByText('USDC')).toHaveLength(2);
+ expect(screen.getAllByText('aid-001')).toHaveLength(2);
+ });
+});
diff --git a/app/frontend/src/components/verification-review/__tests__/ReviewActionDialog.test.tsx b/app/frontend/src/components/verification-review/__tests__/ReviewActionDialog.test.tsx
new file mode 100644
index 00000000..e5f2931f
--- /dev/null
+++ b/app/frontend/src/components/verification-review/__tests__/ReviewActionDialog.test.tsx
@@ -0,0 +1,159 @@
+/** @jest-environment jsdom */
+import '@testing-library/jest-dom';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { ReviewActionDialog } from '../ReviewActionDialog';
+import {
+ useApproveVerification,
+ useRejectVerification,
+ useRequestResubmission,
+} from '@/hooks/useVerificationInbox';
+
+jest.mock('@/hooks/useVerificationInbox', () => ({
+ useApproveVerification: jest.fn(),
+ useRejectVerification: jest.fn(),
+ useRequestResubmission: jest.fn(),
+}));
+
+const mockUseApproveVerification =
+ useApproveVerification as jest.MockedFunction<
+ typeof useApproveVerification
+ >;
+const mockUseRejectVerification = useRejectVerification as jest.MockedFunction<
+ typeof useRejectVerification
+>;
+const mockUseRequestResubmission =
+ useRequestResubmission as jest.MockedFunction<
+ typeof useRequestResubmission
+ >;
+
+const approveMutateAsync = jest.fn();
+const rejectMutateAsync = jest.fn();
+const resubmitMutateAsync = jest.fn();
+
+function mockMutations() {
+ mockUseApproveVerification.mockReturnValue({
+ mutateAsync: approveMutateAsync,
+ isPending: false,
+ } as ReturnType);
+ mockUseRejectVerification.mockReturnValue({
+ mutateAsync: rejectMutateAsync,
+ isPending: false,
+ } as ReturnType);
+ mockUseRequestResubmission.mockReturnValue({
+ mutateAsync: resubmitMutateAsync,
+ isPending: false,
+ } as ReturnType);
+}
+
+describe('ReviewActionDialog', () => {
+ beforeEach(() => {
+ approveMutateAsync.mockReset();
+ rejectMutateAsync.mockReset();
+ resubmitMutateAsync.mockReset();
+ approveMutateAsync.mockResolvedValue(undefined);
+ rejectMutateAsync.mockResolvedValue(undefined);
+ resubmitMutateAsync.mockResolvedValue(undefined);
+ mockMutations();
+ });
+
+ it('approves a verification with optional reviewer messages', async () => {
+ const onOpenChange = jest.fn();
+ render(
+ ,
+ );
+
+ fireEvent.change(screen.getByPlaceholderText('Instructions shown to the applicant'), {
+ target: { value: 'Payment can proceed.' },
+ });
+ fireEvent.change(screen.getByPlaceholderText('Private note for the review team'), {
+ target: { value: 'Matched government ID.' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Approve' }));
+
+ await waitFor(() => {
+ expect(approveMutateAsync).toHaveBeenCalledWith({
+ id: 'ver-201',
+ payload: {
+ nextStepMessage: 'Payment can proceed.',
+ internalNote: 'Matched government ID.',
+ },
+ });
+ });
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+
+ it('requires a rejection reason before submitting', () => {
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Reject' }));
+
+ expect(screen.getByText('A reason is required.')).toBeInTheDocument();
+ expect(rejectMutateAsync).not.toHaveBeenCalled();
+ });
+
+ it('submits rejection and resubmission payloads through the correct hooks', async () => {
+ const { rerender } = render(
+ ,
+ );
+
+ fireEvent.change(screen.getByPlaceholderText('e.g. Document appears fraudulent'), {
+ target: { value: 'Document image is unreadable.' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Reject' }));
+
+ await waitFor(() => {
+ expect(rejectMutateAsync).toHaveBeenCalledWith({
+ id: 'ver-203',
+ payload: {
+ rejectionReason: 'Document image is unreadable.',
+ nextStepMessage: undefined,
+ internalNote: undefined,
+ },
+ });
+ });
+
+ rerender(
+ ,
+ );
+
+ fireEvent.change(screen.getByPlaceholderText('e.g. ID document is expired'), {
+ target: { value: 'ID document is expired.' },
+ });
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Request Resubmission' }),
+ );
+
+ await waitFor(() => {
+ expect(resubmitMutateAsync).toHaveBeenCalledWith({
+ id: 'ver-204',
+ payload: {
+ rejectionReason: 'ID document is expired.',
+ nextStepMessage: 'Please resubmit the required documents.',
+ internalNote: undefined,
+ },
+ });
+ });
+ });
+});
diff --git a/app/frontend/src/components/verification-review/__tests__/ReviewFiltersBar.test.tsx b/app/frontend/src/components/verification-review/__tests__/ReviewFiltersBar.test.tsx
new file mode 100644
index 00000000..bf0bc563
--- /dev/null
+++ b/app/frontend/src/components/verification-review/__tests__/ReviewFiltersBar.test.tsx
@@ -0,0 +1,64 @@
+/** @jest-environment jsdom */
+import '@testing-library/jest-dom';
+import { fireEvent, render, screen } from '@testing-library/react';
+import { ReviewFiltersBar } from '../ReviewFiltersBar';
+import type { ReviewFilters } from '@/types/verification-review';
+
+const baseFilters: ReviewFilters = {
+ status: '',
+ riskLevel: '',
+ dateFrom: '',
+ dateTo: '',
+ page: 1,
+};
+
+describe('ReviewFiltersBar', () => {
+ it('updates date filters and resets pagination', () => {
+ const onChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const [fromInput, toInput] = container.querySelectorAll(
+ 'input[type="date"]',
+ );
+
+ fireEvent.change(fromInput, { target: { value: '2026-07-01' } });
+ fireEvent.change(toInput, { target: { value: '2026-07-05' } });
+
+ expect(onChange).toHaveBeenNthCalledWith(1, {
+ dateFrom: '2026-07-01',
+ page: 1,
+ });
+ expect(onChange).toHaveBeenNthCalledWith(2, {
+ dateTo: '2026-07-05',
+ page: 1,
+ });
+ });
+
+ it('clears active status, risk, and date filters', () => {
+ const onChange = jest.fn();
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
+
+ expect(onChange).toHaveBeenCalledWith({
+ status: '',
+ riskLevel: '',
+ dateFrom: '',
+ dateTo: '',
+ page: 1,
+ });
+ });
+});
diff --git a/app/frontend/src/components/verification-review/__tests__/ReviewQueue.test.tsx b/app/frontend/src/components/verification-review/__tests__/ReviewQueue.test.tsx
new file mode 100644
index 00000000..ad298f23
--- /dev/null
+++ b/app/frontend/src/components/verification-review/__tests__/ReviewQueue.test.tsx
@@ -0,0 +1,140 @@
+/** @jest-environment jsdom */
+import '@testing-library/jest-dom';
+import { fireEvent, render, screen } from '@testing-library/react';
+import { ReviewQueue } from '../ReviewQueue';
+import { useInbox } from '@/hooks/useVerificationInbox';
+import type {
+ ReviewFilters,
+ VerificationInboxItem,
+ VerificationInboxResponse,
+} from '@/types/verification-review';
+
+jest.mock('@/hooks/useVerificationInbox', () => ({
+ useInbox: jest.fn(),
+}));
+
+jest.mock('../VerificationDetailPanel', () => ({
+ VerificationDetailPanel: ({
+ verificationId,
+ onClose,
+ }: {
+ verificationId: string;
+ onClose: () => void;
+ }) => (
+
+ Detail for {verificationId}
+
+
+ ),
+}));
+
+const mockUseInbox = useInbox as jest.MockedFunction;
+
+const filters: ReviewFilters = {
+ status: '',
+ riskLevel: '',
+ dateFrom: '',
+ dateTo: '',
+ page: 1,
+};
+
+const inboxItem: VerificationInboxItem = {
+ id: 'ver-101',
+ status: 'pending_review',
+ createdAt: '2026-07-01T12:00:00.000Z',
+ reviewedAt: null,
+ reviewedBy: null,
+ rejectionReason: null,
+ nextStepMessage: 'Confirm identity document',
+ deepLink: '/verifications/ver-101',
+ riskLevel: 'high',
+};
+
+const response: VerificationInboxResponse = {
+ items: [inboxItem],
+ total: 7,
+ page: 2,
+ limit: 5,
+ totalPages: 3,
+};
+
+describe('ReviewQueue', () => {
+ beforeEach(() => {
+ mockUseInbox.mockReset();
+ });
+
+ it('renders loading placeholders while the verification queue is loading', () => {
+ mockUseInbox.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ isError: false,
+ error: null,
+ } as ReturnType);
+
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelectorAll('.animate-pulse .h-16')).toHaveLength(5);
+ });
+
+ it('renders queue errors from the inbox hook', () => {
+ mockUseInbox.mockReturnValue({
+ data: undefined,
+ isLoading: false,
+ isError: true,
+ error: new Error('Inbox unavailable'),
+ } as ReturnType);
+
+ render();
+
+ expect(
+ screen.getByText('Failed to load queue: Inbox unavailable'),
+ ).toBeInTheDocument();
+ });
+
+ it('renders an empty queue message when filters have no matches', () => {
+ mockUseInbox.mockReturnValue({
+ data: { ...response, items: [], total: 0, totalPages: 1 },
+ isLoading: false,
+ isError: false,
+ error: null,
+ } as ReturnType);
+
+ render();
+
+ expect(
+ screen.getByText('No verification cases match the current filters.'),
+ ).toBeInTheDocument();
+ });
+
+ it('renders queue rows, opens details, and pages through results', () => {
+ const onPageChange = jest.fn();
+ mockUseInbox.mockReturnValue({
+ data: response,
+ isLoading: false,
+ isError: false,
+ error: null,
+ } as ReturnType);
+
+ const { container } = render(
+ ,
+ );
+
+ fireEvent.click(screen.getByText('ver-101'));
+ expect(screen.getByTestId('verification-detail')).toHaveTextContent(
+ 'Detail for ver-101',
+ );
+ expect(screen.getByText('Confirm identity document')).toBeInTheDocument();
+ expect(screen.getByText('Page 2 of 3 ยท 7 total')).toBeInTheDocument();
+
+ const pagerButtons = container.querySelectorAll('button.h-8');
+ fireEvent.click(pagerButtons[0]);
+ fireEvent.click(pagerButtons[1]);
+
+ expect(onPageChange).toHaveBeenNthCalledWith(1, 1);
+ expect(onPageChange).toHaveBeenNthCalledWith(2, 3);
+ });
+});