Skip to content

Commit 33e7ec0

Browse files
committed
test(react): add tests and mocks for invitation components
1 parent d1e9fd7 commit 33e7ec0

11 files changed

Lines changed: 2043 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { vi, describe, it, expect, afterEach } from 'vitest';
4+
5+
import { OrganizationInvitationDetailsModal } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-details/organization-invitation-details-modal';
6+
import { renderWithProviders } from '@/tests/utils';
7+
import {
8+
createMockDetailsModalProps,
9+
createMockInvitation,
10+
createMockPendingInvitation,
11+
createMockExpiredInvitation,
12+
createMockRoles,
13+
createMockProviders,
14+
} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
15+
16+
describe('OrganizationInvitationDetailsModal', () => {
17+
afterEach(() => {
18+
vi.clearAllMocks();
19+
});
20+
21+
describe('isOpen', () => {
22+
describe('when is true', () => {
23+
it('should render the modal', () => {
24+
renderWithProviders(
25+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps()} />,
26+
);
27+
28+
expect(screen.getByRole('dialog')).toBeInTheDocument();
29+
expect(
30+
screen.getByRole('heading', { name: 'invitation.details.title' }),
31+
).toBeInTheDocument();
32+
});
33+
});
34+
35+
describe('when is false', () => {
36+
it('should not render the modal content', () => {
37+
renderWithProviders(
38+
<OrganizationInvitationDetailsModal
39+
{...createMockDetailsModalProps({ isOpen: false })}
40+
/>,
41+
);
42+
43+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
44+
});
45+
});
46+
});
47+
48+
describe('invitation', () => {
49+
describe('when invitation is provided', () => {
50+
it('should display the invitee email', () => {
51+
const invitation = createMockInvitation({ invitee: { email: 'user@example.com' } });
52+
53+
renderWithProviders(
54+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
55+
);
56+
57+
expect(screen.getByDisplayValue('user@example.com')).toBeInTheDocument();
58+
});
59+
60+
it('should display the inviter name', () => {
61+
const invitation = createMockInvitation({ inviter: { name: 'John Doe' } });
62+
63+
renderWithProviders(
64+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
65+
);
66+
67+
expect(screen.getByDisplayValue('John Doe')).toBeInTheDocument();
68+
});
69+
70+
it('should display created_at date', () => {
71+
const invitation = createMockInvitation({
72+
created_at: '2024-06-15T10:00:00.000Z',
73+
});
74+
75+
renderWithProviders(
76+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
77+
);
78+
79+
expect(screen.getByText('invitation.details.created_at_label')).toBeInTheDocument();
80+
});
81+
82+
it('should display expires_at date', () => {
83+
const invitation = createMockInvitation({
84+
expires_at: '2025-06-15T10:00:00.000Z',
85+
});
86+
87+
renderWithProviders(
88+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
89+
);
90+
91+
expect(screen.getByText('invitation.details.expires_at_label')).toBeInTheDocument();
92+
});
93+
});
94+
95+
describe('when invitation is null', () => {
96+
it('should handle null invitation gracefully', () => {
97+
renderWithProviders(
98+
<OrganizationInvitationDetailsModal
99+
{...createMockDetailsModalProps({ invitation: null })}
100+
/>,
101+
);
102+
103+
expect(screen.getByRole('dialog')).toBeInTheDocument();
104+
});
105+
});
106+
});
107+
108+
describe('status badge', () => {
109+
it('should display pending status for pending invitations', () => {
110+
renderWithProviders(
111+
<OrganizationInvitationDetailsModal
112+
{...createMockDetailsModalProps({
113+
invitation: createMockPendingInvitation(),
114+
})}
115+
/>,
116+
);
117+
118+
expect(screen.getByText('invitation.table.status_pending')).toBeInTheDocument();
119+
});
120+
121+
it('should display expired status for expired invitations', () => {
122+
renderWithProviders(
123+
<OrganizationInvitationDetailsModal
124+
{...createMockDetailsModalProps({
125+
invitation: createMockExpiredInvitation(),
126+
})}
127+
/>,
128+
);
129+
130+
expect(screen.getByText('invitation.table.status_expired')).toBeInTheDocument();
131+
});
132+
});
133+
134+
describe('roles', () => {
135+
it('should resolve role IDs to names when availableRoles provided', () => {
136+
const invitation = createMockInvitation({ roles: ['role_admin', 'role_member'] });
137+
const availableRoles = createMockRoles();
138+
139+
renderWithProviders(
140+
<OrganizationInvitationDetailsModal
141+
{...createMockDetailsModalProps({ invitation, availableRoles })}
142+
/>,
143+
);
144+
145+
expect(screen.getByText('Admin')).toBeInTheDocument();
146+
expect(screen.getByText('Member')).toBeInTheDocument();
147+
});
148+
149+
it('should show role ID as fallback when role not found in availableRoles', () => {
150+
const invitation = createMockInvitation({ roles: ['role_unknown'] });
151+
152+
renderWithProviders(
153+
<OrganizationInvitationDetailsModal
154+
{...createMockDetailsModalProps({ invitation, availableRoles: [] })}
155+
/>,
156+
);
157+
158+
expect(screen.getByText('role_unknown')).toBeInTheDocument();
159+
});
160+
161+
it('should show dash when no roles assigned', () => {
162+
const invitation = createMockInvitation({ roles: [] });
163+
164+
renderWithProviders(
165+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
166+
);
167+
168+
expect(screen.getByText('invitation.details.roles_label')).toBeInTheDocument();
169+
});
170+
});
171+
172+
describe('invitation URL', () => {
173+
it('should display invitation URL when available', () => {
174+
const invitation = createMockInvitation({
175+
invitation_url: 'https://example.auth0.com/invite?ticket=abc',
176+
});
177+
178+
renderWithProviders(
179+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
180+
);
181+
182+
expect(screen.getByText('invitation.details.invitation_url_label')).toBeInTheDocument();
183+
});
184+
185+
it('should not display invitation URL section when no URL', () => {
186+
const invitation = createMockInvitation({ invitation_url: undefined });
187+
188+
renderWithProviders(
189+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
190+
);
191+
192+
expect(screen.queryByText('invitation.details.invitation_url_label')).not.toBeInTheDocument();
193+
});
194+
});
195+
196+
describe('identity provider', () => {
197+
it('should display provider name when resolved', () => {
198+
const invitation = createMockInvitation({ identity_provider_id: 'con_provider1' });
199+
const availableProviders = createMockProviders();
200+
201+
renderWithProviders(
202+
<OrganizationInvitationDetailsModal
203+
{...createMockDetailsModalProps({ invitation, availableProviders })}
204+
/>,
205+
);
206+
207+
expect(screen.getByDisplayValue('Google')).toBeInTheDocument();
208+
});
209+
210+
it('should show provider ID as fallback when provider not found', () => {
211+
const invitation = createMockInvitation({ identity_provider_id: 'con_unknown' });
212+
213+
renderWithProviders(
214+
<OrganizationInvitationDetailsModal
215+
{...createMockDetailsModalProps({ invitation, availableProviders: [] })}
216+
/>,
217+
);
218+
219+
expect(screen.getByDisplayValue('con_unknown')).toBeInTheDocument();
220+
});
221+
222+
it('should not display provider section when no provider assigned', () => {
223+
const invitation = createMockInvitation({ identity_provider_id: undefined });
224+
225+
renderWithProviders(
226+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ invitation })} />,
227+
);
228+
229+
expect(screen.queryByText('invitation.details.provider_label')).not.toBeInTheDocument();
230+
});
231+
});
232+
233+
describe('readOnly', () => {
234+
describe('when readOnly is false', () => {
235+
it('should show Revoke and Resend buttons', () => {
236+
renderWithProviders(
237+
<OrganizationInvitationDetailsModal
238+
{...createMockDetailsModalProps({ readOnly: false })}
239+
/>,
240+
);
241+
242+
expect(
243+
screen.getByRole('button', { name: 'invitation.details.revoke_button' }),
244+
).toBeInTheDocument();
245+
expect(
246+
screen.getByRole('button', { name: 'invitation.details.resend_button' }),
247+
).toBeInTheDocument();
248+
});
249+
});
250+
251+
describe('when readOnly is true', () => {
252+
it('should not show Revoke and Resend buttons', () => {
253+
renderWithProviders(
254+
<OrganizationInvitationDetailsModal
255+
{...createMockDetailsModalProps({ readOnly: true })}
256+
/>,
257+
);
258+
259+
expect(
260+
screen.queryByRole('button', { name: 'invitation.details.revoke_button' }),
261+
).not.toBeInTheDocument();
262+
expect(
263+
screen.queryByRole('button', { name: 'invitation.details.resend_button' }),
264+
).not.toBeInTheDocument();
265+
});
266+
});
267+
});
268+
269+
describe('action callbacks', () => {
270+
it('should call onRevoke when Revoke button is clicked', async () => {
271+
const user = userEvent.setup();
272+
const onRevoke = vi.fn();
273+
const invitation = createMockPendingInvitation();
274+
275+
renderWithProviders(
276+
<OrganizationInvitationDetailsModal
277+
{...createMockDetailsModalProps({ invitation, onRevoke })}
278+
/>,
279+
);
280+
281+
const revokeButton = screen.getByRole('button', {
282+
name: 'invitation.details.revoke_button',
283+
});
284+
await user.click(revokeButton);
285+
286+
expect(onRevoke).toHaveBeenCalledTimes(1);
287+
expect(onRevoke).toHaveBeenCalledWith(invitation);
288+
});
289+
290+
it('should call onResend when Resend button is clicked', async () => {
291+
const user = userEvent.setup();
292+
const onResend = vi.fn();
293+
const invitation = createMockPendingInvitation();
294+
295+
renderWithProviders(
296+
<OrganizationInvitationDetailsModal
297+
{...createMockDetailsModalProps({ invitation, onResend })}
298+
/>,
299+
);
300+
301+
const resendButton = screen.getByRole('button', {
302+
name: 'invitation.details.resend_button',
303+
});
304+
await user.click(resendButton);
305+
306+
expect(onResend).toHaveBeenCalledTimes(1);
307+
expect(onResend).toHaveBeenCalledWith(invitation);
308+
});
309+
310+
it('should call onClose when Close button is clicked', async () => {
311+
const user = userEvent.setup();
312+
const onClose = vi.fn();
313+
314+
renderWithProviders(
315+
<OrganizationInvitationDetailsModal {...createMockDetailsModalProps({ onClose })} />,
316+
);
317+
318+
const closeButton = screen.getByRole('button', {
319+
name: 'invitation.details.close_button',
320+
});
321+
await user.click(closeButton);
322+
323+
expect(onClose).toHaveBeenCalledTimes(1);
324+
});
325+
});
326+
327+
describe('action in progress', () => {
328+
it('should disable Revoke button when isRevoking is true', () => {
329+
renderWithProviders(
330+
<OrganizationInvitationDetailsModal
331+
{...createMockDetailsModalProps({ isRevoking: true })}
332+
/>,
333+
);
334+
335+
const revokeButton = screen.getByRole('button', {
336+
name: 'invitation.details.revoke_button',
337+
});
338+
expect(revokeButton).toBeDisabled();
339+
});
340+
341+
it('should disable Resend button when isResending is true', () => {
342+
renderWithProviders(
343+
<OrganizationInvitationDetailsModal
344+
{...createMockDetailsModalProps({ isResending: true })}
345+
/>,
346+
);
347+
348+
const resendButton = screen.getByRole('button', {
349+
name: 'invitation.details.resend_button',
350+
});
351+
expect(resendButton).toBeDisabled();
352+
});
353+
354+
it('should disable both buttons when either action is in progress', () => {
355+
renderWithProviders(
356+
<OrganizationInvitationDetailsModal
357+
{...createMockDetailsModalProps({ isRevoking: true })}
358+
/>,
359+
);
360+
361+
const revokeButton = screen.getByRole('button', {
362+
name: 'invitation.details.revoke_button',
363+
});
364+
const resendButton = screen.getByRole('button', {
365+
name: 'invitation.details.resend_button',
366+
});
367+
expect(revokeButton).toBeDisabled();
368+
expect(resendButton).toBeDisabled();
369+
});
370+
});
371+
372+
describe('className', () => {
373+
it('should apply custom class to modal', () => {
374+
const customClass = 'custom-details-class';
375+
376+
renderWithProviders(
377+
<OrganizationInvitationDetailsModal
378+
{...createMockDetailsModalProps({ className: customClass })}
379+
/>,
380+
);
381+
382+
const modalContent = document.querySelector('[data-slot="dialog-content"]');
383+
expect(modalContent).toHaveClass(customClass);
384+
});
385+
});
386+
});

0 commit comments

Comments
 (0)