Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ module.exports = createConfig('jest', {
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
// If you want to add config BEFORE jest loads, use setupFiles instead.
setupFilesAfterEnv: [
'<rootDir>/src/setupTest.jsx',
'<rootDir>/src/setupTest.tsx',
],
moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1',
},
coveragePathIgnorePatterns: [
'src/setupTest.jsx',
'src/setupTest.tsx',
'src/i18n',
],
});
8 changes: 7 additions & 1 deletion src/authz-module/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,10 @@
// Move toast to the right
left: auto;
right: var(--pgn-spacing-toast-container-gutter-lg);
}
}

// Fix a bug with a toast on edit tags sheet component: can't click on close toast button
// https://github.com/openedx/frontend-app-authoring/issues/1898
#toast-root[data-focus-on-hidden] {
pointer-events: initial !important;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jest.mock('./context', () => {
LibraryAuthZProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
};
});

const mockedUseLibraryAuthZ = useLibraryAuthZ as jest.Mock;

jest.mock('@src/authz-module/data/hooks', () => ({
Expand Down Expand Up @@ -165,6 +166,10 @@ describe('LibrariesTeamManager', () => {
onSuccess: expect.any(Function),
}),
);
const { onSuccess } = (mutate as jest.Mock).mock.calls[0][1];
onSuccess?.();

expect(await screen.findByText(/updated successfully/i)).toBeInTheDocument();
});

it('should not render the toggle if the user can not manage team and the Library Public Read is disabled', () => {
Expand Down
53 changes: 48 additions & 5 deletions src/authz-module/libraries-manager/LibrariesUserManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ describe('LibrariesUserManager', () => {
await user.click(removeButton);

const onSuccessCallback = mockMutate.mock.calls[0][1].onSuccess;
onSuccessCallback();
onSuccessCallback({ errors: [] });

await waitFor(() => {
expect(screen.getByText(/The Admin role has been successfully removed/)).toBeInTheDocument();
Expand Down Expand Up @@ -278,14 +278,14 @@ describe('LibrariesUserManager', () => {
await user.click(removeButton);

const onSuccessCallback = mockMutate.mock.calls[0][1].onSuccess;
onSuccessCallback();
onSuccessCallback({ errors: [] });

await waitFor(() => {
expect(screen.getByText(/The user no longer has access to this library/)).toBeInTheDocument();
});
});

it('shows error toast when role revocation fails', async () => {
it('shows error toast when role revocation fails with server error', async () => {
const user = userEvent.setup();
renderComponent();

Expand All @@ -302,8 +302,50 @@ describe('LibrariesUserManager', () => {
const onErrorCallback = mockMutate.mock.calls[0][1].onError;
onErrorCallback(new Error('Network error'));

// Wait for the error toast to appear with a retry button
await waitFor(() => {
expect(screen.getByText(/Something went wrong on our end/)).toBeInTheDocument();
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
});

// Second call to mutate also fails
mockMutate.mockImplementationOnce((_vars, { onError }) => {
onError(new Error('Network error'), _vars);
});

// Click retry button
const retryButton = screen.getByRole('button', { name: /retry/i });
await user.click(retryButton);

// The retry toast should appear again
await waitFor(() => {
expect(screen.getAllByText(/Something went wrong/i).length).toBeGreaterThanOrEqual(1);
});

// Ensure mutate was called twice (original + retry)
expect(mockMutate).toHaveBeenCalledTimes(2);
});

it('shows error toast when API fails to remove a role', async () => {
const user = userEvent.setup();

renderComponent();

const deleteButton = screen.getByText('delete-role-Admin');
await user.click(deleteButton);

await waitFor(() => {
expect(screen.getByText('Remove role?')).toBeInTheDocument();
});

const removeButton = screen.getByText('Remove');
await user.click(removeButton);

const { onSuccess } = mockMutate.mock.calls[0][1];
onSuccess({ errors: [{ error: 'role_removal_error' }] });

await waitFor(() => {
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
});
});

Expand All @@ -322,11 +364,12 @@ describe('LibrariesUserManager', () => {
await user.click(removeButton);

const onSuccessCallback = mockMutate.mock.calls[0][1].onSuccess;
onSuccessCallback();
onSuccessCallback({ errors: [] });

await waitFor(() => {
expect(screen.queryByText('Remove role?')).not.toBeInTheDocument();
});
expect(await screen.findByText(/role has been successfully removed/i)).toBeInTheDocument();
});

it('disables delete action when revocation is in progress', async () => {
Expand Down
61 changes: 39 additions & 22 deletions src/authz-module/libraries-manager/LibrariesUserManager.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';
import { Container, Skeleton } from '@openedx/paragon';
import { ROUTES } from '@src/authz-module/constants';
import { Role } from 'types';
Expand Down Expand Up @@ -47,7 +46,9 @@ const LibrariesUserManager = () => {

const [roleToDelete, setRoleToDelete] = useState<Role | null>(null);
const [showConfirmDeletionModal, setShowConfirmDeletionModal] = useState(false);
const { handleShowToast, handleDiscardToast } = useToastManager();
const {
showToast, showErrorToast, Bold, Br,
} = useToastManager();

const {
data: teamMember, isLoading: isLoadingTeamMember, isFetching: isFetchingMember,
Expand Down Expand Up @@ -78,7 +79,6 @@ const LibrariesUserManager = () => {
const handleShowConfirmDeletionModal = (role: Role) => {
if (isRevokingUserRole) { return; }

handleDiscardToast();
setRoleToDelete(role);
setShowConfirmDeletionModal(true);
};
Expand All @@ -92,25 +92,42 @@ const LibrariesUserManager = () => {
scope: libraryId,
};

revokeUserRoles({ data }, {
onSuccess: () => {
const remainingRolesCount = userRoles.length - 1;
handleShowToast(intl.formatMessage(
messages['library.authz.team.remove.user.toast.success.description'],
{
role: roleToDelete.name,
rolesCount: remainingRolesCount,
},
));
handleCloseConfirmDeletionModal();
},
onError: (error) => {
logError(error);
// eslint-disable-next-line react/no-unstable-nested-components
handleShowToast(intl.formatMessage(messages['library.authz.team.default.error.toast.message'], { b: chunk => <b>{chunk}</b>, br: () => <br /> }));
handleCloseConfirmDeletionModal();
},
});
const runRevokeRole = (variables = { data }) => {
revokeUserRoles(variables, {
onSuccess: (response) => {
const { errors } = response;

if (errors.length) {
showToast({
type: 'error',
message: intl.formatMessage(
messages['library.authz.team.toast.default.error.message'],
{ Bold, Br },
),
});
return;
}

const remainingRolesCount = userRoles.length - 1;
showToast({
message: intl.formatMessage(
messages['library.authz.team.remove.user.toast.success.description'],
{
role: roleToDelete.name,
rolesCount: remainingRolesCount,
},
),
type: 'success',
});
},
onError: (error, retryVariables) => {
showErrorToast(error, () => runRevokeRole(retryVariables));
},
});
};

handleCloseConfirmDeletionModal();
runRevokeRole();
};

return (
Expand Down
Loading