241 lines
8.4 KiB
TypeScript
241 lines
8.4 KiB
TypeScript
/**
|
|
* 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(<AdminDashboardWrapper viewData={mockDashboardData} />);
|
|
|
|
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(<AdminDashboardWrapper viewData={mockDashboardData} />);
|
|
|
|
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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
const clearButton = screen.getByText(/Clear all/i);
|
|
fireEvent.click(clearButton);
|
|
|
|
expect(mockPush).toHaveBeenCalledWith('/admin/users');
|
|
});
|
|
|
|
it('should select individual users', () => {
|
|
render(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
// 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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
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(<AdminUsersWrapper viewData={mockUsersData} />);
|
|
|
|
const suspendButtons = screen.getAllByRole('button', { name: /Suspend/i });
|
|
fireEvent.click(suspendButtons[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Failed to update')).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
});
|