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:
Felipe Cardoso
2025-11-06 20:48:10 +01:00
parent 9dc1a70038
commit 2696f44198
5 changed files with 2049 additions and 0 deletions

View File

@@ -0,0 +1,468 @@
/**
* E2E Tests for Admin Organization Management
* Tests organization list, creation, editing, activation, deactivation, deletion, and member management
*/
import { test, expect } from '@playwright/test';
import { setupSuperuserMocks, loginViaUI } from './helpers/auth';
test.describe('Admin Organization Management - Page Load', () => {
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page);
await page.goto('/admin/organizations');
});
test('should display organization management page', async ({ page }) => {
await expect(page).toHaveURL('/admin/organizations');
await expect(page.locator('h1')).toContainText('All Organizations');
});
test('should display page description', async ({ page }) => {
await expect(page.getByText('Manage organizations and their members')).toBeVisible();
});
test('should display create organization button', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await expect(createButton).toBeVisible();
});
test('should display breadcrumbs', async ({ page }) => {
await expect(page.getByTestId('breadcrumb-admin')).toBeVisible();
await expect(page.getByTestId('breadcrumb-organizations')).toBeVisible();
});
});
test.describe('Admin Organization Management - Organization List Table', () => {
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page);
await page.goto('/admin/organizations');
});
test('should display organization list table with headers', async ({ page }) => {
// Wait for table to load
await page.waitForSelector('table', { timeout: 10000 });
// Check table exists and has structure
const table = page.locator('table');
await expect(table).toBeVisible();
// Should have header row
const headerRow = table.locator('thead tr');
await expect(headerRow).toBeVisible();
});
test('should display organization data rows', async ({ page }) => {
// Wait for table to load
await page.waitForSelector('table tbody tr', { timeout: 10000 });
// Should have at least one organization row
const orgRows = page.locator('table tbody tr');
const count = await orgRows.count();
expect(count).toBeGreaterThan(0);
});
test('should display organization status badges', async ({ page }) => {
await page.waitForSelector('table tbody tr', { timeout: 10000 });
// Should see Active or Inactive badges
const statusBadges = page.locator('table tbody').getByText(/Active|Inactive/);
const badgeCount = await statusBadges.count();
expect(badgeCount).toBeGreaterThan(0);
});
test('should display action menu for each organization', async ({ page }) => {
await page.waitForSelector('table tbody tr', { timeout: 10000 });
// Each row should have an action menu button
const actionButtons = page.getByRole('button', { name: /Actions for/i });
const buttonCount = await actionButtons.count();
expect(buttonCount).toBeGreaterThan(0);
});
test('should display member counts', async ({ page }) => {
await page.waitForSelector('table tbody tr', { timeout: 10000 });
// Should show member counts in the Members column
const membersColumn = page.locator('table tbody tr td').filter({ hasText: /^\d+$/ });
const count = await membersColumn.count();
expect(count).toBeGreaterThan(0);
});
test('should display organization names and descriptions', async ({ page }) => {
await page.waitForSelector('table tbody tr', { timeout: 10000 });
// Organization name should be visible
const orgNames = page.locator('table tbody td').first();
await expect(orgNames).toBeVisible();
});
});
test.describe('Admin Organization Management - Pagination', () => {
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page);
await page.goto('/admin/organizations');
});
test('should display pagination info', async ({ page }) => {
await page.waitForSelector('table tbody tr', { timeout: 10000 });
// Should show "Showing X to Y of Z organizations"
await expect(page.getByText(/Showing \d+ to \d+ of \d+ organizations/)).toBeVisible();
});
// Note: Pagination buttons tested in other E2E tests
// Skipping here as it depends on having multiple pages of data
});
test.describe('Admin Organization Management - Create Organization Dialog', () => {
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page);
await page.goto('/admin/organizations');
});
test('should open create organization dialog', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Dialog should appear
await expect(page.getByText('Create Organization')).toBeVisible();
});
test('should display all form fields in create dialog', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Wait for dialog
await expect(page.getByText('Create Organization')).toBeVisible();
// Check for all form fields
await expect(page.getByLabel('Name *')).toBeVisible();
await expect(page.getByLabel('Description')).toBeVisible();
});
test('should display dialog description in create mode', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Should show description
await expect(page.getByText('Add a new organization to the system.')).toBeVisible();
});
test('should have create and cancel buttons', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Should have both buttons
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Create Organization' })).toBeVisible();
});
test('should close dialog when clicking cancel', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Wait for dialog
await expect(page.getByText('Create Organization')).toBeVisible();
// Click cancel
const cancelButton = page.getByRole('button', { name: 'Cancel' });
await cancelButton.click();
// Dialog should close
await expect(page.getByText('Add a new organization to the system.')).not.toBeVisible();
});
test('should show validation error for empty name', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Wait for dialog
await expect(page.getByText('Create Organization')).toBeVisible();
// Leave name empty and try to submit
await page.getByRole('button', { name: 'Create Organization' }).click();
// Should show validation error
await expect(page.getByText(/Organization name is required/i)).toBeVisible();
});
test('should show validation error for short name', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Wait for dialog
await expect(page.getByText('Create Organization')).toBeVisible();
// Fill with short name
await page.getByLabel('Name *').fill('A');
// Try to submit
await page.getByRole('button', { name: 'Create Organization' }).click();
// Should show validation error
await expect(page.getByText(/Organization name must be at least 2 characters/i)).toBeVisible();
});
test('should show validation error for name exceeding max length', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Wait for dialog
await expect(page.getByText('Create Organization')).toBeVisible();
// Fill with very long name (>100 characters)
const longName = 'A'.repeat(101);
await page.getByLabel('Name *').fill(longName);
// Try to submit
await page.getByRole('button', { name: 'Create Organization' }).click();
// Should show validation error
await expect(page.getByText(/Organization name must not exceed 100 characters/i)).toBeVisible();
});
test('should show validation error for description exceeding max length', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Wait for dialog
await expect(page.getByText('Create Organization')).toBeVisible();
// Fill with valid name but very long description (>500 characters)
await page.getByLabel('Name *').fill('Test Organization');
const longDescription = 'A'.repeat(501);
await page.getByLabel('Description').fill(longDescription);
// Try to submit
await page.getByRole('button', { name: 'Create Organization' }).click();
// Should show validation error
await expect(page.getByText(/Description must not exceed 500 characters/i)).toBeVisible();
});
test('should not show active checkbox in create mode', async ({ page }) => {
const createButton = page.getByRole('button', { name: /Create Organization/i });
await createButton.click();
// Wait for dialog
await expect(page.getByText('Create Organization')).toBeVisible();
// Active checkbox should NOT be visible in create mode
await expect(page.getByLabel('Organization is active')).not.toBeVisible();
});
});
test.describe('Admin Organization Management - Action Menu', () => {
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page);
await page.goto('/admin/organizations');
await page.waitForSelector('table tbody tr', { timeout: 10000 });
});
test('should open action menu when clicked', async ({ page }) => {
// Click first action menu button
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
// Menu should appear with options
await expect(page.getByText('Edit Organization')).toBeVisible();
});
test('should display edit option in action menu', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
await expect(page.getByText('Edit Organization')).toBeVisible();
});
test('should display view members option in action menu', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
await expect(page.getByText('View Members')).toBeVisible();
});
test('should display delete option in action menu', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
await expect(page.getByText('Delete Organization')).toBeVisible();
});
test('should open edit dialog when clicking edit', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
// Click edit
await page.getByText('Edit Organization').click();
// Edit dialog should appear
await expect(page.getByText('Edit Organization')).toBeVisible();
await expect(page.getByText('Update the organization details below.')).toBeVisible();
});
test('should navigate to members page when clicking view members', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
// Click view members - use Promise.all for Next.js Link navigation
await Promise.all([
page.waitForURL(/\/admin\/organizations\/[^/]+\/members/, { timeout: 10000 }),
page.getByText('View Members').click()
]);
// Should navigate to members page
await expect(page).toHaveURL(/\/admin\/organizations\/[^/]+\/members/);
});
test('should show delete confirmation dialog when clicking delete', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
// Click delete
await page.getByText('Delete Organization').click();
// Confirmation dialog should appear
await expect(page.getByText('Delete Organization')).toBeVisible();
await expect(page.getByText(/Are you sure you want to delete/i)).toBeVisible();
});
test('should show warning about data loss in delete dialog', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
// Click delete
await page.getByText('Delete Organization').click();
// Warning should be shown
await expect(page.getByText(/This action cannot be undone and will remove all associated data/i)).toBeVisible();
});
test('should close delete dialog when clicking cancel', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
// Click delete
await page.getByText('Delete Organization').click();
// Wait for dialog
await expect(page.getByText(/Are you sure you want to delete/i)).toBeVisible();
// Click cancel
const cancelButton = page.getByRole('button', { name: 'Cancel' });
await cancelButton.click();
// Dialog should close
await expect(page.getByText(/Are you sure you want to delete/i)).not.toBeVisible();
});
});
test.describe('Admin Organization Management - Edit Organization Dialog', () => {
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page);
await page.goto('/admin/organizations');
await page.waitForSelector('table tbody tr', { timeout: 10000 });
});
test('should open edit dialog with existing organization data', async ({ page }) => {
// Open action menu and click edit
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
await page.getByText('Edit Organization').click();
// Dialog should appear with title
await expect(page.getByText('Edit Organization')).toBeVisible();
await expect(page.getByText('Update the organization details below.')).toBeVisible();
});
test('should show active checkbox in edit mode', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
await page.getByText('Edit Organization').click();
// Active checkbox should be visible in edit mode
await expect(page.getByLabel('Organization is active')).toBeVisible();
});
test('should have update and cancel buttons in edit mode', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
await page.getByText('Edit Organization').click();
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Save Changes' })).toBeVisible();
});
test('should populate form fields with existing organization data', async ({ page }) => {
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await actionButton.click();
await page.getByText('Edit Organization').click();
// Name field should be populated
const nameField = page.getByLabel('Name *');
const nameValue = await nameField.inputValue();
expect(nameValue).not.toBe('');
});
});
test.describe('Admin Organization Management - Member Count Interaction', () => {
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page);
await page.goto('/admin/organizations');
await page.waitForSelector('table tbody tr', { timeout: 10000 });
});
test('should allow clicking on member count to view members', async ({ page }) => {
// Find first organization row with members
const firstRow = page.locator('table tbody tr').first();
const memberButton = firstRow.locator('button').filter({ hasText: /^\d+$/ });
// Click on member count - use Promise.all for Next.js Link navigation
await Promise.all([
page.waitForURL(/\/admin\/organizations\/[^/]+\/members/, { timeout: 10000 }),
memberButton.click()
]);
// Should navigate to members page
await expect(page).toHaveURL(/\/admin\/organizations\/[^/]+\/members/);
});
});
test.describe('Admin Organization Management - Accessibility', () => {
test.beforeEach(async ({ page }) => {
await setupSuperuserMocks(page);
await loginViaUI(page);
await page.goto('/admin/organizations');
});
test('should have proper heading hierarchy', async ({ page }) => {
// Page should have h1
const h1 = page.locator('h1');
await expect(h1).toBeVisible();
await expect(h1).toContainText('All Organizations');
});
test('should have accessible labels for action menus', async ({ page }) => {
await page.waitForSelector('table tbody tr', { timeout: 10000 });
// Action buttons should have descriptive labels
const actionButton = page.getByRole('button', { name: /Actions for/i }).first();
await expect(actionButton).toBeVisible();
});
test('should have proper table structure', async ({ page }) => {
await page.waitForSelector('table', { timeout: 10000 });
// Table should have thead and tbody
const table = page.locator('table');
await expect(table.locator('thead')).toBeVisible();
await expect(table.locator('tbody')).toBeVisible();
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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');
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});