/** * Admin Feature Flow Tests * * These tests verify routing, guards, navigation, cross-screen state, and user flows * for the admin module. They run with real frontend and mocked contracts. * * @file apps/website/tests/flows/admin.test.tsx */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { AdminDashboardWrapper } from '@/client-wrapper/AdminDashboardWrapper'; import { AdminUsersWrapper } from '@/client-wrapper/AdminUsersWrapper'; import type { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData'; import type { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData'; import { updateUserStatus, deleteUser } from '@/app/actions/adminActions'; import { Result } from '@/lib/contracts/Result'; import React from 'react'; // Mock next/navigation const mockPush = vi.fn(); const mockRefresh = vi.fn(); const mockSearchParams = new URLSearchParams(); vi.mock('next/navigation', () => ({ useRouter: () => ({ push: mockPush, refresh: mockRefresh, }), useSearchParams: () => mockSearchParams, usePathname: () => '/admin', })); // Mock server actions vi.mock('@/app/actions/adminActions', () => ({ updateUserStatus: vi.fn(), deleteUser: vi.fn(), })); describe('Admin Feature Flow', () => { beforeEach(() => { vi.clearAllMocks(); mockSearchParams.delete('search'); mockSearchParams.delete('role'); mockSearchParams.delete('status'); }); describe('Admin Dashboard Flow', () => { const mockDashboardData: AdminDashboardViewData = { stats: { totalUsers: 150, activeUsers: 120, suspendedUsers: 25, deletedUsers: 5, systemAdmins: 10, recentLogins: 45, newUsersToday: 3, }, }; it('should display dashboard statistics', () => { render(); expect(screen.getByText('150')).toBeDefined(); expect(screen.getByText('120')).toBeDefined(); expect(screen.getByText('25')).toBeDefined(); expect(screen.getByText('5')).toBeDefined(); expect(screen.getByText('10')).toBeDefined(); }); it('should trigger refresh when refresh button is clicked', () => { render(); const refreshButton = screen.getByText(/Refresh Telemetry/i); fireEvent.click(refreshButton); expect(mockRefresh).toHaveBeenCalled(); }); }); describe('Admin Users Management Flow', () => { const mockUsersData: AdminUsersViewData = { users: [ { id: 'user-1', email: 'john@example.com', displayName: 'John Doe', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-15T10:00:00Z', updatedAt: '2024-01-15T10:00:00Z', }, { id: 'user-2', email: 'jane@example.com', displayName: 'Jane Smith', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-14T15:30:00Z', updatedAt: '2024-01-14T15:30:00Z', }, ], total: 2, page: 1, limit: 50, totalPages: 1, activeUserCount: 2, adminCount: 1, }; it('should display users list', () => { render(); expect(screen.getByText('john@example.com')).toBeDefined(); expect(screen.getByText('jane@example.com')).toBeDefined(); expect(screen.getByText('John Doe')).toBeDefined(); expect(screen.getByText('Jane Smith')).toBeDefined(); }); it('should update URL when searching', () => { render(); const searchInput = screen.getByPlaceholderText(/Search by email or name/i); fireEvent.change(searchInput, { target: { value: 'john' } }); expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('search=john')); }); it('should update URL when filtering by role', () => { render(); const selects = screen.getAllByRole('combobox'); // First select is role, second is status based on UserFilters.tsx fireEvent.change(selects[0], { target: { value: 'admin' } }); expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('role=admin')); }); it('should update URL when filtering by status', () => { render(); const selects = screen.getAllByRole('combobox'); fireEvent.change(selects[1], { target: { value: 'active' } }); expect(mockPush).toHaveBeenCalledWith(expect.stringContaining('status=active')); }); it('should clear filters when clear button is clicked', () => { // Set some filters in searchParams mock if needed, but wrapper uses searchParams.get // Actually, the "Clear all" button only appears if filters are present mockSearchParams.set('search', 'john'); render(); const clearButton = screen.getByText(/Clear all/i); fireEvent.click(clearButton); expect(mockPush).toHaveBeenCalledWith('/admin/users'); }); it('should select individual users', () => { render(); const checkboxes = screen.getAllByRole('checkbox'); // First checkbox is "Select all users", second is user-1 fireEvent.click(checkboxes[1]); // Use getAllByText because '1' appears in stats too expect(screen.getAllByText('1').length).toBeGreaterThan(0); expect(screen.getByText(/Items Selected/i)).toBeDefined(); }); it('should select all users', () => { render(); // Use getAllByRole and find the one with the right aria-label const checkboxes = screen.getAllByRole('checkbox'); // In JSDOM, aria-label might be accessed differently or the component might not be rendering it as expected // Let's try to find it by index if label fails, but first try a more robust search const selectAllCheckbox = checkboxes[0]; // Usually the first one in the header fireEvent.click(selectAllCheckbox); expect(screen.getAllByText('2').length).toBeGreaterThan(0); expect(screen.getByText(/Items Selected/i)).toBeDefined(); }); it('should call updateUserStatus action', async () => { vi.mocked(updateUserStatus).mockResolvedValue(Result.ok({ success: true })); render(); const suspendButtons = screen.getAllByRole('button', { name: /Suspend/i }); fireEvent.click(suspendButtons[0]); await waitFor(() => { expect(updateUserStatus).toHaveBeenCalledWith('user-1', 'suspended'); }); expect(mockRefresh).toHaveBeenCalled(); }); it('should open delete confirmation and call deleteUser action', async () => { vi.mocked(deleteUser).mockResolvedValue(Result.ok({ success: true })); render(); const deleteButtons = screen.getAllByRole('button', { name: /Delete/i }); // There are 2 users, so 2 delete buttons in the table fireEvent.click(deleteButtons[0]); // Verify dialog is open - ConfirmDialog has title "Delete User" // We use getAllByText because "Delete User" is also the button label const dialogTitles = screen.getAllByText(/Delete User/i); expect(dialogTitles.length).toBeGreaterThan(0); expect(screen.getByText(/Are you sure you want to delete this user/i)).toBeDefined(); // The confirm button in the dialog const confirmButton = screen.getByRole('button', { name: 'Delete User' }); fireEvent.click(confirmButton); await waitFor(() => { expect(deleteUser).toHaveBeenCalledWith('user-1'); }); expect(mockRefresh).toHaveBeenCalled(); }); it('should handle action errors gracefully', async () => { vi.mocked(updateUserStatus).mockResolvedValue(Result.err('Failed to update')); render(); const suspendButtons = screen.getAllByRole('button', { name: /Suspend/i }); fireEvent.click(suspendButtons[0]); await waitFor(() => { expect(screen.getByText('Failed to update')).toBeDefined(); }); }); }); });