Surface errors via toasts while keeping the modal open. This ensures they can retry or correct input without losing context, improving usability and reducing frustration.
379 lines
13 KiB
TypeScript
379 lines
13 KiB
TypeScript
import React from 'react';
|
|
import { screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { renderWrapper } from '@src/setupTest';
|
|
import { useAssignTeamMembersRole } from '@src/authz-module/data/hooks';
|
|
import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext';
|
|
import AddNewTeamMemberTrigger from './AddNewTeamMemberTrigger';
|
|
|
|
jest.mock('@edx/frontend-platform/logging');
|
|
|
|
const mockMutate = jest.fn();
|
|
|
|
// Mock the hooks module
|
|
jest.mock('@src/authz-module/data/hooks', () => ({
|
|
useAssignTeamMembersRole: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('./AddNewTeamMemberModal', () => {
|
|
/* eslint-disable react/prop-types */
|
|
const MockModal = ({
|
|
isOpen, close, onSave, isLoading, formValues, handleChangeForm,
|
|
}) => (
|
|
isOpen ? (
|
|
<div data-testid="add-team-member-modal">
|
|
<button type="button" onClick={close} data-testid="close-modal">Close</button>
|
|
<button type="button" onClick={onSave} data-testid="save-modal">Save</button>
|
|
<textarea
|
|
name="users"
|
|
value={formValues?.users || ''}
|
|
onChange={handleChangeForm}
|
|
data-testid="users-input"
|
|
/>
|
|
<select
|
|
name="role"
|
|
value={formValues?.role || ''}
|
|
onChange={handleChangeForm}
|
|
data-testid="role-select"
|
|
>
|
|
<option value="">Select role</option>
|
|
<option value="admin">Admin</option>
|
|
<option value="editor">Editor</option>
|
|
</select>
|
|
{isLoading && <div data-testid="loading-indicator">Loading...</div>}
|
|
</div>
|
|
) : null
|
|
);
|
|
/* eslint-enable react/prop-types */
|
|
return MockModal;
|
|
});
|
|
|
|
describe('AddNewTeamMemberTrigger', () => {
|
|
const mockLibraryId = 'lib:123';
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
(useAssignTeamMembersRole as jest.Mock).mockReturnValue({
|
|
mutate: mockMutate,
|
|
isPending: false,
|
|
isError: false,
|
|
isSuccess: false,
|
|
} as any);
|
|
});
|
|
|
|
it('renders the trigger button', () => {
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const button = screen.getByRole('button', { name: /add new team member/i });
|
|
expect(button).toBeInTheDocument();
|
|
});
|
|
|
|
it('opens modal when trigger button is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
|
|
});
|
|
|
|
it('closes modal when close button is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
|
|
|
|
const closeButton = screen.getByTestId('close-modal');
|
|
await user.click(closeButton);
|
|
|
|
expect(screen.queryByTestId('add-team-member-modal')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('calls addTeamMember with correct data when save is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
const usersInput = screen.getByTestId('users-input');
|
|
const roleSelect = screen.getByTestId('role-select');
|
|
const saveButton = screen.getByTestId('save-modal');
|
|
|
|
await user.type(usersInput, 'alice@example.com, bob@example.com');
|
|
await user.selectOptions(roleSelect, 'editor');
|
|
await user.click(saveButton);
|
|
|
|
expect(mockMutate).toHaveBeenCalledWith(
|
|
{
|
|
data: {
|
|
users: ['alice@example.com', 'bob@example.com'],
|
|
role: 'editor',
|
|
scope: mockLibraryId,
|
|
},
|
|
},
|
|
expect.objectContaining({
|
|
onSuccess: expect.any(Function),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('displays success toast and closes modal on successful addition with no errors', async () => {
|
|
const user = userEvent.setup();
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
const saveButton = screen.getByTestId('save-modal');
|
|
await user.click(saveButton);
|
|
|
|
// Simulate successful response with no errors
|
|
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
|
onSuccess({
|
|
completed: [
|
|
{ userIdentifier: 'alice@example.com', status: 'role_added' },
|
|
{ userIdentifier: 'bob@example.com', status: 'added_to_team' },
|
|
],
|
|
errors: [],
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('add-team-member-modal')).not.toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.getByText('2 team members added successfully.')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays mixed success and error toast on partial success', async () => {
|
|
const user = userEvent.setup();
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
const saveButton = screen.getByTestId('save-modal');
|
|
await user.click(saveButton);
|
|
|
|
// Simulate partial success response
|
|
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
|
onSuccess({
|
|
completed: [
|
|
{ userIdentifier: 'alice@example.com', status: 'role_added' },
|
|
],
|
|
errors: [
|
|
{ userIdentifier: 'unknown@example.com', error: 'user_not_found' },
|
|
],
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/1 team member added successfully/)).toBeInTheDocument();
|
|
expect(screen.getByText(/We couldn't find a user for 1 email address or username/)).toBeInTheDocument();
|
|
});
|
|
|
|
// Modal should remain open when there are errors
|
|
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays only error toast when all additions fail', async () => {
|
|
const user = userEvent.setup();
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
const saveButton = screen.getByTestId('save-modal');
|
|
await user.click(saveButton);
|
|
|
|
// Simulate all failed response
|
|
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
|
onSuccess({
|
|
completed: [],
|
|
errors: [
|
|
{ userIdentifier: 'unknown1@example.com', error: 'user_not_found' },
|
|
{ userIdentifier: 'unknown2@example.com', error: 'user_not_found' },
|
|
],
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/We couldn't find a user for 2 email addresses or usernames/)).toBeInTheDocument();
|
|
});
|
|
|
|
// Modal should remain open when there are errors
|
|
expect(screen.getByTestId('add-team-member-modal')).toBeInTheDocument();
|
|
});
|
|
|
|
it('resets form values after successful addition with no errors', async () => {
|
|
const user = userEvent.setup();
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
const usersInput = screen.getByTestId('users-input');
|
|
const roleSelect = screen.getByTestId('role-select');
|
|
const saveButton = screen.getByTestId('save-modal');
|
|
|
|
await user.type(usersInput, 'alice@example.com');
|
|
await user.selectOptions(roleSelect, 'editor');
|
|
await user.click(saveButton);
|
|
|
|
// Simulate successful response with no errors
|
|
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
|
onSuccess({
|
|
completed: [{ userIdentifier: 'alice@example.com', status: 'role_added' }],
|
|
errors: [],
|
|
});
|
|
|
|
// Open modal again to check if form is reset
|
|
await user.click(triggerButton);
|
|
|
|
const newUsersInput = screen.getByTestId('users-input');
|
|
const newRoleSelect = screen.getByTestId('role-select');
|
|
|
|
expect(newUsersInput).toHaveValue('');
|
|
expect(newRoleSelect).toHaveValue('');
|
|
});
|
|
|
|
it('allows closing the success/error toast message', async () => {
|
|
const user = userEvent.setup();
|
|
renderWrapper(<ToastManagerProvider><AddNewTeamMemberTrigger libraryId={mockLibraryId} /></ToastManagerProvider>);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
const saveButton = screen.getByTestId('save-modal');
|
|
await user.click(saveButton);
|
|
|
|
// Simulate successful response
|
|
const [, { onSuccess }] = mockMutate.mock.calls[0];
|
|
onSuccess({
|
|
completed: [{ userIdentifier: 'alice@example.com', status: 'role_added' }],
|
|
errors: [],
|
|
});
|
|
|
|
// Toast should be visible
|
|
await waitFor(() => {
|
|
expect(screen.getByText('1 team member added successfully.')).toBeInTheDocument();
|
|
});
|
|
|
|
// Find and close the toast
|
|
const toastCloseButton = screen.getByLabelText(/close/i);
|
|
await user.click(toastCloseButton);
|
|
|
|
// Toast should be removed
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('1 team member added successfully.')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows retry toast on API failure and displays another toast when retry fails again', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
const mockError = new Error('Network error');
|
|
|
|
mockMutate.mockImplementationOnce((_vars, { onError }) => {
|
|
onError(mockError, _vars);
|
|
});
|
|
|
|
renderWrapper(
|
|
<ToastManagerProvider>
|
|
<AddNewTeamMemberTrigger libraryId={mockLibraryId} />
|
|
</ToastManagerProvider>,
|
|
);
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
const saveButton = screen.getByTestId('save-modal');
|
|
await user.click(saveButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
|
|
});
|
|
|
|
mockMutate.mockImplementationOnce((_vars, { onError }) => {
|
|
onError(new Error('Network error'), _vars);
|
|
});
|
|
|
|
const retryButton = screen.getByRole('button', { name: /retry/i });
|
|
await user.click(retryButton);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
|
|
});
|
|
|
|
// Ensure mutate was called twice (original + retry)
|
|
expect(mockMutate).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('displays loading state when adding team member', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
const { rerender } = renderWrapper(
|
|
<ToastManagerProvider>
|
|
<AddNewTeamMemberTrigger libraryId="lib:123" />
|
|
</ToastManagerProvider>,
|
|
);
|
|
|
|
const rerenderHook = () => rerender(
|
|
<ToastManagerProvider>
|
|
<AddNewTeamMemberTrigger libraryId="lib:123" />
|
|
</ToastManagerProvider>,
|
|
);
|
|
|
|
let isPending = false;
|
|
const mutateMock = jest.fn((_args, { onSuccess }) => {
|
|
isPending = true;
|
|
rerenderHook();
|
|
setTimeout(() => {
|
|
isPending = false;
|
|
rerenderHook();
|
|
onSuccess?.({
|
|
completed: [{ userIdentifier: _args.data.users[0], status: 'role_added' }],
|
|
errors: [],
|
|
});
|
|
}, 10);
|
|
});
|
|
|
|
(useAssignTeamMembersRole as jest.Mock).mockImplementation(() => ({
|
|
mutate: mutateMock,
|
|
isPending,
|
|
isError: false,
|
|
isSuccess: false,
|
|
}));
|
|
|
|
const triggerButton = screen.getByRole('button', { name: /add new team member/i });
|
|
await user.click(triggerButton);
|
|
|
|
const userInput = screen.getByTestId('users-input');
|
|
const roleSelect = screen.getByTestId('role-select');
|
|
await user.type(userInput, 'alice@example.com');
|
|
await user.selectOptions(roleSelect, 'editor');
|
|
|
|
const saveButton = screen.getByTestId('save-modal');
|
|
await user.click(saveButton);
|
|
|
|
// should now reflect isPending = true
|
|
const loadingIndicator = await screen.findByTestId('loading-indicator');
|
|
expect(loadingIndicator).toBeInTheDocument();
|
|
expect(loadingIndicator).toHaveTextContent('Loading...');
|
|
|
|
expect(mutateMock).toHaveBeenCalledWith(
|
|
{
|
|
data: {
|
|
users: ['alice@example.com'],
|
|
role: 'editor',
|
|
scope: 'lib:123',
|
|
},
|
|
},
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
});
|