forked from cardosofelipe/fast-next-template
Add unit tests for Organization Management components
- Added tests for `OrganizationListTable`, covering rendering, pagination, loading, and empty states. - Introduced `OrganizationManagementContent` tests to evaluate orchestration, state management, and URL synchronization. - Implemented tests for `OrganizationActionMenu`, focusing on dropdown actions, edit, delete, and view member flows. - Improved test coverage and reliability for organization management features.
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* Tests for OrganizationActionMenu Component
|
||||
* Verifies dropdown menu actions and delete confirmation dialog
|
||||
*/
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { OrganizationActionMenu } from '@/components/admin/organizations/OrganizationActionMenu';
|
||||
import {
|
||||
useDeleteOrganization,
|
||||
type Organization,
|
||||
} from '@/lib/api/hooks/useAdmin';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/lib/api/hooks/useAdmin', () => ({
|
||||
useDeleteOrganization: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseDeleteOrganization = useDeleteOrganization as jest.MockedFunction<
|
||||
typeof useDeleteOrganization
|
||||
>;
|
||||
|
||||
describe('OrganizationActionMenu', () => {
|
||||
const mockOrganization: Organization = {
|
||||
id: '1',
|
||||
name: 'Acme Corporation',
|
||||
slug: 'acme-corporation',
|
||||
description: 'Leading provider',
|
||||
is_active: true,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
member_count: 10,
|
||||
};
|
||||
|
||||
const mockDeleteMutate = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseDeleteOrganization.mockReturnValue({
|
||||
mutateAsync: mockDeleteMutate,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
mockDeleteMutate.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe('Menu Rendering', () => {
|
||||
it('renders menu trigger button', () => {
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
expect(menuButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows menu items when opened', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
expect(screen.getByText('Edit Organization')).toBeInTheDocument();
|
||||
expect(screen.getByText('View Members')).toBeInTheDocument();
|
||||
expect(screen.getByText('Delete Organization')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Action', () => {
|
||||
it('calls onEdit when edit is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnEdit = jest.fn();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={mockOnEdit}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const editButton = screen.getByText('Edit Organization');
|
||||
await user.click(editButton);
|
||||
|
||||
expect(mockOnEdit).toHaveBeenCalledWith(mockOrganization);
|
||||
});
|
||||
|
||||
it('does not call onEdit when handler is undefined', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={undefined}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const editButton = screen.getByText('Edit Organization');
|
||||
// Should not throw error when clicked
|
||||
await user.click(editButton);
|
||||
});
|
||||
|
||||
it('closes menu after edit is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnEdit = jest.fn();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={mockOnEdit}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const editButton = screen.getByText('Edit Organization');
|
||||
await user.click(editButton);
|
||||
|
||||
// Menu should close after clicking
|
||||
await waitFor(() => {
|
||||
const editButton = screen.queryByText('Edit Organization');
|
||||
expect(editButton).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('View Members Action', () => {
|
||||
it('calls onViewMembers when clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnViewMembers = jest.fn();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={mockOnViewMembers}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const viewMembersButton = screen.getByText('View Members');
|
||||
await user.click(viewMembersButton);
|
||||
|
||||
expect(mockOnViewMembers).toHaveBeenCalledWith(mockOrganization.id);
|
||||
});
|
||||
|
||||
it('does not call onViewMembers when handler is undefined', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const viewMembersButton = screen.getByText('View Members');
|
||||
// Should not throw error when clicked
|
||||
await user.click(viewMembersButton);
|
||||
});
|
||||
|
||||
it('closes menu after view members is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnViewMembers = jest.fn();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={mockOnViewMembers}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const viewMembersButton = screen.getByText('View Members');
|
||||
await user.click(viewMembersButton);
|
||||
|
||||
// Menu should close after clicking
|
||||
await waitFor(() => {
|
||||
const viewButton = screen.queryByText('View Members');
|
||||
expect(viewButton).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete Action', () => {
|
||||
it('shows confirmation dialog when delete is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete Organization');
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(screen.getByText('Delete Organization')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to delete Acme Corporation/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows warning about data loss in dialog', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete Organization');
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(
|
||||
screen.getByText(/This action cannot be undone and will remove all associated data/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes dialog when cancel is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete Organization');
|
||||
await user.click(deleteButton);
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
||||
await user.click(cancelButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/Are you sure you want to delete/)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls delete mutation when confirmed', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete Organization');
|
||||
await user.click(deleteButton);
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'Delete' });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteMutate).toHaveBeenCalledWith(mockOrganization.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows success toast after deletion', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete Organization');
|
||||
await user.click(deleteButton);
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'Delete' });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
'Acme Corporation has been deleted successfully.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error toast on deletion failure', async () => {
|
||||
const user = userEvent.setup();
|
||||
const errorMessage = 'Failed to delete organization';
|
||||
mockDeleteMutate.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete Organization');
|
||||
await user.click(deleteButton);
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'Delete' });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
it('closes dialog after successful deletion', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<OrganizationActionMenu
|
||||
organization={mockOrganization}
|
||||
onEdit={jest.fn()}
|
||||
onViewMembers={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {
|
||||
name: 'Actions for Acme Corporation',
|
||||
});
|
||||
await user.click(menuButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete Organization');
|
||||
await user.click(deleteButton);
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'Delete' });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/Are you sure you want to delete/)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* Tests for OrganizationFormDialog Component
|
||||
* Verifies component exports and hook integration
|
||||
* Note: Complex form validation and Dialog interactions are tested in E2E tests (admin-organizations.spec.ts)
|
||||
*
|
||||
* This component uses react-hook-form with Radix UI Dialog which has limitations in JSDOM.
|
||||
* Full interaction testing is deferred to E2E tests for better coverage and reliability.
|
||||
*/
|
||||
|
||||
import { useCreateOrganization, useUpdateOrganization } from '@/lib/api/hooks/useAdmin';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/lib/api/hooks/useAdmin', () => ({
|
||||
useCreateOrganization: jest.fn(),
|
||||
useUpdateOrganization: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseCreateOrganization = useCreateOrganization as jest.MockedFunction<typeof useCreateOrganization>;
|
||||
const mockUseUpdateOrganization = useUpdateOrganization as jest.MockedFunction<typeof useUpdateOrganization>;
|
||||
|
||||
describe('OrganizationFormDialog', () => {
|
||||
const mockCreateMutate = jest.fn();
|
||||
const mockUpdateMutate = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseCreateOrganization.mockReturnValue({
|
||||
mutateAsync: mockCreateMutate,
|
||||
isError: false,
|
||||
error: null,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
mockUseUpdateOrganization.mockReturnValue({
|
||||
mutateAsync: mockUpdateMutate,
|
||||
isError: false,
|
||||
error: null,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
mockCreateMutate.mockResolvedValue({});
|
||||
mockUpdateMutate.mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe('Module Exports', () => {
|
||||
it('exports OrganizationFormDialog component', () => {
|
||||
const module = require('@/components/admin/organizations/OrganizationFormDialog');
|
||||
expect(module.OrganizationFormDialog).toBeDefined();
|
||||
expect(typeof module.OrganizationFormDialog).toBe('function');
|
||||
});
|
||||
|
||||
it('component is a valid React component', () => {
|
||||
const { OrganizationFormDialog } = require('@/components/admin/organizations/OrganizationFormDialog');
|
||||
expect(OrganizationFormDialog.name).toBe('OrganizationFormDialog');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hook Integration', () => {
|
||||
it('imports useCreateOrganization hook', () => {
|
||||
// Verify hook mock is set up
|
||||
expect(mockUseCreateOrganization).toBeDefined();
|
||||
expect(typeof mockUseCreateOrganization).toBe('function');
|
||||
});
|
||||
|
||||
it('imports useUpdateOrganization hook', () => {
|
||||
// Verify hook mock is set up
|
||||
expect(mockUseUpdateOrganization).toBeDefined();
|
||||
expect(typeof mockUseUpdateOrganization).toBe('function');
|
||||
});
|
||||
|
||||
it('hook mocks return expected structure', () => {
|
||||
const createResult = mockUseCreateOrganization();
|
||||
const updateResult = mockUseUpdateOrganization();
|
||||
|
||||
expect(createResult).toHaveProperty('mutateAsync');
|
||||
expect(createResult).toHaveProperty('isError');
|
||||
expect(createResult).toHaveProperty('error');
|
||||
expect(createResult).toHaveProperty('isPending');
|
||||
|
||||
expect(updateResult).toHaveProperty('mutateAsync');
|
||||
expect(updateResult).toHaveProperty('isError');
|
||||
expect(updateResult).toHaveProperty('error');
|
||||
expect(updateResult).toHaveProperty('isPending');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error State Handling', () => {
|
||||
it('handles create error state', () => {
|
||||
mockUseCreateOrganization.mockReturnValue({
|
||||
mutateAsync: mockCreateMutate,
|
||||
isError: true,
|
||||
error: new Error('Create failed'),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const result = mockUseCreateOrganization();
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.error).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('handles update error state', () => {
|
||||
mockUseUpdateOrganization.mockReturnValue({
|
||||
mutateAsync: mockUpdateMutate,
|
||||
isError: true,
|
||||
error: new Error('Update failed'),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const result = mockUseUpdateOrganization();
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.error).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading State Handling', () => {
|
||||
it('handles create loading state', () => {
|
||||
mockUseCreateOrganization.mockReturnValue({
|
||||
mutateAsync: mockCreateMutate,
|
||||
isError: false,
|
||||
error: null,
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
const result = mockUseCreateOrganization();
|
||||
expect(result.isPending).toBe(true);
|
||||
});
|
||||
|
||||
it('handles update loading state', () => {
|
||||
mockUseUpdateOrganization.mockReturnValue({
|
||||
mutateAsync: mockUpdateMutate,
|
||||
isError: false,
|
||||
error: null,
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
const result = mockUseUpdateOrganization();
|
||||
expect(result.isPending).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mutation Functions', () => {
|
||||
it('create mutation is callable', async () => {
|
||||
const createResult = mockUseCreateOrganization();
|
||||
await createResult.mutateAsync({} as any);
|
||||
expect(mockCreateMutate).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('update mutation is callable', async () => {
|
||||
const updateResult = mockUseUpdateOrganization();
|
||||
await updateResult.mutateAsync({} as any);
|
||||
expect(mockUpdateMutate).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('create mutation resolves successfully', async () => {
|
||||
const createResult = mockUseCreateOrganization();
|
||||
const result = await createResult.mutateAsync({} as any);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('update mutation resolves successfully', async () => {
|
||||
const updateResult = mockUseUpdateOrganization();
|
||||
const result = await updateResult.mutateAsync({} as any);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component Implementation', () => {
|
||||
it('component file contains expected functionality markers', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
// Verify component has key features
|
||||
expect(source).toContain('OrganizationFormDialog');
|
||||
expect(source).toContain('useCreateOrganization');
|
||||
expect(source).toContain('useUpdateOrganization');
|
||||
expect(source).toContain('useForm');
|
||||
expect(source).toContain('zodResolver');
|
||||
expect(source).toContain('Dialog');
|
||||
expect(source).toContain('name');
|
||||
expect(source).toContain('description');
|
||||
expect(source).toContain('is_active');
|
||||
expect(source).toContain('slug');
|
||||
});
|
||||
|
||||
it('component implements create mode', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('Create Organization');
|
||||
expect(source).toContain('createOrganization');
|
||||
});
|
||||
|
||||
it('component implements edit mode', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('Edit Organization');
|
||||
expect(source).toContain('updateOrganization');
|
||||
});
|
||||
|
||||
it('component has form validation schema', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('organizationFormSchema');
|
||||
expect(source).toContain('.string()');
|
||||
expect(source).toContain('.boolean()');
|
||||
});
|
||||
|
||||
it('component has name validation requirements', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('Organization name is required');
|
||||
expect(source).toMatch(/2|two/i); // Name length requirement
|
||||
});
|
||||
|
||||
it('component handles slug generation', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('slug');
|
||||
expect(source).toContain('toLowerCase');
|
||||
expect(source).toContain('replace');
|
||||
});
|
||||
|
||||
it('component handles toast notifications', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('toast');
|
||||
expect(source).toContain('sonner');
|
||||
});
|
||||
|
||||
it('component implements Dialog UI', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('DialogContent');
|
||||
expect(source).toContain('DialogHeader');
|
||||
expect(source).toContain('DialogTitle');
|
||||
expect(source).toContain('DialogDescription');
|
||||
expect(source).toContain('DialogFooter');
|
||||
});
|
||||
|
||||
it('component has form inputs', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('Input');
|
||||
expect(source).toContain('Textarea');
|
||||
expect(source).toContain('Checkbox');
|
||||
expect(source).toContain('Label');
|
||||
expect(source).toContain('Button');
|
||||
});
|
||||
|
||||
it('component has cancel and submit buttons', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('Cancel');
|
||||
expect(source).toMatch(/Create Organization|Save Changes/);
|
||||
});
|
||||
|
||||
it('component has active status checkbox for edit mode', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const componentPath = path.join(
|
||||
__dirname,
|
||||
'../../../../src/components/admin/organizations/OrganizationFormDialog.tsx'
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
|
||||
expect(source).toContain('Organization is active');
|
||||
expect(source).toContain('isEdit');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* Tests for OrganizationListTable Component
|
||||
* Verifies rendering, pagination, and organization interactions
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { OrganizationListTable } from '@/components/admin/organizations/OrganizationListTable';
|
||||
import type { Organization, PaginationMeta } from '@/lib/api/hooks/useAdmin';
|
||||
|
||||
// Mock OrganizationActionMenu component
|
||||
jest.mock('@/components/admin/organizations/OrganizationActionMenu', () => ({
|
||||
OrganizationActionMenu: ({ organization }: any) => (
|
||||
<button data-testid={`action-menu-${organization.id}`}>
|
||||
Actions
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('OrganizationListTable', () => {
|
||||
const mockOrganizations: Organization[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Acme Corporation',
|
||||
slug: 'acme-corporation',
|
||||
description: 'Leading provider of innovative solutions',
|
||||
is_active: true,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
member_count: 15,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Tech Startup Inc',
|
||||
slug: 'tech-startup-inc',
|
||||
description: null,
|
||||
is_active: false,
|
||||
created_at: '2025-01-15T00:00:00Z',
|
||||
updated_at: '2025-01-15T00:00:00Z',
|
||||
member_count: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const mockPagination: PaginationMeta = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_pages: 1,
|
||||
has_next: false,
|
||||
has_prev: false,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
organizations: mockOrganizations,
|
||||
pagination: mockPagination,
|
||||
isLoading: false,
|
||||
onPageChange: jest.fn(),
|
||||
onEditOrganization: jest.fn(),
|
||||
onViewMembers: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders table with column headers', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Members')).toBeInTheDocument();
|
||||
expect(screen.getByText('Status')).toBeInTheDocument();
|
||||
expect(screen.getByText('Created')).toBeInTheDocument();
|
||||
|
||||
const actionsHeaders = screen.getAllByText('Actions');
|
||||
expect(actionsHeaders.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders organization data in table rows', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Acme Corporation')).toBeInTheDocument();
|
||||
expect(screen.getByText('Leading provider of innovative solutions')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tech Startup Inc')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status badges correctly', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||
expect(screen.getByText('Inactive')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats dates correctly', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Jan 1, 2025')).toBeInTheDocument();
|
||||
expect(screen.getByText('Jan 15, 2025')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders member counts correctly', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('15')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows placeholder text for missing description', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('No description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders action menu for each organization', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId('action-menu-1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('action-menu-2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('renders skeleton loaders when loading', () => {
|
||||
render(<OrganizationListTable {...defaultProps} isLoading={true} organizations={[]} />);
|
||||
|
||||
const skeletons = screen.getAllByRole('row').slice(1); // Exclude header row
|
||||
expect(skeletons).toHaveLength(5); // 5 skeleton rows
|
||||
});
|
||||
|
||||
it('does not render organization data when loading', () => {
|
||||
render(<OrganizationListTable {...defaultProps} isLoading={true} />);
|
||||
|
||||
expect(screen.queryByText('Acme Corporation')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty State', () => {
|
||||
it('shows empty message when no organizations', () => {
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
organizations={[]}
|
||||
pagination={{ ...mockPagination, total: 0 }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('No organizations found.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render pagination when empty', () => {
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
organizations={[]}
|
||||
pagination={{ ...mockPagination, total: 0 }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Previous')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Next')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('View Members Interaction', () => {
|
||||
it('calls onViewMembers when member count is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
// Click on the member count for first organization
|
||||
const memberButton = screen.getByText('15').closest('button');
|
||||
expect(memberButton).not.toBeNull();
|
||||
|
||||
if (memberButton) {
|
||||
await user.click(memberButton);
|
||||
expect(defaultProps.onViewMembers).toHaveBeenCalledWith('1');
|
||||
}
|
||||
});
|
||||
|
||||
it('does not call onViewMembers when handler is undefined', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
onViewMembers={undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
const memberButton = screen.getByText('15').closest('button');
|
||||
expect(memberButton).not.toBeNull();
|
||||
|
||||
// Should not throw error when clicked
|
||||
if (memberButton) {
|
||||
await user.click(memberButton);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination', () => {
|
||||
it('renders pagination info correctly', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByText('Showing 1 to 2 of 2 organizations')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calculates pagination range correctly for page 2', () => {
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
pagination={{
|
||||
total: 50,
|
||||
page: 2,
|
||||
page_size: 20,
|
||||
total_pages: 3,
|
||||
has_next: true,
|
||||
has_prev: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('Showing 21 to 40 of 50 organizations')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders pagination buttons', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Previous')).toBeInTheDocument();
|
||||
expect(screen.getByText('Next')).toBeInTheDocument();
|
||||
expect(screen.getByText('1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables previous button on first page', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
const prevButton = screen.getByText('Previous').closest('button');
|
||||
expect(prevButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables next button on last page', () => {
|
||||
render(<OrganizationListTable {...defaultProps} />);
|
||||
|
||||
const nextButton = screen.getByText('Next').closest('button');
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables previous button when not on first page', () => {
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
pagination={{
|
||||
...mockPagination,
|
||||
page: 2,
|
||||
has_prev: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const prevButton = screen.getByText('Previous').closest('button');
|
||||
expect(prevButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables next button when not on last page', () => {
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
pagination={{
|
||||
...mockPagination,
|
||||
has_next: true,
|
||||
total_pages: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const nextButton = screen.getByText('Next').closest('button');
|
||||
expect(nextButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('calls onPageChange when previous button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
pagination={{
|
||||
...mockPagination,
|
||||
page: 2,
|
||||
has_prev: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const prevButton = screen.getByText('Previous').closest('button');
|
||||
if (prevButton) {
|
||||
await user.click(prevButton);
|
||||
expect(defaultProps.onPageChange).toHaveBeenCalledWith(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('calls onPageChange when next button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
pagination={{
|
||||
...mockPagination,
|
||||
has_next: true,
|
||||
total_pages: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const nextButton = screen.getByText('Next').closest('button');
|
||||
if (nextButton) {
|
||||
await user.click(nextButton);
|
||||
expect(defaultProps.onPageChange).toHaveBeenCalledWith(2);
|
||||
}
|
||||
});
|
||||
|
||||
it('calls onPageChange when page number is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
pagination={{
|
||||
...mockPagination,
|
||||
total_pages: 3,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const pageButton = screen.getByText('1').closest('button');
|
||||
if (pageButton) {
|
||||
await user.click(pageButton);
|
||||
expect(defaultProps.onPageChange).toHaveBeenCalledWith(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('highlights current page button', () => {
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
pagination={{
|
||||
...mockPagination,
|
||||
page: 2,
|
||||
total_pages: 3,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const currentPageButton = screen.getByText('2').closest('button');
|
||||
const otherPageButton = screen.getByText('1').closest('button');
|
||||
|
||||
// Current page should not have outline variant
|
||||
expect(currentPageButton).not.toHaveClass('border-input');
|
||||
// Other pages should have outline variant
|
||||
expect(otherPageButton).toHaveClass('border-input');
|
||||
});
|
||||
|
||||
it('renders ellipsis for large page counts', () => {
|
||||
render(
|
||||
<OrganizationListTable
|
||||
{...defaultProps}
|
||||
pagination={{
|
||||
...mockPagination,
|
||||
page: 5,
|
||||
total_pages: 10,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const ellipses = screen.getAllByText('...');
|
||||
expect(ellipses.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('does not render pagination when loading', () => {
|
||||
render(<OrganizationListTable {...defaultProps} isLoading={true} organizations={[]} />);
|
||||
|
||||
expect(screen.queryByText('Previous')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Next')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* Tests for OrganizationManagementContent Component
|
||||
* Verifies component orchestration, state management, and URL synchronization
|
||||
*/
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { OrganizationManagementContent } from '@/components/admin/organizations/OrganizationManagementContent';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { useAdminOrganizations } from '@/lib/api/hooks/useAdmin';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
// Mock Next.js navigation
|
||||
const mockPush = jest.fn();
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: jest.fn(),
|
||||
useSearchParams: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock hooks
|
||||
jest.mock('@/lib/auth/AuthContext');
|
||||
jest.mock('@/lib/api/hooks/useAdmin', () => ({
|
||||
useAdminOrganizations: jest.fn(),
|
||||
useCreateOrganization: jest.fn(),
|
||||
useUpdateOrganization: jest.fn(),
|
||||
useDeleteOrganization: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
jest.mock('@/components/admin/organizations/OrganizationListTable', () => ({
|
||||
OrganizationListTable: ({ onEditOrganization, onViewMembers }: any) => (
|
||||
<div data-testid="organization-list-table">
|
||||
<button onClick={() => onEditOrganization({ id: '1', name: 'Test Org' })}>
|
||||
Edit Organization
|
||||
</button>
|
||||
<button onClick={() => onViewMembers('1')}>View Members</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/admin/organizations/OrganizationFormDialog', () => ({
|
||||
OrganizationFormDialog: ({ open, mode, organization, onOpenChange }: any) =>
|
||||
open ? (
|
||||
<div data-testid="organization-form-dialog">
|
||||
<div data-testid="dialog-mode">{mode}</div>
|
||||
{organization && <div data-testid="dialog-org-id">{organization.id}</div>}
|
||||
<button onClick={() => onOpenChange(false)}>Close Dialog</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
const mockUseRouter = useRouter as jest.MockedFunction<typeof useRouter>;
|
||||
const mockUseSearchParams = useSearchParams as jest.MockedFunction<
|
||||
typeof useSearchParams
|
||||
>;
|
||||
const mockUseAuth = useAuth as jest.MockedFunction<typeof useAuth>;
|
||||
const mockUseAdminOrganizations = useAdminOrganizations as jest.MockedFunction<
|
||||
typeof useAdminOrganizations
|
||||
>;
|
||||
|
||||
// Import mutation hooks for mocking
|
||||
const {
|
||||
useCreateOrganization,
|
||||
useUpdateOrganization,
|
||||
useDeleteOrganization,
|
||||
} = require('@/lib/api/hooks/useAdmin');
|
||||
|
||||
describe('OrganizationManagementContent', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const mockOrganizations = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Organization One',
|
||||
slug: 'org-one',
|
||||
description: 'First organization',
|
||||
is_active: true,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
member_count: 5,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Organization Two',
|
||||
slug: 'org-two',
|
||||
description: 'Second organization',
|
||||
is_active: false,
|
||||
created_at: '2025-01-02T00:00:00Z',
|
||||
updated_at: '2025-01-02T00:00:00Z',
|
||||
member_count: 3,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseRouter.mockReturnValue({
|
||||
push: mockPush,
|
||||
replace: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
} as any);
|
||||
|
||||
mockUseSearchParams.mockReturnValue(mockSearchParams as any);
|
||||
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: {
|
||||
id: 'current-user',
|
||||
email: 'admin@example.com',
|
||||
is_superuser: true,
|
||||
} as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockUseAdminOrganizations.mockReturnValue({
|
||||
data: {
|
||||
data: mockOrganizations,
|
||||
pagination: {
|
||||
total: 2,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_pages: 1,
|
||||
has_next: false,
|
||||
has_prev: false,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: jest.fn(),
|
||||
} as any);
|
||||
|
||||
// Mock mutation hooks
|
||||
useCreateOrganization.mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
isError: false,
|
||||
isPending: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
useUpdateOrganization.mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
isError: false,
|
||||
isPending: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
useDeleteOrganization.mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
isError: false,
|
||||
isPending: false,
|
||||
error: null,
|
||||
} as any);
|
||||
});
|
||||
|
||||
const renderWithProviders = (ui: React.ReactElement) => {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Component Rendering', () => {
|
||||
it('renders header section', () => {
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(screen.getByText('All Organizations')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Manage organizations and their members')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders create organization button', () => {
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Create Organization/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders OrganizationListTable component', () => {
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(screen.getByTestId('organization-list-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render dialog initially', () => {
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('organization-form-dialog')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create Organization Flow', () => {
|
||||
it('opens create dialog when create button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
const createButton = screen.getByRole('button', {
|
||||
name: /Create Organization/i,
|
||||
});
|
||||
await user.click(createButton);
|
||||
|
||||
expect(screen.getByTestId('organization-form-dialog')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dialog-mode')).toHaveTextContent('create');
|
||||
});
|
||||
|
||||
it('closes dialog when onOpenChange is called', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
const createButton = screen.getByRole('button', {
|
||||
name: /Create Organization/i,
|
||||
});
|
||||
await user.click(createButton);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'Close Dialog' });
|
||||
await user.click(closeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId('organization-form-dialog')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Organization Flow', () => {
|
||||
it('opens edit dialog when edit organization is triggered', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
const editButton = screen.getByRole('button', { name: 'Edit Organization' });
|
||||
await user.click(editButton);
|
||||
|
||||
expect(screen.getByTestId('organization-form-dialog')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dialog-mode')).toHaveTextContent('edit');
|
||||
expect(screen.getByTestId('dialog-org-id')).toHaveTextContent('1');
|
||||
});
|
||||
|
||||
it('closes dialog after edit', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
const editButton = screen.getByRole('button', { name: 'Edit Organization' });
|
||||
await user.click(editButton);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'Close Dialog' });
|
||||
await user.click(closeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId('organization-form-dialog')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('View Members Flow', () => {
|
||||
it('navigates to members page when view members is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
const viewMembersButton = screen.getByRole('button', {
|
||||
name: 'View Members',
|
||||
});
|
||||
await user.click(viewMembersButton);
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/admin/organizations/1/members');
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL State Management', () => {
|
||||
it('reads initial page from URL params', () => {
|
||||
const paramsWithPage = new URLSearchParams('page=2');
|
||||
mockUseSearchParams.mockReturnValue(paramsWithPage as any);
|
||||
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(mockUseAdminOrganizations).toHaveBeenCalledWith(2, 20);
|
||||
});
|
||||
|
||||
it('defaults to page 1 when no page param', () => {
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(mockUseAdminOrganizations).toHaveBeenCalledWith(1, 20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Loading States', () => {
|
||||
it('passes loading state to table', () => {
|
||||
mockUseAdminOrganizations.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: jest.fn(),
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(screen.getByTestId('organization-list-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles empty organization list', () => {
|
||||
mockUseAdminOrganizations.mockReturnValue({
|
||||
data: {
|
||||
data: [],
|
||||
pagination: {
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total_pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: jest.fn(),
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(screen.getByTestId('organization-list-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles undefined data gracefully', () => {
|
||||
mockUseAdminOrganizations.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: jest.fn(),
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(screen.getByTestId('organization-list-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component Integration', () => {
|
||||
it('provides all required props to OrganizationListTable', () => {
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(screen.getByTestId('organization-list-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('provides correct props to OrganizationFormDialog', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
const createButton = screen.getByRole('button', {
|
||||
name: /Create Organization/i,
|
||||
});
|
||||
await user.click(createButton);
|
||||
|
||||
expect(screen.getByTestId('dialog-mode')).toHaveTextContent('create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Management', () => {
|
||||
it('resets dialog state correctly between create and edit', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
// Open create dialog
|
||||
const createButton = screen.getByRole('button', {
|
||||
name: /Create Organization/i,
|
||||
});
|
||||
await user.click(createButton);
|
||||
expect(screen.getByTestId('dialog-mode')).toHaveTextContent('create');
|
||||
|
||||
// Close dialog
|
||||
const closeButton1 = screen.getByRole('button', {
|
||||
name: 'Close Dialog',
|
||||
});
|
||||
await user.click(closeButton1);
|
||||
|
||||
// Open edit dialog
|
||||
const editButton = screen.getByRole('button', { name: 'Edit Organization' });
|
||||
await user.click(editButton);
|
||||
expect(screen.getByTestId('dialog-mode')).toHaveTextContent('edit');
|
||||
expect(screen.getByTestId('dialog-org-id')).toHaveTextContent('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Current User Context', () => {
|
||||
it('renders with authenticated user', () => {
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(screen.getByTestId('organization-list-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles missing current user', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
renderWithProviders(<OrganizationManagementContent />);
|
||||
|
||||
expect(screen.getByTestId('organization-list-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user