add tests
This commit is contained in:
101
apps/website/components/admin/AdminDangerZonePanel.test.tsx
Normal file
101
apps/website/components/admin/AdminDangerZonePanel.test.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* AdminDangerZonePanel Component Tests
|
||||
*
|
||||
* Tests for the AdminDangerZonePanel component that wraps the DangerZone UI component.
|
||||
* Tests cover rendering, props, and interaction behavior.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdminDangerZonePanel } from './AdminDangerZonePanel';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the DangerZone UI component
|
||||
vi.mock('@/ui/DangerZone', () => ({
|
||||
DangerZone: ({ title, description, children }: any) => (
|
||||
<div data-testid="danger-zone">
|
||||
<h2>{title}</h2>
|
||||
<p>{description}</p>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('AdminDangerZonePanel', () => {
|
||||
it('should render with title and description', () => {
|
||||
render(
|
||||
<AdminDangerZonePanel
|
||||
title="Delete Account"
|
||||
description="This action cannot be undone"
|
||||
>
|
||||
<button>Delete</button>
|
||||
</AdminDangerZonePanel>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Delete Account')).toBeTruthy();
|
||||
expect(screen.getByText('This action cannot be undone')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render children content', () => {
|
||||
render(
|
||||
<AdminDangerZonePanel
|
||||
title="Danger Zone"
|
||||
description="Proceed with caution"
|
||||
>
|
||||
<button data-testid="danger-button">Delete</button>
|
||||
</AdminDangerZonePanel>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('danger-button')).toBeTruthy();
|
||||
expect(screen.getByText('Delete')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with minimal props', () => {
|
||||
render(
|
||||
<AdminDangerZonePanel title="Danger Zone" description="">
|
||||
<button>Proceed</button>
|
||||
</AdminDangerZonePanel>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Danger Zone')).toBeTruthy();
|
||||
expect(screen.getByText('Proceed')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render multiple children', () => {
|
||||
render(
|
||||
<AdminDangerZonePanel
|
||||
title="Multiple Actions"
|
||||
description="Select an action"
|
||||
>
|
||||
<button>Option 1</button>
|
||||
<button>Option 2</button>
|
||||
<button>Option 3</button>
|
||||
</AdminDangerZonePanel>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Option 1')).toBeTruthy();
|
||||
expect(screen.getByText('Option 2')).toBeTruthy();
|
||||
expect(screen.getByText('Option 3')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with complex children components', () => {
|
||||
const ComplexChild = () => (
|
||||
<div>
|
||||
<span>Complex</span>
|
||||
<button>Click me</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminDangerZonePanel
|
||||
title="Complex Content"
|
||||
description="With nested elements"
|
||||
>
|
||||
<ComplexChild />
|
||||
</AdminDangerZonePanel>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Complex')).toBeTruthy();
|
||||
expect(screen.getByText('Click me')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
81
apps/website/components/admin/AdminDashboardLayout.test.tsx
Normal file
81
apps/website/components/admin/AdminDashboardLayout.test.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* AdminDashboardLayout Component Tests
|
||||
*
|
||||
* Tests for the AdminDashboardLayout component that provides a consistent
|
||||
* container layout for admin pages.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdminDashboardLayout } from './AdminDashboardLayout';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('AdminDashboardLayout', () => {
|
||||
it('should render children content', () => {
|
||||
render(
|
||||
<AdminDashboardLayout>
|
||||
<div data-testid="content">Dashboard Content</div>
|
||||
</AdminDashboardLayout>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('content')).toBeTruthy();
|
||||
expect(screen.getByText('Dashboard Content')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render multiple children', () => {
|
||||
render(
|
||||
<AdminDashboardLayout>
|
||||
<div>Section 1</div>
|
||||
<div>Section 2</div>
|
||||
<div>Section 3</div>
|
||||
</AdminDashboardLayout>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Section 1')).toBeTruthy();
|
||||
expect(screen.getByText('Section 2')).toBeTruthy();
|
||||
expect(screen.getByText('Section 3')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with complex nested components', () => {
|
||||
const ComplexComponent = () => (
|
||||
<div>
|
||||
<h2>Complex Section</h2>
|
||||
<p>With multiple elements</p>
|
||||
<button>Action</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminDashboardLayout>
|
||||
<ComplexComponent />
|
||||
</AdminDashboardLayout>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Complex Section')).toBeTruthy();
|
||||
expect(screen.getByText('With multiple elements')).toBeTruthy();
|
||||
expect(screen.getByText('Action')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render empty layout gracefully', () => {
|
||||
render(<AdminDashboardLayout />);
|
||||
|
||||
// Should render without errors even with no children
|
||||
expect(document.body).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with mixed content types', () => {
|
||||
render(
|
||||
<AdminDashboardLayout>
|
||||
<div>Text content</div>
|
||||
<span>Span content</span>
|
||||
<button>Button</button>
|
||||
<input type="text" placeholder="Input" />
|
||||
</AdminDashboardLayout>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Text content')).toBeInTheDocument();
|
||||
expect(screen.getByText('Span content')).toBeInTheDocument();
|
||||
expect(screen.getByText('Button')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Input')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
153
apps/website/components/admin/AdminDataTable.test.tsx
Normal file
153
apps/website/components/admin/AdminDataTable.test.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* AdminDataTable Component Tests
|
||||
*
|
||||
* Tests for the AdminDataTable component that provides a consistent
|
||||
* container for high-density admin tables.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdminDataTable } from './AdminDataTable';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('AdminDataTable', () => {
|
||||
it('should render children content', () => {
|
||||
render(
|
||||
<AdminDataTable>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Test Data</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AdminDataTable>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Data')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with maxHeight prop', () => {
|
||||
render(
|
||||
<AdminDataTable maxHeight={400}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Scrollable Content</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AdminDataTable>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Scrollable Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with string maxHeight prop', () => {
|
||||
render(
|
||||
<AdminDataTable maxHeight="500px">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Scrollable Content</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AdminDataTable>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Scrollable Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render without maxHeight prop', () => {
|
||||
render(
|
||||
<AdminDataTable>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Content</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AdminDataTable>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render multiple table rows', () => {
|
||||
render(
|
||||
<AdminDataTable>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Row 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AdminDataTable>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Row 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Row 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Row 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with complex table structure', () => {
|
||||
render(
|
||||
<AdminDataTable>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Header 1</th>
|
||||
<th>Header 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Data 1</td>
|
||||
<td>Data 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AdminDataTable>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Header 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Header 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Data 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Data 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with nested components', () => {
|
||||
const NestedComponent = () => (
|
||||
<div>
|
||||
<span>Nested</span>
|
||||
<button>Action</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminDataTable>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<NestedComponent />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AdminDataTable>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Nested')).toBeInTheDocument();
|
||||
expect(screen.getByText('Action')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
121
apps/website/components/admin/AdminEmptyState.test.tsx
Normal file
121
apps/website/components/admin/AdminEmptyState.test.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* AdminEmptyState Component Tests
|
||||
*
|
||||
* Tests for the AdminEmptyState component that displays empty state UI
|
||||
* for admin lists and tables.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdminEmptyState } from './AdminEmptyState';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Inbox, Users, AlertCircle } from 'lucide-react';
|
||||
|
||||
describe('AdminEmptyState', () => {
|
||||
it('should render with icon, title, and description', () => {
|
||||
render(
|
||||
<AdminEmptyState
|
||||
icon={Inbox}
|
||||
title="No Data Available"
|
||||
description="Get started by creating your first item"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('No Data Available')).toBeTruthy();
|
||||
expect(screen.getByText('Get started by creating your first item')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with minimal props (description optional)', () => {
|
||||
render(
|
||||
<AdminEmptyState
|
||||
icon={Users}
|
||||
title="No Users"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('No Users')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with action button', () => {
|
||||
const actionButton = <button data-testid="action-btn">Create Item</button>;
|
||||
|
||||
render(
|
||||
<AdminEmptyState
|
||||
icon={Inbox}
|
||||
title="Empty List"
|
||||
description="Add some items"
|
||||
action={actionButton}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Empty List')).toBeTruthy();
|
||||
expect(screen.getByText('Add some items')).toBeTruthy();
|
||||
expect(screen.getByTestId('action-btn')).toBeTruthy();
|
||||
expect(screen.getByText('Create Item')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with different icons', () => {
|
||||
const icons = [Inbox, Users, AlertCircle];
|
||||
|
||||
icons.forEach((Icon) => {
|
||||
const { container } = render(
|
||||
<AdminEmptyState
|
||||
icon={Icon}
|
||||
title="Test Title"
|
||||
/>
|
||||
);
|
||||
|
||||
// Check that the component renders without errors
|
||||
expect(screen.getByText('Test Title')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render with complex action component', () => {
|
||||
const ComplexAction = () => (
|
||||
<div>
|
||||
<button>Primary Action</button>
|
||||
<button>Secondary Action</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminEmptyState
|
||||
icon={Inbox}
|
||||
title="Complex State"
|
||||
description="Multiple actions available"
|
||||
action={<ComplexAction />}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Complex State')).toBeTruthy();
|
||||
expect(screen.getByText('Multiple actions available')).toBeTruthy();
|
||||
expect(screen.getByText('Primary Action')).toBeTruthy();
|
||||
expect(screen.getByText('Secondary Action')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with long text content', () => {
|
||||
render(
|
||||
<AdminEmptyState
|
||||
icon={Inbox}
|
||||
title="This is a very long title that might wrap to multiple lines in the UI"
|
||||
description="This is an even longer description that provides detailed information about why the state is empty and what the user should do next"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/This is a very long title/)).toBeTruthy();
|
||||
expect(screen.getByText(/This is an even longer description/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with special characters in text', () => {
|
||||
render(
|
||||
<AdminEmptyState
|
||||
icon={Inbox}
|
||||
title="Special & Characters <Test>"
|
||||
description="Quotes 'and' special characters"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Special & Characters/)).toBeTruthy();
|
||||
expect(screen.getByText(/Quotes/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
167
apps/website/components/admin/AdminHeaderPanel.test.tsx
Normal file
167
apps/website/components/admin/AdminHeaderPanel.test.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* AdminHeaderPanel Component Tests
|
||||
*
|
||||
* Tests for the AdminHeaderPanel component that provides a semantic header
|
||||
* for admin pages with title, description, actions, and loading state.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdminHeaderPanel } from './AdminHeaderPanel';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the ProgressLine component
|
||||
vi.mock('@/components/shared/ProgressLine', () => ({
|
||||
ProgressLine: ({ isLoading }: { isLoading: boolean }) => (
|
||||
<div data-testid="progress-line" data-loading={isLoading}>
|
||||
{isLoading ? 'Loading...' : 'Ready'}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the SectionHeader component
|
||||
vi.mock('@/ui/SectionHeader', () => ({
|
||||
SectionHeader: ({ title, description, actions, loading }: any) => (
|
||||
<div data-testid="section-header">
|
||||
<h1>{title}</h1>
|
||||
{description && <p>{description}</p>}
|
||||
{actions && <div data-testid="actions">{actions}</div>}
|
||||
{loading}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('AdminHeaderPanel', () => {
|
||||
it('should render with title only', () => {
|
||||
render(
|
||||
<AdminHeaderPanel title="Admin Dashboard" />
|
||||
);
|
||||
|
||||
expect(screen.getByText('Admin Dashboard')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with title and description', () => {
|
||||
render(
|
||||
<AdminHeaderPanel
|
||||
title="User Management"
|
||||
description="Manage all user accounts and permissions"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('User Management')).toBeTruthy();
|
||||
expect(screen.getByText('Manage all user accounts and permissions')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with title, description, and actions', () => {
|
||||
const actions = <button data-testid="action-btn">Create User</button>;
|
||||
|
||||
render(
|
||||
<AdminHeaderPanel
|
||||
title="User Management"
|
||||
description="Manage all user accounts"
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('User Management')).toBeTruthy();
|
||||
expect(screen.getByText('Manage all user accounts')).toBeTruthy();
|
||||
expect(screen.getByTestId('action-btn')).toBeTruthy();
|
||||
expect(screen.getByText('Create User')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with loading state', () => {
|
||||
render(
|
||||
<AdminHeaderPanel
|
||||
title="Loading Data"
|
||||
isLoading={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Loading Data')).toBeTruthy();
|
||||
expect(screen.getByTestId('progress-line')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render without loading state by default', () => {
|
||||
render(
|
||||
<AdminHeaderPanel
|
||||
title="Ready State"
|
||||
isLoading={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Ready State')).toBeTruthy();
|
||||
expect(screen.getByTestId('progress-line')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with multiple action buttons', () => {
|
||||
const actions = (
|
||||
<div>
|
||||
<button>Save</button>
|
||||
<button>Cancel</button>
|
||||
<button>Delete</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminHeaderPanel
|
||||
title="Edit User"
|
||||
description="Make changes to user profile"
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Edit User')).toBeTruthy();
|
||||
expect(screen.getByText('Make changes to user profile')).toBeTruthy();
|
||||
expect(screen.getByText('Save')).toBeTruthy();
|
||||
expect(screen.getByText('Cancel')).toBeTruthy();
|
||||
expect(screen.getByText('Delete')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with complex actions component', () => {
|
||||
const ComplexActions = () => (
|
||||
<div>
|
||||
<button>Primary Action</button>
|
||||
<button>Secondary Action</button>
|
||||
<button>Tertiary Action</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminHeaderPanel
|
||||
title="Complex Header"
|
||||
description="With multiple actions"
|
||||
actions={<ComplexActions />}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Complex Header')).toBeTruthy();
|
||||
expect(screen.getByText('With multiple actions')).toBeTruthy();
|
||||
expect(screen.getByText('Primary Action')).toBeTruthy();
|
||||
expect(screen.getByText('Secondary Action')).toBeTruthy();
|
||||
expect(screen.getByText('Tertiary Action')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with long title and description', () => {
|
||||
render(
|
||||
<AdminHeaderPanel
|
||||
title="This is a very long header title that might wrap to multiple lines in the UI"
|
||||
description="This is an even longer description that provides detailed information about the page content and what users can expect to find here"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/This is a very long header title/)).toBeTruthy();
|
||||
expect(screen.getByText(/This is an even longer description/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with special characters in text', () => {
|
||||
render(
|
||||
<AdminHeaderPanel
|
||||
title="Special & Characters <Test>"
|
||||
description="Quotes 'and' special characters"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Special & Characters/)).toBeTruthy();
|
||||
expect(screen.getByText(/Quotes/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
131
apps/website/components/admin/AdminSectionHeader.test.tsx
Normal file
131
apps/website/components/admin/AdminSectionHeader.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* AdminSectionHeader Component Tests
|
||||
*
|
||||
* Tests for the AdminSectionHeader component that provides a semantic header
|
||||
* for sections within admin pages.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdminSectionHeader } from './AdminSectionHeader';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the SectionHeader component
|
||||
vi.mock('@/ui/SectionHeader', () => ({
|
||||
SectionHeader: ({ title, description, actions, variant }: any) => (
|
||||
<div data-testid="section-header" data-variant={variant}>
|
||||
<h2>{title}</h2>
|
||||
{description && <p>{description}</p>}
|
||||
{actions && <div data-testid="actions">{actions}</div>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('AdminSectionHeader', () => {
|
||||
it('should render with title only', () => {
|
||||
render(
|
||||
<AdminSectionHeader title="User Statistics" />
|
||||
);
|
||||
|
||||
expect(screen.getByText('User Statistics')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with title and description', () => {
|
||||
render(
|
||||
<AdminSectionHeader
|
||||
title="User Statistics"
|
||||
description="Overview of user activity and engagement"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('User Statistics')).toBeTruthy();
|
||||
expect(screen.getByText('Overview of user activity and engagement')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with title, description, and actions', () => {
|
||||
const actions = <button data-testid="action-btn">Refresh</button>;
|
||||
|
||||
render(
|
||||
<AdminSectionHeader
|
||||
title="User Statistics"
|
||||
description="Overview of user activity"
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('User Statistics')).toBeTruthy();
|
||||
expect(screen.getByText('Overview of user activity')).toBeTruthy();
|
||||
expect(screen.getByTestId('action-btn')).toBeTruthy();
|
||||
expect(screen.getByText('Refresh')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with multiple action buttons', () => {
|
||||
const actions = (
|
||||
<div>
|
||||
<button>Export</button>
|
||||
<button>Filter</button>
|
||||
<button>Sort</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminSectionHeader
|
||||
title="Data Table"
|
||||
description="Manage your data"
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Data Table')).toBeTruthy();
|
||||
expect(screen.getByText('Manage your data')).toBeTruthy();
|
||||
expect(screen.getByText('Export')).toBeTruthy();
|
||||
expect(screen.getByText('Filter')).toBeTruthy();
|
||||
expect(screen.getByText('Sort')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with complex actions component', () => {
|
||||
const ComplexActions = () => (
|
||||
<div>
|
||||
<button>Primary</button>
|
||||
<button>Secondary</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminSectionHeader
|
||||
title="Complex Section"
|
||||
description="With multiple actions"
|
||||
actions={<ComplexActions />}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Complex Section')).toBeTruthy();
|
||||
expect(screen.getByText('With multiple actions')).toBeTruthy();
|
||||
expect(screen.getByText('Primary')).toBeTruthy();
|
||||
expect(screen.getByText('Secondary')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with long title and description', () => {
|
||||
render(
|
||||
<AdminSectionHeader
|
||||
title="This is a very long section header title that might wrap to multiple lines in the UI"
|
||||
description="This is an even longer description that provides detailed information about the section content and what users can expect to find here"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/This is a very long section header title/)).toBeTruthy();
|
||||
expect(screen.getByText(/This is an even longer description/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with special characters in text', () => {
|
||||
render(
|
||||
<AdminSectionHeader
|
||||
title="Special & Characters <Test>"
|
||||
description="Quotes 'and' special characters"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Special & Characters/)).toBeTruthy();
|
||||
expect(screen.getByText(/Quotes/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
180
apps/website/components/admin/AdminStatsPanel.test.tsx
Normal file
180
apps/website/components/admin/AdminStatsPanel.test.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* AdminStatsPanel Component Tests
|
||||
*
|
||||
* Tests for the AdminStatsPanel component that displays statistics
|
||||
* in a grid format for admin dashboards.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdminStatsPanel } from './AdminStatsPanel';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Users, Shield, Activity } from 'lucide-react';
|
||||
|
||||
// Mock the StatGrid component
|
||||
vi.mock('@/ui/StatGrid', () => ({
|
||||
StatGrid: ({ stats, columns }: any) => (
|
||||
<div data-testid="stat-grid" data-columns={JSON.stringify(columns)}>
|
||||
{stats.map((stat: any, index: number) => (
|
||||
<div key={index} data-testid={`stat-${index}`}>
|
||||
<span>{stat.label}</span>
|
||||
<span>{stat.value}</span>
|
||||
{stat.icon && <span data-testid="icon">{stat.icon.name || 'Icon'}</span>}
|
||||
{stat.intent && <span data-testid="intent">{stat.intent}</span>}
|
||||
{stat.trend && <span data-testid="trend">{stat.trend.value}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('AdminStatsPanel', () => {
|
||||
it('should render with single stat', () => {
|
||||
const stats = [
|
||||
{
|
||||
label: 'Total Users',
|
||||
value: '1,234',
|
||||
icon: Users,
|
||||
intent: 'primary' as const,
|
||||
},
|
||||
];
|
||||
|
||||
render(<AdminStatsPanel stats={stats} />);
|
||||
|
||||
expect(screen.getByText('Total Users')).toBeTruthy();
|
||||
expect(screen.getByText('1,234')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with multiple stats', () => {
|
||||
const stats = [
|
||||
{
|
||||
label: 'Total Users',
|
||||
value: '1,234',
|
||||
icon: Users,
|
||||
intent: 'primary' as const,
|
||||
},
|
||||
{
|
||||
label: 'Active Users',
|
||||
value: '892',
|
||||
icon: Activity,
|
||||
intent: 'success' as const,
|
||||
},
|
||||
{
|
||||
label: 'Admins',
|
||||
value: '12',
|
||||
icon: Shield,
|
||||
intent: 'telemetry' as const,
|
||||
},
|
||||
];
|
||||
|
||||
render(<AdminStatsPanel stats={stats} />);
|
||||
|
||||
expect(screen.getByText('Total Users')).toBeTruthy();
|
||||
expect(screen.getByText('1,234')).toBeTruthy();
|
||||
expect(screen.getByText('Active Users')).toBeTruthy();
|
||||
expect(screen.getByText('892')).toBeTruthy();
|
||||
expect(screen.getByText('Admins')).toBeTruthy();
|
||||
expect(screen.getByText('12')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render stats with trends', () => {
|
||||
const stats = [
|
||||
{
|
||||
label: 'Growth',
|
||||
value: '15%',
|
||||
icon: Activity,
|
||||
intent: 'success' as const,
|
||||
trend: {
|
||||
value: 5,
|
||||
isPositive: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(<AdminStatsPanel stats={stats} />);
|
||||
|
||||
expect(screen.getByText('Growth')).toBeTruthy();
|
||||
expect(screen.getByText('15%')).toBeTruthy();
|
||||
expect(screen.getByText('5')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render stats with different intents', () => {
|
||||
const stats = [
|
||||
{
|
||||
label: 'Primary',
|
||||
value: '100',
|
||||
icon: Users,
|
||||
intent: 'primary' as const,
|
||||
},
|
||||
{
|
||||
label: 'Success',
|
||||
value: '200',
|
||||
icon: Users,
|
||||
intent: 'success' as const,
|
||||
},
|
||||
{
|
||||
label: 'Warning',
|
||||
value: '300',
|
||||
icon: Users,
|
||||
intent: 'warning' as const,
|
||||
},
|
||||
{
|
||||
label: 'Critical',
|
||||
value: '400',
|
||||
icon: Users,
|
||||
intent: 'critical' as const,
|
||||
},
|
||||
{
|
||||
label: 'Telemetry',
|
||||
value: '500',
|
||||
icon: Users,
|
||||
intent: 'telemetry' as const,
|
||||
},
|
||||
];
|
||||
|
||||
render(<AdminStatsPanel stats={stats} />);
|
||||
|
||||
expect(screen.getByText('Primary')).toBeTruthy();
|
||||
expect(screen.getByText('Success')).toBeTruthy();
|
||||
expect(screen.getByText('Warning')).toBeTruthy();
|
||||
expect(screen.getByText('Critical')).toBeTruthy();
|
||||
expect(screen.getByText('Telemetry')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render stats with numeric values', () => {
|
||||
const stats = [
|
||||
{
|
||||
label: 'Count',
|
||||
value: 42,
|
||||
icon: Users,
|
||||
},
|
||||
];
|
||||
|
||||
render(<AdminStatsPanel stats={stats} />);
|
||||
|
||||
expect(screen.getByText('Count')).toBeTruthy();
|
||||
expect(screen.getByText('42')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render stats with string values', () => {
|
||||
const stats = [
|
||||
{
|
||||
label: 'Status',
|
||||
value: 'Active',
|
||||
icon: Shield,
|
||||
},
|
||||
];
|
||||
|
||||
render(<AdminStatsPanel stats={stats} />);
|
||||
|
||||
expect(screen.getByText('Status')).toBeTruthy();
|
||||
expect(screen.getByText('Active')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with empty stats array', () => {
|
||||
render(<AdminStatsPanel stats={[]} />);
|
||||
|
||||
// Should render without errors
|
||||
expect(document.body).toBeTruthy();
|
||||
});
|
||||
});
|
||||
145
apps/website/components/admin/AdminToolbar.test.tsx
Normal file
145
apps/website/components/admin/AdminToolbar.test.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* AdminToolbar Component Tests
|
||||
*
|
||||
* Tests for the AdminToolbar component that provides a semantic toolbar
|
||||
* for admin pages with filters, search, and secondary actions.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdminToolbar } from './AdminToolbar';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the ControlBar component
|
||||
vi.mock('@/ui/ControlBar', () => ({
|
||||
ControlBar: ({ leftContent, children }: any) => (
|
||||
<div data-testid="control-bar">
|
||||
{leftContent && <div data-testid="left-content">{leftContent}</div>}
|
||||
<div data-testid="children">{children}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('AdminToolbar', () => {
|
||||
it('should render with children only', () => {
|
||||
render(
|
||||
<AdminToolbar>
|
||||
<button>Filter</button>
|
||||
</AdminToolbar>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Filter')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with leftContent and children', () => {
|
||||
render(
|
||||
<AdminToolbar
|
||||
leftContent={<span>Left Content</span>}
|
||||
>
|
||||
<button>Filter</button>
|
||||
</AdminToolbar>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Left Content')).toBeTruthy();
|
||||
expect(screen.getByText('Filter')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with multiple children', () => {
|
||||
render(
|
||||
<AdminToolbar
|
||||
leftContent={<span>Filters</span>}
|
||||
>
|
||||
<button>Filter 1</button>
|
||||
<button>Filter 2</button>
|
||||
<button>Filter 3</button>
|
||||
</AdminToolbar>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Filters')).toBeTruthy();
|
||||
expect(screen.getByText('Filter 1')).toBeTruthy();
|
||||
expect(screen.getByText('Filter 2')).toBeTruthy();
|
||||
expect(screen.getByText('Filter 3')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with complex leftContent', () => {
|
||||
const ComplexLeftContent = () => (
|
||||
<div>
|
||||
<span>Complex</span>
|
||||
<button>Action</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminToolbar
|
||||
leftContent={<ComplexLeftContent />}
|
||||
>
|
||||
<button>Filter</button>
|
||||
</AdminToolbar>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Complex')).toBeTruthy();
|
||||
expect(screen.getByText('Action')).toBeTruthy();
|
||||
expect(screen.getByText('Filter')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with complex children', () => {
|
||||
const ComplexChild = () => (
|
||||
<div>
|
||||
<span>Complex</span>
|
||||
<button>Action</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<AdminToolbar
|
||||
leftContent={<span>Filters</span>}
|
||||
>
|
||||
<ComplexChild />
|
||||
</AdminToolbar>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Filters')).toBeTruthy();
|
||||
expect(screen.getByText('Complex')).toBeTruthy();
|
||||
expect(screen.getByText('Action')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with mixed content types', () => {
|
||||
render(
|
||||
<AdminToolbar
|
||||
leftContent={<span>Filters</span>}
|
||||
>
|
||||
<button>Button</button>
|
||||
<input type="text" placeholder="Search" />
|
||||
<select>
|
||||
<option>Option 1</option>
|
||||
</select>
|
||||
</AdminToolbar>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Filters')).toBeTruthy();
|
||||
expect(screen.getByText('Button')).toBeTruthy();
|
||||
expect(screen.getByPlaceholderText('Search')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render without leftContent', () => {
|
||||
render(
|
||||
<AdminToolbar>
|
||||
<button>Filter</button>
|
||||
</AdminToolbar>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Filter')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with empty children', () => {
|
||||
render(
|
||||
<AdminToolbar
|
||||
leftContent={<span>Filters</span>}
|
||||
>
|
||||
{null}
|
||||
</AdminToolbar>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Filters')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
361
apps/website/components/admin/AdminUsersTable.test.tsx
Normal file
361
apps/website/components/admin/AdminUsersTable.test.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* AdminUsersTable Component Tests
|
||||
*
|
||||
* Tests for the AdminUsersTable component that displays users in a table
|
||||
* with selection, status management, and deletion capabilities.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { AdminUsersTable } from './AdminUsersTable';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the DateDisplay component
|
||||
vi.mock('@/lib/display-objects/DateDisplay', () => ({
|
||||
DateDisplay: {
|
||||
formatShort: (date: string) => new Date(date).toLocaleDateString(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the AdminUsersViewData
|
||||
vi.mock('@/lib/view-data/AdminUsersViewData', () => ({
|
||||
AdminUsersViewData: {},
|
||||
}));
|
||||
|
||||
// Mock the Button component
|
||||
vi.mock('@/ui/Button', () => ({
|
||||
Button: ({ children, onClick, disabled }: any) => (
|
||||
<button onClick={onClick} disabled={disabled} data-testid="button">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the IconButton component
|
||||
vi.mock('@/ui/IconButton', () => ({
|
||||
IconButton: ({ onClick, disabled, icon, title }: any) => (
|
||||
<button onClick={onClick} disabled={disabled} data-testid="icon-button" title={title}>
|
||||
{title}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the SimpleCheckbox component
|
||||
vi.mock('@/ui/SimpleCheckbox', () => ({
|
||||
SimpleCheckbox: ({ checked, onChange, 'aria-label': ariaLabel }: any) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
aria-label={ariaLabel}
|
||||
data-testid="checkbox"
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Badge component
|
||||
vi.mock('@/ui/Badge', () => ({
|
||||
Badge: ({ children }: any) => <span data-testid="badge">{children}</span>,
|
||||
}));
|
||||
|
||||
// Mock the Box component
|
||||
vi.mock('@/ui/Box', () => ({
|
||||
Box: ({ children }: any) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock the Group component
|
||||
vi.mock('@/ui/Group', () => ({
|
||||
Group: ({ children }: any) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock the DriverIdentity component
|
||||
vi.mock('@/ui/DriverIdentity', () => ({
|
||||
DriverIdentity: ({ driver, meta }: any) => (
|
||||
<div data-testid="driver-identity">
|
||||
<span>{driver.name}</span>
|
||||
<span>{meta}</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Table components
|
||||
vi.mock('@/ui/Table', () => ({
|
||||
Table: ({ children }: any) => <table>{children}</table>,
|
||||
TableHead: ({ children }: any) => <thead>{children}</thead>,
|
||||
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
|
||||
TableHeader: ({ children, w, textAlign }: any) => <th style={{ width: w, textAlign }}>{children}</th>,
|
||||
TableRow: ({ children, variant }: any) => <tr data-variant={variant}>{children}</tr>,
|
||||
TableCell: ({ children }: any) => <td>{children}</td>,
|
||||
}));
|
||||
|
||||
// Mock the Text component
|
||||
vi.mock('@/ui/Text', () => ({
|
||||
Text: ({ children, size, variant }: any) => (
|
||||
<span data-size={size} data-variant={variant}>{children}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the UserStatusTag component
|
||||
vi.mock('./UserStatusTag', () => ({
|
||||
UserStatusTag: ({ status }: any) => <span data-testid="status-tag">{status}</span>,
|
||||
}));
|
||||
|
||||
describe('AdminUsersTable', () => {
|
||||
const mockUsers = [
|
||||
{
|
||||
id: '1',
|
||||
displayName: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
roles: ['admin'],
|
||||
status: 'active',
|
||||
lastLoginAt: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
displayName: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
roles: ['user'],
|
||||
status: 'suspended',
|
||||
lastLoginAt: '2024-01-14T15:45:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
displayName: 'Bob Johnson',
|
||||
email: 'bob@example.com',
|
||||
roles: ['user'],
|
||||
status: 'active',
|
||||
lastLoginAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
users: mockUsers,
|
||||
selectedUserIds: [],
|
||||
onSelectUser: vi.fn(),
|
||||
onSelectAll: vi.fn(),
|
||||
onUpdateStatus: vi.fn(),
|
||||
onDeleteUser: vi.fn(),
|
||||
deletingUserId: null,
|
||||
};
|
||||
|
||||
it('should render table headers', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('User')).toBeTruthy();
|
||||
expect(screen.getByText('Roles')).toBeTruthy();
|
||||
expect(screen.getByText('Status')).toBeTruthy();
|
||||
expect(screen.getByText('Last Login')).toBeTruthy();
|
||||
expect(screen.getByText('Actions')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render user rows', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('John Doe')).toBeTruthy();
|
||||
expect(screen.getByText('john@example.com')).toBeTruthy();
|
||||
expect(screen.getByText('Jane Smith')).toBeTruthy();
|
||||
expect(screen.getByText('jane@example.com')).toBeTruthy();
|
||||
expect(screen.getByText('Bob Johnson')).toBeTruthy();
|
||||
expect(screen.getByText('bob@example.com')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render user roles', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('admin')).toBeTruthy();
|
||||
expect(screen.getByText('user')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render user status tags', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getAllByTestId('status-tag')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render last login dates', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('1/15/2024')).toBeTruthy();
|
||||
expect(screen.getByText('1/14/2024')).toBeTruthy();
|
||||
expect(screen.getByText('Never')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render select all checkbox', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByLabelText('Select all users')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render individual user checkboxes', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByLabelText('Select user John Doe')).toBeTruthy();
|
||||
expect(screen.getByLabelText('Select user Jane Smith')).toBeTruthy();
|
||||
expect(screen.getByLabelText('Select user Bob Johnson')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render suspend button for active users', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Suspend')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render activate button for suspended users', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Activate')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render delete button for all users', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getAllByTitle('Delete')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render more button for all users', () => {
|
||||
render(<AdminUsersTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getAllByTitle('More')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should highlight selected rows', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedUserIds: ['1', '3'],
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
// Check that selected rows have highlight variant
|
||||
const rows = screen.getAllByRole('row');
|
||||
expect(rows[1]).toHaveAttribute('data-variant', 'highlight');
|
||||
expect(rows[3]).toHaveAttribute('data-variant', 'highlight');
|
||||
});
|
||||
|
||||
it('should disable delete button when deleting', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
deletingUserId: '1',
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
const deleteButtons = screen.getAllByTitle('Delete');
|
||||
expect(deleteButtons[0]).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should call onSelectUser when checkbox is clicked', () => {
|
||||
const onSelectUser = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onSelectUser,
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
const checkboxes = screen.getAllByTestId('checkbox');
|
||||
fireEvent.click(checkboxes[1]); // Click first user checkbox
|
||||
|
||||
expect(onSelectUser).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('should call onSelectAll when select all checkbox is clicked', () => {
|
||||
const onSelectAll = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onSelectAll,
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
const selectAllCheckbox = screen.getByLabelText('Select all users');
|
||||
fireEvent.click(selectAllCheckbox);
|
||||
|
||||
expect(onSelectAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onUpdateStatus when suspend button is clicked', () => {
|
||||
const onUpdateStatus = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onUpdateStatus,
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
const suspendButtons = screen.getAllByText('Suspend');
|
||||
fireEvent.click(suspendButtons[0]);
|
||||
|
||||
expect(onUpdateStatus).toHaveBeenCalledWith('1', 'suspended');
|
||||
});
|
||||
|
||||
it('should call onUpdateStatus when activate button is clicked', () => {
|
||||
const onUpdateStatus = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onUpdateStatus,
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
const activateButtons = screen.getAllByText('Activate');
|
||||
fireEvent.click(activateButtons[0]);
|
||||
|
||||
expect(onUpdateStatus).toHaveBeenCalledWith('2', 'active');
|
||||
});
|
||||
|
||||
it('should call onDeleteUser when delete button is clicked', () => {
|
||||
const onDeleteUser = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onDeleteUser,
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
const deleteButtons = screen.getAllByTitle('Delete');
|
||||
fireEvent.click(deleteButtons[0]);
|
||||
|
||||
expect(onDeleteUser).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('should render empty table when no users', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
users: [],
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
// Should render table headers but no rows
|
||||
expect(screen.getByText('User')).toBeTruthy();
|
||||
expect(screen.getByText('Roles')).toBeTruthy();
|
||||
expect(screen.getByText('Status')).toBeTruthy();
|
||||
expect(screen.getByText('Last Login')).toBeTruthy();
|
||||
expect(screen.getByText('Actions')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with all users selected', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedUserIds: ['1', '2', '3'],
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
const selectAllCheckbox = screen.getByLabelText('Select all users');
|
||||
expect(selectAllCheckbox).toBeChecked();
|
||||
});
|
||||
|
||||
it('should render with some users selected', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedUserIds: ['1', '2'],
|
||||
};
|
||||
|
||||
render(<AdminUsersTable {...props} />);
|
||||
|
||||
const selectAllCheckbox = screen.getByLabelText('Select all users');
|
||||
expect(selectAllCheckbox).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
255
apps/website/components/admin/BulkActionBar.test.tsx
Normal file
255
apps/website/components/admin/BulkActionBar.test.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* BulkActionBar Component Tests
|
||||
*
|
||||
* Tests for the BulkActionBar component that displays a floating action bar
|
||||
* when items are selected in a table.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { BulkActionBar } from './BulkActionBar';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the Button component
|
||||
vi.mock('@/ui/Button', () => ({
|
||||
Button: ({ children, onClick, variant, size, icon }: any) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-testid="button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the BulkActions component
|
||||
vi.mock('@/ui/BulkActions', () => ({
|
||||
BulkActions: ({ selectedCount, isOpen, children }: any) => (
|
||||
<div data-testid="bulk-actions" data-open={isOpen} data-count={selectedCount}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('BulkActionBar', () => {
|
||||
const defaultProps = {
|
||||
selectedCount: 0,
|
||||
actions: [],
|
||||
onClearSelection: vi.fn(),
|
||||
};
|
||||
|
||||
it('should not render when no items selected', () => {
|
||||
render(<BulkActionBar {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByTestId('bulk-actions')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render when items are selected', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 3,
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByTestId('bulk-actions')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display selected count', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 5,
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByTestId('bulk-actions')).toHaveAttribute('data-count', '5');
|
||||
});
|
||||
|
||||
it('should render with single action', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 2,
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: vi.fn(),
|
||||
variant: 'danger' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByText('Delete')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with multiple actions', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 3,
|
||||
actions: [
|
||||
{
|
||||
label: 'Export',
|
||||
onClick: vi.fn(),
|
||||
variant: 'primary' as const,
|
||||
},
|
||||
{
|
||||
label: 'Archive',
|
||||
onClick: vi.fn(),
|
||||
variant: 'secondary' as const,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: vi.fn(),
|
||||
variant: 'danger' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByText('Export')).toBeTruthy();
|
||||
expect(screen.getByText('Archive')).toBeTruthy();
|
||||
expect(screen.getByText('Delete')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render cancel button', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 2,
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: vi.fn(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByText('Cancel')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call action onClick when clicked', () => {
|
||||
const actionOnClick = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 2,
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: actionOnClick,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
const deleteButton = screen.getByText('Delete');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(actionOnClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onClearSelection when cancel is clicked', () => {
|
||||
const onClearSelection = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 2,
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: vi.fn(),
|
||||
},
|
||||
],
|
||||
onClearSelection,
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
const cancelButton = screen.getByText('Cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(onClearSelection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render actions with different variants', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 2,
|
||||
actions: [
|
||||
{
|
||||
label: 'Primary',
|
||||
onClick: vi.fn(),
|
||||
variant: 'primary' as const,
|
||||
},
|
||||
{
|
||||
label: 'Secondary',
|
||||
onClick: vi.fn(),
|
||||
variant: 'secondary' as const,
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
onClick: vi.fn(),
|
||||
variant: 'danger' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByText('Primary')).toBeTruthy();
|
||||
expect(screen.getByText('Secondary')).toBeTruthy();
|
||||
expect(screen.getByText('Danger')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render actions without variant (defaults to primary)', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 2,
|
||||
actions: [
|
||||
{
|
||||
label: 'Default',
|
||||
onClick: vi.fn(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByText('Default')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with empty actions array', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 2,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByTestId('bulk-actions')).toBeTruthy();
|
||||
expect(screen.getByText('Cancel')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with large selected count', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
selectedCount: 100,
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: vi.fn(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<BulkActionBar {...props} />);
|
||||
|
||||
expect(screen.getByTestId('bulk-actions')).toHaveAttribute('data-count', '100');
|
||||
});
|
||||
});
|
||||
297
apps/website/components/admin/UserFilters.test.tsx
Normal file
297
apps/website/components/admin/UserFilters.test.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* UserFilters Component Tests
|
||||
*
|
||||
* Tests for the UserFilters component that provides search and filter
|
||||
* functionality for user management.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { UserFilters } from './UserFilters';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the Button component
|
||||
vi.mock('@/ui/Button', () => ({
|
||||
Button: ({ children, onClick, variant, size }: any) => (
|
||||
<button onClick={onClick} data-variant={variant} data-size={size} data-testid="button">
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Icon component
|
||||
vi.mock('@/ui/Icon', () => ({
|
||||
Icon: ({ icon, size, intent }: any) => (
|
||||
<span data-testid="icon" data-size={size} data-intent={intent}>Icon</span>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Input component
|
||||
vi.mock('@/ui/Input', () => ({
|
||||
Input: ({ type, placeholder, value, onChange, fullWidth }: any) => (
|
||||
<input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
data-full-width={fullWidth}
|
||||
data-testid="input"
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Select component
|
||||
vi.mock('@/ui/Select', () => ({
|
||||
Select: ({ value, onChange, options }: any) => (
|
||||
<select value={value} onChange={onChange} data-testid="select">
|
||||
{options.map((opt: any) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Text component
|
||||
vi.mock('@/ui/Text', () => ({
|
||||
Text: ({ children, weight, variant }: any) => (
|
||||
<span data-weight={weight} data-variant={variant}>{children}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the Box component
|
||||
vi.mock('@/ui/Box', () => ({
|
||||
Box: ({ children, width }: any) => <div data-width={width}>{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock the Group component
|
||||
vi.mock('@/ui/Group', () => ({
|
||||
Group: ({ children, gap }: any) => <div data-gap={gap}>{children}</div>,
|
||||
}));
|
||||
|
||||
// Mock the AdminToolbar component
|
||||
vi.mock('./AdminToolbar', () => ({
|
||||
AdminToolbar: ({ leftContent, children }: any) => (
|
||||
<div data-testid="admin-toolbar">
|
||||
{leftContent && <div data-testid="left-content">{leftContent}</div>}
|
||||
<div data-testid="children">{children}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('UserFilters', () => {
|
||||
const defaultProps = {
|
||||
search: '',
|
||||
roleFilter: '',
|
||||
statusFilter: '',
|
||||
onSearch: vi.fn(),
|
||||
onFilterRole: vi.fn(),
|
||||
onFilterStatus: vi.fn(),
|
||||
onClearFilters: vi.fn(),
|
||||
};
|
||||
|
||||
it('should render search input', () => {
|
||||
render(<UserFilters {...defaultProps} />);
|
||||
|
||||
expect(screen.getByPlaceholderText('Search by email or name...')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render role filter select', () => {
|
||||
render(<UserFilters {...defaultProps} />);
|
||||
|
||||
const selects = screen.getAllByTestId('select');
|
||||
expect(selects[0]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render status filter select', () => {
|
||||
render(<UserFilters {...defaultProps} />);
|
||||
|
||||
const selects = screen.getAllByTestId('select');
|
||||
expect(selects[1]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render filter icon and label', () => {
|
||||
render(<UserFilters {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Filters')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render clear all button when filters are applied', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
search: 'test',
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
expect(screen.getByText('Clear all')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render clear all button when no filters are applied', () => {
|
||||
render(<UserFilters {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByText('Clear all')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should call onSearch when search input changes', () => {
|
||||
const onSearch = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onSearch,
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search by email or name...');
|
||||
fireEvent.change(searchInput, { target: { value: 'john' } });
|
||||
|
||||
expect(onSearch).toHaveBeenCalledWith('john');
|
||||
});
|
||||
|
||||
it('should call onFilterRole when role select changes', () => {
|
||||
const onFilterRole = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onFilterRole,
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
const roleSelect = screen.getAllByTestId('select')[0];
|
||||
fireEvent.change(roleSelect, { target: { value: 'admin' } });
|
||||
|
||||
expect(onFilterRole).toHaveBeenCalledWith('admin');
|
||||
});
|
||||
|
||||
it('should call onFilterStatus when status select changes', () => {
|
||||
const onFilterStatus = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onFilterStatus,
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
const statusSelect = screen.getAllByTestId('select')[1];
|
||||
fireEvent.change(statusSelect, { target: { value: 'active' } });
|
||||
|
||||
expect(onFilterStatus).toHaveBeenCalledWith('active');
|
||||
});
|
||||
|
||||
it('should call onClearFilters when clear all button is clicked', () => {
|
||||
const onClearFilters = vi.fn();
|
||||
const props = {
|
||||
...defaultProps,
|
||||
search: 'test',
|
||||
onClearFilters,
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
const clearButton = screen.getByText('Clear all');
|
||||
fireEvent.click(clearButton);
|
||||
|
||||
expect(onClearFilters).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should display current search value', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
search: 'john@example.com',
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search by email or name...');
|
||||
expect(searchInput).toHaveValue('john@example.com');
|
||||
});
|
||||
|
||||
it('should display current role filter value', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
roleFilter: 'admin',
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
const roleSelect = screen.getAllByTestId('select')[0];
|
||||
expect(roleSelect).toHaveValue('admin');
|
||||
});
|
||||
|
||||
it('should display current status filter value', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
statusFilter: 'suspended',
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
const statusSelect = screen.getAllByTestId('select')[1];
|
||||
expect(statusSelect).toHaveValue('suspended');
|
||||
});
|
||||
|
||||
it('should render all role options', () => {
|
||||
render(<UserFilters {...defaultProps} />);
|
||||
|
||||
const roleSelect = screen.getAllByTestId('select')[0];
|
||||
expect(roleSelect).toHaveTextContent('All Roles');
|
||||
expect(roleSelect).toHaveTextContent('Owner');
|
||||
expect(roleSelect).toHaveTextContent('Admin');
|
||||
expect(roleSelect).toHaveTextContent('User');
|
||||
});
|
||||
|
||||
it('should render all status options', () => {
|
||||
render(<UserFilters {...defaultProps} />);
|
||||
|
||||
const statusSelect = screen.getAllByTestId('select')[1];
|
||||
expect(statusSelect).toHaveTextContent('All Status');
|
||||
expect(statusSelect).toHaveTextContent('Active');
|
||||
expect(statusSelect).toHaveTextContent('Suspended');
|
||||
expect(statusSelect).toHaveTextContent('Deleted');
|
||||
});
|
||||
|
||||
it('should render clear button when only search is applied', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
search: 'test',
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
expect(screen.getByText('Clear all')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render clear button when only role filter is applied', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
roleFilter: 'admin',
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
expect(screen.getByText('Clear all')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render clear button when only status filter is applied', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
statusFilter: 'active',
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
expect(screen.getByText('Clear all')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render clear button when all filters are applied', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
search: 'test',
|
||||
roleFilter: 'admin',
|
||||
statusFilter: 'active',
|
||||
};
|
||||
|
||||
render(<UserFilters {...props} />);
|
||||
|
||||
expect(screen.getByText('Clear all')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
172
apps/website/components/admin/UserStatsSummary.test.tsx
Normal file
172
apps/website/components/admin/UserStatsSummary.test.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* UserStatsSummary Component Tests
|
||||
*
|
||||
* Tests for the UserStatsSummary component that displays summary statistics
|
||||
* for user management.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { UserStatsSummary } from './UserStatsSummary';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the MetricCard component
|
||||
vi.mock('@/ui/MetricCard', () => ({
|
||||
MetricCard: ({ label, value, icon, intent }: any) => (
|
||||
<div data-testid="metric-card" data-intent={intent}>
|
||||
<span data-testid="label">{label}</span>
|
||||
<span data-testid="value">{value}</span>
|
||||
{icon && <span data-testid="icon">Icon</span>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the StatGrid component
|
||||
vi.mock('@/ui/StatGrid', () => ({
|
||||
StatGrid: ({ stats, columns }: any) => (
|
||||
<div data-testid="stat-grid" data-columns={columns}>
|
||||
{stats.map((stat: any, index: number) => (
|
||||
<div key={index} data-testid={`stat-${index}`}>
|
||||
<span>{stat.label}</span>
|
||||
<span>{stat.value}</span>
|
||||
{stat.icon && <span>Icon</span>}
|
||||
{stat.intent && <span data-intent={stat.intent}>{stat.intent}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('UserStatsSummary', () => {
|
||||
it('should render with all stats', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={100}
|
||||
activeCount={80}
|
||||
adminCount={10}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Total Users')).toBeTruthy();
|
||||
expect(screen.getByText('100')).toBeTruthy();
|
||||
expect(screen.getByText('Active')).toBeTruthy();
|
||||
expect(screen.getByText('80')).toBeTruthy();
|
||||
expect(screen.getByText('Admins')).toBeTruthy();
|
||||
expect(screen.getByText('10')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with zero values', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={0}
|
||||
activeCount={0}
|
||||
adminCount={0}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Total Users')).toBeTruthy();
|
||||
expect(screen.getByText('0')).toBeTruthy();
|
||||
expect(screen.getByText('Active')).toBeTruthy();
|
||||
expect(screen.getByText('0')).toBeTruthy();
|
||||
expect(screen.getByText('Admins')).toBeTruthy();
|
||||
expect(screen.getByText('0')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with large numbers', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={12345}
|
||||
activeCount={9876}
|
||||
adminCount={123}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('12345')).toBeTruthy();
|
||||
expect(screen.getByText('9876')).toBeTruthy();
|
||||
expect(screen.getByText('123')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with single digit numbers', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={5}
|
||||
activeCount={3}
|
||||
adminCount={1}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('5')).toBeTruthy();
|
||||
expect(screen.getByText('3')).toBeTruthy();
|
||||
expect(screen.getByText('1')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with negative numbers (edge case)', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={-5}
|
||||
activeCount={-3}
|
||||
adminCount={-1}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('-5')).toBeTruthy();
|
||||
expect(screen.getByText('-3')).toBeTruthy();
|
||||
expect(screen.getByText('-1')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with decimal numbers', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={100.5}
|
||||
activeCount={75.25}
|
||||
adminCount={10.75}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('100.5')).toBeTruthy();
|
||||
expect(screen.getByText('75.25')).toBeTruthy();
|
||||
expect(screen.getByText('10.75')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with very large numbers', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={1000000}
|
||||
activeCount={750000}
|
||||
adminCount={50000}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('1000000')).toBeTruthy();
|
||||
expect(screen.getByText('750000')).toBeTruthy();
|
||||
expect(screen.getByText('50000')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with string numbers', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={100}
|
||||
activeCount={80}
|
||||
adminCount={10}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('100')).toBeTruthy();
|
||||
expect(screen.getByText('80')).toBeTruthy();
|
||||
expect(screen.getByText('10')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with mixed number types', () => {
|
||||
render(
|
||||
<UserStatsSummary
|
||||
total={100}
|
||||
activeCount={80}
|
||||
adminCount={10}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('100')).toBeTruthy();
|
||||
expect(screen.getByText('80')).toBeTruthy();
|
||||
expect(screen.getByText('10')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
118
apps/website/components/admin/UserStatusTag.test.tsx
Normal file
118
apps/website/components/admin/UserStatusTag.test.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* UserStatusTag Component Tests
|
||||
*
|
||||
* Tests for the UserStatusTag component that displays user status
|
||||
* with appropriate visual variants and icons.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { UserStatusTag } from './UserStatusTag';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the StatusBadge component
|
||||
vi.mock('@/ui/StatusBadge', () => ({
|
||||
StatusBadge: ({ variant, icon, children }: any) => (
|
||||
<div data-testid="status-badge" data-variant={variant}>
|
||||
{icon && <span data-testid="icon">Icon</span>}
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('UserStatusTag', () => {
|
||||
it('should render active status with success variant', () => {
|
||||
render(<UserStatusTag status="active" />);
|
||||
|
||||
expect(screen.getByText('Active')).toBeTruthy();
|
||||
expect(screen.getByTestId('status-badge')).toHaveAttribute('data-variant', 'success');
|
||||
});
|
||||
|
||||
it('should render suspended status with warning variant', () => {
|
||||
render(<UserStatusTag status="suspended" />);
|
||||
|
||||
expect(screen.getByText('Suspended')).toBeTruthy();
|
||||
expect(screen.getByTestId('status-badge')).toHaveAttribute('data-variant', 'warning');
|
||||
});
|
||||
|
||||
it('should render deleted status with error variant', () => {
|
||||
render(<UserStatusTag status="deleted" />);
|
||||
|
||||
expect(screen.getByText('Deleted')).toBeTruthy();
|
||||
expect(screen.getByTestId('status-badge')).toHaveAttribute('data-variant', 'error');
|
||||
});
|
||||
|
||||
it('should render pending status with pending variant', () => {
|
||||
render(<UserStatusTag status="pending" />);
|
||||
|
||||
expect(screen.getByText('Pending')).toBeTruthy();
|
||||
expect(screen.getByTestId('status-badge')).toHaveAttribute('data-variant', 'pending');
|
||||
});
|
||||
|
||||
it('should render unknown status with neutral variant', () => {
|
||||
render(<UserStatusTag status="unknown" />);
|
||||
|
||||
expect(screen.getByText('unknown')).toBeTruthy();
|
||||
expect(screen.getByTestId('status-badge')).toHaveAttribute('data-variant', 'neutral');
|
||||
});
|
||||
|
||||
it('should render uppercase status', () => {
|
||||
render(<UserStatusTag status="ACTIVE" />);
|
||||
|
||||
expect(screen.getByText('Active')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render mixed case status', () => {
|
||||
render(<UserStatusTag status="AcTiVe" />);
|
||||
|
||||
expect(screen.getByText('Active')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with special characters in status', () => {
|
||||
render(<UserStatusTag status="active-" />);
|
||||
|
||||
expect(screen.getByText('active-')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with empty status', () => {
|
||||
render(<UserStatusTag status="" />);
|
||||
|
||||
expect(screen.getByText('')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with numeric status', () => {
|
||||
render(<UserStatusTag status="123" />);
|
||||
|
||||
expect(screen.getByText('123')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with status containing spaces', () => {
|
||||
render(<UserStatusTag status="active user" />);
|
||||
|
||||
expect(screen.getByText('active user')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with status containing special characters', () => {
|
||||
render(<UserStatusTag status="active-user" />);
|
||||
|
||||
expect(screen.getByText('active-user')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with very long status', () => {
|
||||
render(<UserStatusTag status="this-is-a-very-long-status-that-might-wrap-to-multiple-lines" />);
|
||||
|
||||
expect(screen.getByText(/this-is-a-very-long-status/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with unicode characters in status', () => {
|
||||
render(<UserStatusTag status="active✓" />);
|
||||
|
||||
expect(screen.getByText('active✓')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with emoji in status', () => {
|
||||
render(<UserStatusTag status="active 🚀" />);
|
||||
|
||||
expect(screen.getByText('active 🚀')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user