diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-details/__tests__/organization-invitation-details-modal.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-details/__tests__/organization-invitation-details-modal.test.tsx new file mode 100644 index 000000000..266f128cc --- /dev/null +++ b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-details/__tests__/organization-invitation-details-modal.test.tsx @@ -0,0 +1,386 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { vi, describe, it, expect, afterEach } from 'vitest'; + +import { OrganizationInvitationDetailsModal } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-details/organization-invitation-details-modal'; +import { renderWithProviders } from '@/tests/utils'; +import { + createMockDetailsModalProps, + createMockInvitation, + createMockPendingInvitation, + createMockExpiredInvitation, + createMockRoles, + createMockProviders, +} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks'; + +describe('OrganizationInvitationDetailsModal', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('isOpen', () => { + describe('when is true', () => { + it('should render the modal', () => { + renderWithProviders( + , + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: 'invitation.details.title' }), + ).toBeInTheDocument(); + }); + }); + + describe('when is false', () => { + it('should not render the modal content', () => { + renderWithProviders( + , + ); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); + }); + + describe('invitation', () => { + describe('when invitation is provided', () => { + it('should display the invitee email', () => { + const invitation = createMockInvitation({ invitee: { email: 'user@example.com' } }); + + renderWithProviders( + , + ); + + expect(screen.getByDisplayValue('user@example.com')).toBeInTheDocument(); + }); + + it('should display the inviter name', () => { + const invitation = createMockInvitation({ inviter: { name: 'John Doe' } }); + + renderWithProviders( + , + ); + + expect(screen.getByDisplayValue('John Doe')).toBeInTheDocument(); + }); + + it('should display created_at date', () => { + const invitation = createMockInvitation({ + created_at: '2024-06-15T10:00:00.000Z', + }); + + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.details.created_at_label')).toBeInTheDocument(); + }); + + it('should display expires_at date', () => { + const invitation = createMockInvitation({ + expires_at: '2025-06-15T10:00:00.000Z', + }); + + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.details.expires_at_label')).toBeInTheDocument(); + }); + }); + + describe('when invitation is null', () => { + it('should handle null invitation gracefully', () => { + renderWithProviders( + , + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + }); + }); + + describe('status badge', () => { + it('should display pending status for pending invitations', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.table.status_pending')).toBeInTheDocument(); + }); + + it('should display expired status for expired invitations', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.table.status_expired')).toBeInTheDocument(); + }); + }); + + describe('roles', () => { + it('should resolve role IDs to names when availableRoles provided', () => { + const invitation = createMockInvitation({ roles: ['role_admin', 'role_member'] }); + const availableRoles = createMockRoles(); + + renderWithProviders( + , + ); + + expect(screen.getByText('Admin')).toBeInTheDocument(); + expect(screen.getByText('Member')).toBeInTheDocument(); + }); + + it('should show role ID as fallback when role not found in availableRoles', () => { + const invitation = createMockInvitation({ roles: ['role_unknown'] }); + + renderWithProviders( + , + ); + + expect(screen.getByText('role_unknown')).toBeInTheDocument(); + }); + + it('should show dash when no roles assigned', () => { + const invitation = createMockInvitation({ roles: [] }); + + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.details.roles_label')).toBeInTheDocument(); + }); + }); + + describe('invitation URL', () => { + it('should display invitation URL when available', () => { + const invitation = createMockInvitation({ + invitation_url: 'https://example.auth0.com/invite?ticket=abc', + }); + + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.details.invitation_url_label')).toBeInTheDocument(); + }); + + it('should not display invitation URL section when no URL', () => { + const invitation = createMockInvitation({ invitation_url: undefined }); + + renderWithProviders( + , + ); + + expect(screen.queryByText('invitation.details.invitation_url_label')).not.toBeInTheDocument(); + }); + }); + + describe('identity provider', () => { + it('should display provider name when resolved', () => { + const invitation = createMockInvitation({ identity_provider_id: 'con_provider1' }); + const availableProviders = createMockProviders(); + + renderWithProviders( + , + ); + + expect(screen.getByDisplayValue('Google')).toBeInTheDocument(); + }); + + it('should show provider ID as fallback when provider not found', () => { + const invitation = createMockInvitation({ identity_provider_id: 'con_unknown' }); + + renderWithProviders( + , + ); + + expect(screen.getByDisplayValue('con_unknown')).toBeInTheDocument(); + }); + + it('should not display provider section when no provider assigned', () => { + const invitation = createMockInvitation({ identity_provider_id: undefined }); + + renderWithProviders( + , + ); + + expect(screen.queryByText('invitation.details.provider_label')).not.toBeInTheDocument(); + }); + }); + + describe('readOnly', () => { + describe('when readOnly is false', () => { + it('should show Revoke and Resend buttons', () => { + renderWithProviders( + , + ); + + expect( + screen.getByRole('button', { name: 'invitation.details.revoke_button' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'invitation.details.resend_button' }), + ).toBeInTheDocument(); + }); + }); + + describe('when readOnly is true', () => { + it('should not show Revoke and Resend buttons', () => { + renderWithProviders( + , + ); + + expect( + screen.queryByRole('button', { name: 'invitation.details.revoke_button' }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'invitation.details.resend_button' }), + ).not.toBeInTheDocument(); + }); + }); + }); + + describe('action callbacks', () => { + it('should call onRevoke when Revoke button is clicked', async () => { + const user = userEvent.setup(); + const onRevoke = vi.fn(); + const invitation = createMockPendingInvitation(); + + renderWithProviders( + , + ); + + const revokeButton = screen.getByRole('button', { + name: 'invitation.details.revoke_button', + }); + await user.click(revokeButton); + + expect(onRevoke).toHaveBeenCalledTimes(1); + expect(onRevoke).toHaveBeenCalledWith(invitation); + }); + + it('should call onResend when Resend button is clicked', async () => { + const user = userEvent.setup(); + const onResend = vi.fn(); + const invitation = createMockPendingInvitation(); + + renderWithProviders( + , + ); + + const resendButton = screen.getByRole('button', { + name: 'invitation.details.resend_button', + }); + await user.click(resendButton); + + expect(onResend).toHaveBeenCalledTimes(1); + expect(onResend).toHaveBeenCalledWith(invitation); + }); + + it('should call onClose when Close button is clicked', async () => { + const user = userEvent.setup(); + const onClose = vi.fn(); + + renderWithProviders( + , + ); + + const closeButton = screen.getByRole('button', { + name: 'invitation.details.close_button', + }); + await user.click(closeButton); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); + + describe('action in progress', () => { + it('should disable Revoke button when isRevoking is true', () => { + renderWithProviders( + , + ); + + const revokeButton = screen.getByRole('button', { + name: 'invitation.details.revoke_button', + }); + expect(revokeButton).toBeDisabled(); + }); + + it('should disable Resend button when isResending is true', () => { + renderWithProviders( + , + ); + + const resendButton = screen.getByRole('button', { + name: 'invitation.details.resend_button', + }); + expect(resendButton).toBeDisabled(); + }); + + it('should disable both buttons when either action is in progress', () => { + renderWithProviders( + , + ); + + const revokeButton = screen.getByRole('button', { + name: 'invitation.details.revoke_button', + }); + const resendButton = screen.getByRole('button', { + name: 'invitation.details.resend_button', + }); + expect(revokeButton).toBeDisabled(); + expect(resendButton).toBeDisabled(); + }); + }); + + describe('className', () => { + it('should apply custom class to modal', () => { + const customClass = 'custom-details-class'; + + renderWithProviders( + , + ); + + const modalContent = document.querySelector('[data-slot="dialog-content"]'); + expect(modalContent).toHaveClass(customClass); + }); + }); +}); diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/__tests__/organization-invitation-revoke-modal.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/__tests__/organization-invitation-revoke-modal.test.tsx new file mode 100644 index 000000000..595076fa3 --- /dev/null +++ b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/__tests__/organization-invitation-revoke-modal.test.tsx @@ -0,0 +1,246 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { vi, describe, it, expect, afterEach } from 'vitest'; + +import { OrganizationInvitationRevokeModal } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/organization-invitation-revoke-modal'; +import { renderWithProviders } from '@/tests/utils'; +import { + createMockRevokeModalProps, + createMockPendingInvitation, +} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks'; + +describe('OrganizationInvitationRevokeModal', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('isOpen', () => { + describe('when is true', () => { + it('should render the modal', () => { + renderWithProviders( + , + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + }); + + describe('when is false', () => { + it('should not render the modal content', () => { + renderWithProviders( + , + ); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); + }); + + describe('revoke mode', () => { + describe('when isRevokeAndResend is false', () => { + it('should render revoke-specific title', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.revoke.title')).toBeInTheDocument(); + }); + + it('should render revoke-specific description', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.revoke.description')).toBeInTheDocument(); + }); + + it('should render revoke-specific button text', () => { + renderWithProviders( + , + ); + + expect( + screen.getByRole('button', { name: 'invitation.revoke.confirm_button' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'invitation.revoke.cancel_button' }), + ).toBeInTheDocument(); + }); + }); + + describe('when isRevokeAndResend is true', () => { + it('should render revoke-and-resend title', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.revoke_resend.title')).toBeInTheDocument(); + }); + + it('should render revoke-and-resend description', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.revoke_resend.description')).toBeInTheDocument(); + }); + + it('should render revoke-and-resend button text', () => { + renderWithProviders( + , + ); + + expect( + screen.getByRole('button', { name: 'invitation.revoke_resend.confirm_button' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'invitation.revoke_resend.cancel_button' }), + ).toBeInTheDocument(); + }); + }); + }); + + describe('isLoading', () => { + describe('when is true', () => { + it('should disable confirm button', () => { + renderWithProviders( + , + ); + + const confirmButton = screen.getByRole('button', { + name: 'invitation.revoke.confirm_button', + }); + expect(confirmButton).toBeDisabled(); + }); + + it('should disable cancel button', () => { + renderWithProviders( + , + ); + + const cancelButton = screen.getByRole('button', { + name: 'invitation.revoke.cancel_button', + }); + expect(cancelButton).toBeDisabled(); + }); + }); + + describe('when is false', () => { + it('should enable confirm button', () => { + renderWithProviders( + , + ); + + const confirmButton = screen.getByRole('button', { + name: 'invitation.revoke.confirm_button', + }); + expect(confirmButton).toBeEnabled(); + }); + }); + }); + + describe('onConfirm', () => { + it('should call onConfirm with invitation when confirm button is clicked', async () => { + const user = userEvent.setup(); + const onConfirm = vi.fn(); + const invitation = createMockPendingInvitation(); + + renderWithProviders( + , + ); + + const confirmButton = screen.getByRole('button', { + name: 'invitation.revoke.confirm_button', + }); + await user.click(confirmButton); + + expect(onConfirm).toHaveBeenCalledTimes(1); + expect(onConfirm).toHaveBeenCalledWith(invitation); + }); + + it('should not call onConfirm when invitation is null', async () => { + const user = userEvent.setup(); + const onConfirm = vi.fn(); + + renderWithProviders( + , + ); + + const confirmButton = screen.getByRole('button', { + name: 'invitation.revoke.confirm_button', + }); + await user.click(confirmButton); + + expect(onConfirm).not.toHaveBeenCalled(); + }); + }); + + describe('onClose', () => { + it('should call onClose when cancel button is clicked', async () => { + const user = userEvent.setup(); + const onClose = vi.fn(); + + renderWithProviders( + , + ); + + const cancelButton = screen.getByRole('button', { + name: 'invitation.revoke.cancel_button', + }); + await user.click(cancelButton); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); + + describe('className', () => { + it('should apply custom class to modal', () => { + const customClass = 'custom-revoke-class'; + + renderWithProviders( + , + ); + + const modalContent = document.querySelector('[data-slot="dialog-content"]'); + expect(modalContent).toHaveClass(customClass); + }); + }); + + describe('invitation', () => { + describe('when invitation is null', () => { + it('should handle null invitation gracefully', () => { + renderWithProviders( + , + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-table/__tests__/organization-invitation-table-actions-column.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-table/__tests__/organization-invitation-table-actions-column.test.tsx new file mode 100644 index 000000000..ee819768e --- /dev/null +++ b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-table/__tests__/organization-invitation-table-actions-column.test.tsx @@ -0,0 +1,321 @@ +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { OrganizationInvitationTableActionsColumn } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-table/organization-invitation-table-actions-column'; +import { renderWithProviders } from '@/tests/utils'; +import { + createMockActionsColumnProps, + createMockPendingInvitation, + createMockExpiredInvitation, +} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks'; + +describe('OrganizationInvitationTableActionsColumn', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('Rendering and Basic Structure', () => { + it('should render dropdown trigger button', () => { + const props = createMockActionsColumnProps(); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + expect(trigger).toBeInTheDocument(); + expect(trigger).toHaveClass('h-8', 'w-8'); + }); + + it('should have proper accessibility attributes', () => { + const props = createMockActionsColumnProps(); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + expect(trigger).toHaveAttribute('type', 'button'); + }); + }); + + describe('Dropdown Menu Interactions', () => { + it('should open dropdown menu when trigger button is clicked', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps(); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }), + ).toBeInTheDocument(); + }); + + it('should close dropdown menu when user presses Escape key', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps(); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }), + ).toBeInTheDocument(); + + await user.keyboard('{Escape}'); + + await waitFor(() => { + expect( + screen.queryByRole('menuitem', { name: 'invitation.actions.view_details' }), + ).not.toBeInTheDocument(); + }); + }); + }); + + describe('Invitation Status: Pending', () => { + it('should show View Details action', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ + invitation: createMockPendingInvitation(), + }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }), + ).toBeInTheDocument(); + }); + + it('should show Copy URL action when invitation has URL', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ + invitation: createMockPendingInvitation({ + invitation_url: 'https://example.com/invite?ticket=abc', + }), + }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.copy_url' }), + ).toBeInTheDocument(); + }); + + it('should not show Copy URL action when invitation has no URL', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ + invitation: createMockPendingInvitation({ invitation_url: undefined }), + }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.queryByRole('menuitem', { name: 'invitation.actions.copy_url' }), + ).not.toBeInTheDocument(); + }); + + it('should show Revoke & Resend action when not readOnly', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ readOnly: false }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.revoke_and_resend' }), + ).toBeInTheDocument(); + }); + + it('should show Revoke action when not readOnly', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ readOnly: false }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.revoke' }), + ).toBeInTheDocument(); + }); + }); + + describe('Invitation Status: Expired', () => { + it('should show View Details action', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ + invitation: createMockExpiredInvitation(), + }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }), + ).toBeInTheDocument(); + }); + + it('should not show Copy URL action for expired invitations', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ + invitation: createMockExpiredInvitation(), + }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.queryByRole('menuitem', { name: 'invitation.actions.copy_url' }), + ).not.toBeInTheDocument(); + }); + }); + + describe('Read-Only Mode', () => { + it('should not show Revoke & Resend action when readOnly', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ readOnly: true }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.queryByRole('menuitem', { name: 'invitation.actions.revoke_and_resend' }), + ).not.toBeInTheDocument(); + }); + + it('should not show Revoke action when readOnly', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ readOnly: true }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.queryByRole('menuitem', { name: 'invitation.actions.revoke' }), + ).not.toBeInTheDocument(); + }); + + it('should still show View Details when readOnly', async () => { + const user = userEvent.setup(); + const props = createMockActionsColumnProps({ readOnly: true }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }), + ).toBeInTheDocument(); + }); + }); + + describe('Callback Invocations', () => { + it('should call onViewDetails when View Details is clicked', async () => { + const user = userEvent.setup(); + const onViewDetails = vi.fn(); + const invitation = createMockPendingInvitation(); + const props = createMockActionsColumnProps({ invitation, onViewDetails }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + const menuItem = screen.getByRole('menuitem', { + name: 'invitation.actions.view_details', + }); + await user.click(menuItem); + + expect(onViewDetails).toHaveBeenCalledTimes(1); + expect(onViewDetails).toHaveBeenCalledWith(invitation); + }); + + it('should call onCopyUrl when Copy URL is clicked', async () => { + const user = userEvent.setup(); + const onCopyUrl = vi.fn(); + const invitation = createMockPendingInvitation({ + invitation_url: 'https://example.com/invite', + }); + const props = createMockActionsColumnProps({ invitation, onCopyUrl }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + const menuItem = screen.getByRole('menuitem', { + name: 'invitation.actions.copy_url', + }); + await user.click(menuItem); + + expect(onCopyUrl).toHaveBeenCalledTimes(1); + expect(onCopyUrl).toHaveBeenCalledWith(invitation); + }); + + it('should call onRevokeAndResend when Revoke & Resend is clicked', async () => { + const user = userEvent.setup(); + const onRevokeAndResend = vi.fn(); + const invitation = createMockPendingInvitation(); + const props = createMockActionsColumnProps({ invitation, onRevokeAndResend }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + const menuItem = screen.getByRole('menuitem', { + name: 'invitation.actions.revoke_and_resend', + }); + await user.click(menuItem); + + expect(onRevokeAndResend).toHaveBeenCalledTimes(1); + expect(onRevokeAndResend).toHaveBeenCalledWith(invitation); + }); + + it('should call onRevoke when Revoke is clicked', async () => { + const user = userEvent.setup(); + const onRevoke = vi.fn(); + const invitation = createMockPendingInvitation(); + const props = createMockActionsColumnProps({ invitation, onRevoke }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + const menuItem = screen.getByRole('menuitem', { + name: 'invitation.actions.revoke', + }); + await user.click(menuItem); + + expect(onRevoke).toHaveBeenCalledTimes(1); + expect(onRevoke).toHaveBeenCalledWith(invitation); + }); + }); + + describe('Custom Messages', () => { + it('should accept custom messages prop without error', async () => { + const user = userEvent.setup(); + const customMessages = { + actions: { + view_details: 'Custom View Details', + }, + }; + const props = createMockActionsColumnProps({ customMessages }); + renderWithProviders(); + + const trigger = screen.getByRole('button'); + await user.click(trigger); + + // The mock translator returns keys, so verify the menu item renders + expect( + screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/shared/invitation-create/__tests__/organization-invitation-create-modal.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/shared/invitation-create/__tests__/organization-invitation-create-modal.test.tsx new file mode 100644 index 000000000..d35fcca86 --- /dev/null +++ b/packages/react/src/components/auth0/my-organization/shared/member-management/shared/invitation-create/__tests__/organization-invitation-create-modal.test.tsx @@ -0,0 +1,218 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { vi, describe, it, expect, afterEach } from 'vitest'; + +import { OrganizationInvitationCreateModal } from '@/components/auth0/my-organization/shared/member-management/shared/invitation-create/organization-invitation-create-modal'; +import { renderWithProviders } from '@/tests/utils'; +import { + createMockCreateModalProps, + createMockRoles, + createMockProviders, +} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks'; + +describe('OrganizationInvitationCreateModal', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('isOpen', () => { + describe('when is true', () => { + it('should render the modal', () => { + renderWithProviders( + , + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText('invitation.create.title')).toBeInTheDocument(); + }); + }); + + describe('when is false', () => { + it('should not render the modal content', () => { + renderWithProviders( + , + ); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); + }); + + describe('isLoading', () => { + describe('when is true', () => { + it('should disable form inputs', () => { + renderWithProviders( + , + ); + + const emailInput = screen.getByPlaceholderText('invitation.create.email_placeholder'); + expect(emailInput).toBeDisabled(); + }); + + it('should disable cancel and submit buttons', () => { + renderWithProviders( + , + ); + + const cancelButton = screen.getByRole('button', { + name: 'invitation.create.cancel_button', + }); + expect(cancelButton).toBeDisabled(); + }); + }); + + describe('when is false', () => { + it('should enable form inputs', () => { + renderWithProviders( + , + ); + + const emailInput = screen.getByPlaceholderText('invitation.create.email_placeholder'); + expect(emailInput).toBeEnabled(); + }); + }); + }); + + describe('className', () => { + describe('when className is provided', () => { + it('should apply custom class to modal', () => { + const customClass = 'custom-modal-class'; + + renderWithProviders( + , + ); + + const modalContent = document.querySelector('[data-slot="dialog-content"]'); + expect(modalContent).toHaveClass(customClass); + }); + }); + }); + + describe('onClose', () => { + describe('when modal is closed', () => { + it('should call onClose callback via cancel button', async () => { + const user = userEvent.setup(); + const mockOnClose = vi.fn(); + + renderWithProviders( + , + ); + + const cancelButton = screen.getByRole('button', { + name: 'invitation.create.cancel_button', + }); + await user.click(cancelButton); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('email input', () => { + it('should render email input field', () => { + renderWithProviders(); + + expect( + screen.getByPlaceholderText('invitation.create.email_placeholder'), + ).toBeInTheDocument(); + expect(screen.getByText(/invitation\.create\.email_label/)).toBeInTheDocument(); + }); + + it('should show helper text by default', () => { + renderWithProviders(); + + expect(screen.getByText('invitation.create.email_helper')).toBeInTheDocument(); + }); + }); + + describe('submit', () => { + it('should disable submit button when no emails are added', () => { + renderWithProviders(); + + const submitButton = screen.getByRole('button', { + name: 'invitation.create.submit_button', + }); + expect(submitButton).toBeDisabled(); + }); + + it('should show creating text when isLoading is true', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.create.creating')).toBeInTheDocument(); + }); + }); + + describe('availableRoles', () => { + describe('when roles are provided', () => { + it('should render roles combobox', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.create.roles_label')).toBeInTheDocument(); + }); + }); + + describe('when no roles are provided', () => { + it('should still render roles section', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.create.roles_label')).toBeInTheDocument(); + }); + }); + }); + + describe('availableProviders', () => { + describe('when providers are provided', () => { + it('should render provider dropdown', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.create.provider_label')).toBeInTheDocument(); + }); + }); + + describe('when no providers are provided', () => { + it('should still render provider section', () => { + renderWithProviders( + , + ); + + expect(screen.getByText('invitation.create.provider_label')).toBeInTheDocument(); + }); + }); + }); + + describe('description', () => { + it('should render description text', () => { + renderWithProviders(); + + expect(screen.getByText('invitation.create.description')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/shared/search-filter/__tests__/search-filter.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/shared/search-filter/__tests__/search-filter.test.tsx new file mode 100644 index 000000000..7b83b6a4d --- /dev/null +++ b/packages/react/src/components/auth0/my-organization/shared/member-management/shared/search-filter/__tests__/search-filter.test.tsx @@ -0,0 +1,103 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { vi, describe, it, expect, afterEach } from 'vitest'; + +import { SearchFilter } from '@/components/auth0/my-organization/shared/member-management/shared/search-filter/search-filter'; +import { renderWithProviders } from '@/tests/utils'; +import { createMockSearchFilterProps } from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks'; + +describe('SearchFilter', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('rendering', () => { + it('should render the filter when roles are provided', () => { + renderWithProviders(); + + expect(screen.getByText(/invitation\.table\.filter_by_role/)).toBeInTheDocument(); + }); + + it('should return null when no roles are provided', () => { + const { container } = renderWithProviders( + , + ); + + expect(container.innerHTML).toBe(''); + }); + + it('should render reset button', () => { + renderWithProviders(); + + expect( + screen.getByRole('button', { name: 'invitation.table.reset_filter' }), + ).toBeInTheDocument(); + }); + }); + + describe('reset button', () => { + it('should be disabled when no active filter', () => { + renderWithProviders(); + + const resetButton = screen.getByRole('button', { + name: 'invitation.table.reset_filter', + }); + expect(resetButton).toBeDisabled(); + }); + + it('should be enabled when there is an active filter', () => { + renderWithProviders( + , + ); + + const resetButton = screen.getByRole('button', { + name: 'invitation.table.reset_filter', + }); + expect(resetButton).toBeEnabled(); + }); + + it('should call onRoleFilterChange with undefined when reset is clicked', async () => { + const user = userEvent.setup(); + const onRoleFilterChange = vi.fn(); + + renderWithProviders( + , + ); + + const resetButton = screen.getByRole('button', { + name: 'invitation.table.reset_filter', + }); + await user.click(resetButton); + + expect(onRoleFilterChange).toHaveBeenCalledTimes(1); + expect(onRoleFilterChange).toHaveBeenCalledWith(undefined); + }); + }); + + describe('className', () => { + it('should apply custom class when provided', () => { + const customClass = 'custom-filter-class'; + + const { container } = renderWithProviders( + , + ); + + const filterDiv = container.firstChild as HTMLElement; + expect(filterDiv).toHaveClass(customClass); + }); + + it('should apply default class when no custom class provided', () => { + const { container } = renderWithProviders( + , + ); + + const filterDiv = container.firstChild as HTMLElement; + expect(filterDiv).toHaveClass('mb-4'); + }); + }); +}); diff --git a/packages/react/src/hooks/my-organization/__tests__/use-member-management-service.test.ts b/packages/react/src/hooks/my-organization/__tests__/use-member-management-service.test.ts new file mode 100644 index 000000000..c007cd215 --- /dev/null +++ b/packages/react/src/hooks/my-organization/__tests__/use-member-management-service.test.ts @@ -0,0 +1,413 @@ +import { renderHook, waitFor, act } from '@testing-library/react'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { useMemberManagementService } from '@/hooks/my-organization/shared/services/use-member-management-service'; +import { memberManagementQueryKeys } from '@/hooks/my-organization/shared/services/use-member-management-service'; +import * as useCoreClientModule from '@/hooks/shared/use-core-client'; +import * as useTranslatorModule from '@/hooks/shared/use-translator'; +import { mockCore, mockToast, createMockI18nService } from '@/tests/utils'; +import { createMockInvitation } from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks'; +import { createTestQueryClientWrapper } from '@/tests/utils/test-provider'; +import type { UseMemberManagementServiceOptions } from '@/types/my-organization/member-management/organization-member-management-types'; + +const { mockedShowToast } = mockToast(); +const { initMockCoreClient } = mockCore(); + +const createDefaultOptions = ( + overrides?: Partial, +): UseMemberManagementServiceOptions => ({ + customMessages: {}, + activeTab: 'invitations', + invitationParams: { + pageSize: 10, + fromToken: undefined, + sortConfig: { key: null, direction: 'asc' }, + filters: {}, + }, + ...overrides, +}); + +const renderService = (options: UseMemberManagementServiceOptions) => { + const { wrapper, queryClient } = createTestQueryClientWrapper(); + return { + queryClient, + ...renderHook(() => useMemberManagementService(options), { wrapper }), + }; +}; + +describe('useMemberManagementService', () => { + let mockCoreClient: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + + mockCoreClient = initMockCoreClient(); + + vi.spyOn(useCoreClientModule, 'useCoreClient').mockReturnValue({ + coreClient: mockCoreClient, + }); + + vi.spyOn(useTranslatorModule, 'useTranslator').mockReturnValue({ + t: createMockI18nService().translator('member_management'), + changeLanguage: vi.fn(), + currentLanguage: 'en', + fallbackLanguage: 'en', + }); + }); + + describe('memberManagementQueryKeys', () => { + it('should have correct base key', () => { + expect(memberManagementQueryKeys.all).toEqual(['member-management']); + }); + + it('should have correct invitations key', () => { + expect(memberManagementQueryKeys.invitations()).toEqual(['member-management', 'invitations']); + }); + }); + + describe('providersQuery', () => { + it('should fetch identity providers when invitations tab is active', async () => { + const options = createDefaultOptions({ activeTab: 'invitations' }); + const { result } = renderService(options); + + await waitFor(() => { + expect(result.current.providersQuery.isSuccess).toBe(true); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list, + ).toHaveBeenCalled(); + }); + + it('should not fetch identity providers when members tab is active', () => { + const options = createDefaultOptions({ activeTab: 'members' }); + const { result } = renderService(options); + + expect(result.current.providersQuery.fetchStatus).toBe('idle'); + }); + }); + + describe('invitationsQuery', () => { + it('should fetch invitations when invitations tab is active', async () => { + const options = createDefaultOptions({ activeTab: 'invitations' }); + const { result } = renderService(options); + + await waitFor(() => { + expect(result.current.invitationsQuery.isSuccess).toBe(true); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.invitations.list, + ).toHaveBeenCalledWith( + expect.objectContaining({ + take: 10, + from: undefined, + sort: undefined, + }), + ); + }); + + it('should not fetch invitations when members tab is active', () => { + const options = createDefaultOptions({ activeTab: 'members' }); + const { result } = renderService(options); + + expect(result.current.invitationsQuery.fetchStatus).toBe('idle'); + }); + + it('should pass sort parameter when sort config has a valid key', async () => { + const options = createDefaultOptions({ + invitationParams: { + pageSize: 10, + fromToken: undefined, + sortConfig: { key: 'created_at', direction: 'desc' }, + filters: {}, + }, + }); + const { result } = renderService(options); + + await waitFor(() => { + expect(result.current.invitationsQuery.isSuccess).toBe(true); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.invitations.list, + ).toHaveBeenCalledWith( + expect.objectContaining({ + sort: 'created_at:-1', + }), + ); + }); + + it('should pass fromToken when provided', async () => { + const options = createDefaultOptions({ + invitationParams: { + pageSize: 10, + fromToken: 'token_abc', + sortConfig: { key: null, direction: 'asc' }, + filters: {}, + }, + }); + const { result } = renderService(options); + + await waitFor(() => { + expect(result.current.invitationsQuery.isSuccess).toBe(true); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.invitations.list, + ).toHaveBeenCalledWith( + expect.objectContaining({ + from: 'token_abc', + }), + ); + }); + + it('should return parsed invitations data', async () => { + const mockInvitation = createMockInvitation(); + mockCoreClient.getMyOrganizationApiClient().organization.invitations.list = vi + .fn() + .mockResolvedValue({ + data: [mockInvitation], + response: { next: 'next_token', total: 5 }, + }); + + const options = createDefaultOptions(); + const { result } = renderService(options); + + await waitFor(() => { + expect(result.current.invitationsQuery.isSuccess).toBe(true); + }); + + expect(result.current.invitationsQuery.data).toEqual({ + invitations: [mockInvitation], + next: 'next_token', + total: 5, + }); + }); + }); + + describe('createInvitationMutation', () => { + it('should create an invitation and show success toast', async () => { + const options = createDefaultOptions(); + const { result } = renderService(options); + + await act(async () => { + result.current.createInvitationMutation.mutate({ + invitees: [{ email: 'new@example.com', roles: ['role_admin'] }], + }); + }); + + await waitFor(() => { + expect(result.current.createInvitationMutation.isSuccess).toBe(true); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.invitations.create, + ).toHaveBeenCalled(); + expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); + }); + + it('should call onBefore action and cancel if it returns false', async () => { + const onBefore = vi.fn().mockReturnValue(false); + const options = createDefaultOptions({ + createInvitationAction: { onBefore }, + }); + const { result } = renderService(options); + + await act(async () => { + result.current.createInvitationMutation.mutate({ + invitees: [{ email: 'new@example.com' }], + }); + }); + + await waitFor(() => { + expect(result.current.createInvitationMutation.isError).toBe(true); + }); + + expect(onBefore).toHaveBeenCalled(); + expect( + mockCoreClient.getMyOrganizationApiClient().organization.invitations.create, + ).not.toHaveBeenCalled(); + }); + + it('should call onAfter action on success', async () => { + const onAfter = vi.fn(); + const options = createDefaultOptions({ + createInvitationAction: { onAfter }, + }); + const { result } = renderService(options); + + await act(async () => { + result.current.createInvitationMutation.mutate({ + invitees: [{ email: 'new@example.com' }], + }); + }); + + await waitFor(() => { + expect(result.current.createInvitationMutation.isSuccess).toBe(true); + }); + + expect(onAfter).toHaveBeenCalled(); + }); + + it('should show error toast on failure', async () => { + mockCoreClient.getMyOrganizationApiClient().organization.invitations.create = vi + .fn() + .mockRejectedValue(new Error('Create failed')); + + const options = createDefaultOptions(); + const { result } = renderService(options); + + await act(async () => { + result.current.createInvitationMutation.mutate({ + invitees: [{ email: 'new@example.com' }], + }); + }); + + await waitFor(() => { + expect(result.current.createInvitationMutation.isError).toBe(true); + }); + + expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })); + }); + }); + + describe('revokeInvitationMutation', () => { + it('should revoke an invitation and show success toast', async () => { + const invitation = createMockInvitation(); + const options = createDefaultOptions(); + const { result } = renderService(options); + + await act(async () => { + result.current.revokeInvitationMutation.mutate(invitation); + }); + + await waitFor(() => { + expect(result.current.revokeInvitationMutation.isSuccess).toBe(true); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.invitations.delete, + ).toHaveBeenCalledWith(invitation.id); + expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); + }); + + it('should call onBefore action and cancel if it returns false', async () => { + const onBefore = vi.fn().mockReturnValue(false); + const invitation = createMockInvitation(); + const options = createDefaultOptions({ + revokeInvitationAction: { onBefore }, + }); + const { result } = renderService(options); + + await act(async () => { + result.current.revokeInvitationMutation.mutate(invitation); + }); + + await waitFor(() => { + expect(result.current.revokeInvitationMutation.isError).toBe(true); + }); + + expect(onBefore).toHaveBeenCalledWith(invitation); + expect( + mockCoreClient.getMyOrganizationApiClient().organization.invitations.delete, + ).not.toHaveBeenCalled(); + }); + + it('should show error toast on failure', async () => { + mockCoreClient.getMyOrganizationApiClient().organization.invitations.delete = vi + .fn() + .mockRejectedValue(new Error('Revoke failed')); + + const invitation = createMockInvitation(); + const options = createDefaultOptions(); + const { result } = renderService(options); + + await act(async () => { + result.current.revokeInvitationMutation.mutate(invitation); + }); + + await waitFor(() => { + expect(result.current.revokeInvitationMutation.isError).toBe(true); + }); + + expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })); + }); + }); + + describe('resendInvitationMutation', () => { + it('should revoke and resend an invitation', async () => { + const invitation = createMockInvitation(); + const options = createDefaultOptions(); + const { result } = renderService(options); + + await act(async () => { + result.current.resendInvitationMutation.mutate(invitation); + }); + + await waitFor(() => { + expect(result.current.resendInvitationMutation.isSuccess).toBe(true); + }); + + const orgApi = mockCoreClient.getMyOrganizationApiClient().organization; + expect(orgApi.invitations.get).toHaveBeenCalledWith(invitation.id); + expect(orgApi.invitations.delete).toHaveBeenCalled(); + expect(orgApi.invitations.create).toHaveBeenCalled(); + expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); + }); + + it('should call onBefore action and cancel if it returns false', async () => { + const onBefore = vi.fn().mockReturnValue(false); + const invitation = createMockInvitation(); + const options = createDefaultOptions({ + resendInvitationAction: { onBefore }, + }); + const { result } = renderService(options); + + await act(async () => { + result.current.resendInvitationMutation.mutate(invitation); + }); + + await waitFor(() => { + expect(result.current.resendInvitationMutation.isError).toBe(true); + }); + + expect(onBefore).toHaveBeenCalledWith(invitation); + }); + + it('should show error toast on failure', async () => { + mockCoreClient.getMyOrganizationApiClient().organization.invitations.get = vi + .fn() + .mockRejectedValue(new Error('Fetch failed')); + + const invitation = createMockInvitation(); + const options = createDefaultOptions(); + const { result } = renderService(options); + + await act(async () => { + result.current.resendInvitationMutation.mutate(invitation); + }); + + await waitFor(() => { + expect(result.current.resendInvitationMutation.isError).toBe(true); + }); + + expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })); + }); + }); + + describe('fetchInvitationDetails', () => { + it('should fetch invitation details by id', async () => { + const mockInvitation = createMockInvitation(); + const options = createDefaultOptions(); + const { result } = renderService(options); + + const details = await result.current.fetchInvitationDetails('inv_abc123xyz456'); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.invitations.get, + ).toHaveBeenCalledWith('inv_abc123xyz456'); + expect(details).toEqual(mockInvitation); + }); + }); +}); diff --git a/packages/react/src/tests/utils/__mocks__/core/core-client.mocks.ts b/packages/react/src/tests/utils/__mocks__/core/core-client.mocks.ts index 2964c44ac..02aa5053c 100644 --- a/packages/react/src/tests/utils/__mocks__/core/core-client.mocks.ts +++ b/packages/react/src/tests/utils/__mocks__/core/core-client.mocks.ts @@ -8,6 +8,7 @@ import { createMockEmptyAuthenticationMethods, } from '@/tests/utils/__mocks__/my-account/mfa/mfa.mocks'; import { createMockIdentityProvider } from '@/tests/utils/__mocks__/my-organization/domain-management/domain.mocks'; +import { createMockInvitation } from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks'; import { createMockOrganization } from '@/tests/utils/__mocks__/my-organization/organization-management/organization-details.mocks'; const createMockMyAccountApiService = (): CoreClientInterface['myAccountApiClient'] => { @@ -55,6 +56,15 @@ const createMockMyOrgApiService = (): CoreClientInterface['myOrganizationApiClie delete: vi.fn().mockResolvedValue(undefined), }, }, + invitations: { + list: vi.fn().mockResolvedValue({ + data: [createMockInvitation()], + response: { next: null }, + }), + get: vi.fn().mockResolvedValue(createMockInvitation()), + create: vi.fn().mockResolvedValue([createMockInvitation()]), + delete: vi.fn().mockResolvedValue(undefined), + }, domains: { list: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue({}), diff --git a/packages/react/src/tests/utils/__mocks__/my-organization/member-management/invitation.mocks.ts b/packages/react/src/tests/utils/__mocks__/my-organization/member-management/invitation.mocks.ts new file mode 100644 index 000000000..79ec5d15d --- /dev/null +++ b/packages/react/src/tests/utils/__mocks__/my-organization/member-management/invitation.mocks.ts @@ -0,0 +1,108 @@ +import type { MemberInvitation } from '@auth0/universal-components-core'; +import { vi } from 'vitest'; + +import type { OrganizationInvitationDetailsModalProps } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-details/organization-invitation-details-modal'; +import type { OrganizationInvitationRevokeModalProps } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/organization-invitation-revoke-modal'; +import type { OrganizationInvitationCreateModalProps } from '@/components/auth0/my-organization/shared/member-management/shared/invitation-create/organization-invitation-create-modal'; +import type { + RoleOption, + IdentityProviderOption, + OrganizationInvitationTableActionsColumnProps, + SearchFilterProps, +} from '@/types/my-organization/member-management/organization-invitation-table-types'; + +export const createMockInvitation = (overrides?: Partial): MemberInvitation => ({ + id: 'inv_abc123xyz456', + invitee: { email: 'test@example.com' }, + inviter: { name: 'Admin User' }, + roles: ['role_admin'], + created_at: '2024-01-01T00:00:00.000Z', + expires_at: '2099-12-31T23:59:59.000Z', + invitation_url: 'https://example.auth0.com/invitation?ticket=abc123', + ...overrides, +}); + +export const createMockPendingInvitation = ( + overrides?: Partial, +): MemberInvitation => + createMockInvitation({ + invitation_url: 'https://example.auth0.com/invitation?ticket=pending123', + ...overrides, + }); + +export const createMockExpiredInvitation = ( + overrides?: Partial, +): MemberInvitation => + createMockInvitation({ + expires_at: '2020-01-01T00:00:00.000Z', + invitation_url: undefined, + ...overrides, + }); + +export const createMockRoles = (): RoleOption[] => [ + { id: 'role_admin', name: 'Admin', description: 'Administrator role' }, + { id: 'role_member', name: 'Member', description: 'Member role' }, + { id: 'role_viewer', name: 'Viewer', description: 'Viewer role' }, +]; + +export const createMockProviders = (): IdentityProviderOption[] => [ + { id: 'con_provider1', name: 'Google', type: 'social' }, + { id: 'con_provider2', name: 'Okta', type: 'enterprise' }, +]; + +export const createMockCreateModalProps = ( + overrides: Partial = {}, +): OrganizationInvitationCreateModalProps => ({ + isOpen: true, + isLoading: false, + onClose: vi.fn(), + onCreate: vi.fn(), + ...overrides, +}); + +export const createMockActionsColumnProps = ( + overrides: Partial = {}, +): OrganizationInvitationTableActionsColumnProps => ({ + invitation: createMockPendingInvitation(), + readOnly: false, + onViewDetails: vi.fn(), + onCopyUrl: vi.fn(), + onRevokeAndResend: vi.fn(), + onRevoke: vi.fn(), + ...overrides, +}); + +export const createMockDetailsModalProps = ( + overrides: Partial = {}, +): OrganizationInvitationDetailsModalProps => ({ + invitation: createMockPendingInvitation(), + isOpen: true, + isRevoking: false, + isResending: false, + onClose: vi.fn(), + onCopyUrl: vi.fn(), + onRevoke: vi.fn(), + onResend: vi.fn(), + ...overrides, +}); + +export const createMockRevokeModalProps = ( + overrides: Partial = {}, +): OrganizationInvitationRevokeModalProps => ({ + invitation: createMockPendingInvitation(), + isOpen: true, + isLoading: false, + isRevokeAndResend: false, + onClose: vi.fn(), + onConfirm: vi.fn(), + ...overrides, +}); + +export const createMockSearchFilterProps = ( + overrides: Partial = {}, +): SearchFilterProps => ({ + filters: {}, + availableRoles: createMockRoles(), + onRoleFilterChange: vi.fn(), + ...overrides, +});