diff --git a/src/course-unit/data/api.js b/src/course-unit/data/api.js
index d0f2d596d3..7a1b82c124 100644
--- a/src/course-unit/data/api.js
+++ b/src/course-unit/data/api.js
@@ -13,7 +13,7 @@ export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
export const getCourseOutlineInfoUrl = (courseId) => `${getStudioBaseUrl()}/course/${courseId}?format=concise`;
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
-export const acceptLibraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`;
+export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`;
/**
* Get course unit.
@@ -213,7 +213,7 @@ export async function patchUnitItem(sourceLocator, targetParentLocator) {
*/
export async function acceptLibraryBlockChanges(blockId) {
await getAuthenticatedHttpClient()
- .post(acceptLibraryBlockChangesUrl(blockId));
+ .post(libraryBlockChangesUrl(blockId));
}
/**
@@ -222,5 +222,5 @@ export async function acceptLibraryBlockChanges(blockId) {
*/
export async function ignoreLibraryBlockChanges(blockId) {
await getAuthenticatedHttpClient()
- .delete(acceptLibraryBlockChangesUrl(blockId));
+ .delete(libraryBlockChangesUrl(blockId));
}
diff --git a/src/course-unit/preview-changes/index.test.tsx b/src/course-unit/preview-changes/index.test.tsx
new file mode 100644
index 0000000000..f603181a1c
--- /dev/null
+++ b/src/course-unit/preview-changes/index.test.tsx
@@ -0,0 +1,128 @@
+import userEvent from '@testing-library/user-event';
+import MockAdapter from 'axios-mock-adapter/types';
+import {
+ act,
+ render as baseRender,
+ screen,
+ initializeMocks,
+ waitFor,
+} from '../../testUtils';
+
+import PreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.';
+import { messageTypes } from '../constants';
+import { IframeProvider } from '../context/iFrameContext';
+import { libraryBlockChangesUrl } from '../data/api';
+import { ToastActionData } from '../../generic/toast-context';
+
+const usageKey = 'some-id';
+const defaultEventData: LibraryChangesMessageData = {
+ displayName: 'Test block',
+ downstreamBlockId: usageKey,
+ upstreamBlockId: 'some-lib-id',
+ upstreamBlockVersionSynced: 1,
+ isVertical: false,
+};
+
+const mockSendMessageToIframe = jest.fn();
+jest.mock('../context/hooks', () => ({
+ useIframe: () => ({
+ sendMessageToIframe: mockSendMessageToIframe,
+ }),
+}));
+const render = (eventData?: LibraryChangesMessageData) => {
+ baseRender(, {
+ extraWrapper: ({ children }) => { children },
+ });
+ const message = {
+ data: {
+ type: messageTypes.showXBlockLibraryChangesPreview,
+ payload: eventData || defaultEventData,
+ },
+ };
+ // Dispatch showXBlockLibraryChangesPreview message event to open the preivew modal.
+ act(() => {
+ window.dispatchEvent(new MessageEvent('message', message));
+ });
+};
+
+let axiosMock: MockAdapter;
+let mockShowToast: (message: string, action?: ToastActionData | undefined) => void;
+
+describe('', () => {
+ beforeEach(() => {
+ const mocks = initializeMocks();
+ axiosMock = mocks.axiosMock;
+ mockShowToast = mocks.mockShowToast;
+ });
+
+ it('renders modal', async () => {
+ render();
+
+ expect(await screen.findByText('Preview changes: Test block')).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: 'Accept changes' })).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: 'Ignore changes' })).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: 'Cancel' })).toBeInTheDocument();
+ expect(await screen.findByRole('tab', { name: 'New version' })).toBeInTheDocument();
+ expect(await screen.findByRole('tab', { name: 'Old version' })).toBeInTheDocument();
+ });
+
+ it('renders displayName for units', async () => {
+ render({ ...defaultEventData, isVertical: true, displayName: '' });
+
+ expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument();
+ });
+
+ it('renders default displayName for components with no displayName', async () => {
+ render({ ...defaultEventData, displayName: '' });
+
+ expect(await screen.findByText('Preview changes: Component')).toBeInTheDocument();
+ });
+
+ it('accept changes works', async () => {
+ axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
+ render();
+
+ expect(await screen.findByText('Preview changes: Test block')).toBeInTheDocument();
+ const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
+ userEvent.click(acceptBtn);
+ await waitFor(() => {
+ expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
+ expect(axiosMock.history.post.length).toEqual(1);
+ expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
+ });
+ expect(screen.queryByText('Preview changes: Test block')).not.toBeInTheDocument();
+ });
+
+ it('shows toast if accept changes fails', async () => {
+ axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(500, {});
+ render();
+
+ expect(await screen.findByText('Preview changes: Test block')).toBeInTheDocument();
+ const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
+ userEvent.click(acceptBtn);
+ await waitFor(() => {
+ expect(mockSendMessageToIframe).not.toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
+ expect(axiosMock.history.post.length).toEqual(1);
+ expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
+ });
+ expect(screen.queryByText('Preview changes: Test block')).not.toBeInTheDocument();
+ expect(mockShowToast).toHaveBeenCalledWith('Failed to update component');
+ });
+
+ it('ignore changes works', async () => {
+ axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(200, {});
+ render();
+
+ expect(await screen.findByText('Preview changes: Test block')).toBeInTheDocument();
+ const ignoreBtn = await screen.findByRole('button', { name: 'Ignore changes' });
+ userEvent.click(ignoreBtn);
+ const ignoreConfirmBtn = await screen.findByRole('button', { name: 'Ignore' });
+ userEvent.click(ignoreConfirmBtn);
+ await waitFor(() => {
+ expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
+ expect(axiosMock.history.delete.length).toEqual(1);
+ expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
+ });
+ expect(screen.queryByText('Preview changes: Test block')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/course-unit/preview-changes/index.tsx b/src/course-unit/preview-changes/index.tsx
index 231a71a58c..c0eb8cd7fd 100644
--- a/src/course-unit/preview-changes/index.tsx
+++ b/src/course-unit/preview-changes/index.tsx
@@ -13,8 +13,9 @@ import DeleteModal from '../../generic/delete-modal/DeleteModal';
import messages from './messages';
import { ToastContext } from '../../generic/toast-context';
import LoadingButton from '../../generic/loading-button';
+import Loading from '../../generic/Loading';
-interface LibraryChangesMessageData {
+export interface LibraryChangesMessageData {
displayName: string,
downstreamBlockId: string,
upstreamBlockId: string,
@@ -23,16 +24,16 @@ interface LibraryChangesMessageData {
}
const PreviewLibraryXBlockChanges = () => {
- const intl = useIntl();
const { showToast } = useContext(ToastContext);
+ const intl = useIntl();
+
+ const [blockData, setBlockData] = useState(undefined);
// Main preview library modal toggle.
const [isModalOpen, openModal, closeModal] = useToggle(false);
// ignore changes confirmation modal toggle.
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
- const [blockData, setBlockData] = useState(undefined);
-
const acceptChangesMutation = useAcceptLibraryBlockChanges();
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
@@ -54,17 +55,19 @@ const PreviewLibraryXBlockChanges = () => {
const getTitle = useCallback(() => {
if (blockData?.displayName) {
- return blockData?.displayName;
+ return intl.formatMessage(messages.title, {
+ blockTitle: blockData?.displayName,
+ });
}
if (blockData?.isVertical) {
- return 'Unit';
+ return intl.formatMessage(messages.defaultUnitTitle);
}
- return 'Component';
+ return intl.formatMessage(messages.defaultComponentTitle);
}, [blockData]);
const getBody = useCallback(() => {
if (!blockData) {
- return null;
+ return ;
}
return (
{
}, [blockData]);
const updateAndRefresh = useCallback(async (accept: boolean) => {
+ // istanbul ignore if: this should never happen
if (!blockData) {
return;
}
@@ -104,10 +108,7 @@ const PreviewLibraryXBlockChanges = () => {
>
-
+ {getTitle()}
diff --git a/src/course-unit/preview-changes/messages.ts b/src/course-unit/preview-changes/messages.ts
index 5be6788b12..4a35b0a86b 100644
--- a/src/course-unit/preview-changes/messages.ts
+++ b/src/course-unit/preview-changes/messages.ts
@@ -6,6 +6,16 @@ const messages = defineMessages({
defaultMessage: 'Preview changes: {blockTitle}',
description: 'Preview changes modal title text',
},
+ defaultUnitTitle: {
+ id: 'authoring.course-unit.preview-changes.modal-default-unit-title',
+ defaultMessage: 'Preview changes: Unit',
+ description: 'Preview changes modal default title text for units',
+ },
+ defaultComponentTitle: {
+ id: 'authoring.course-unit.preview-changes.modal-default-component-title',
+ defaultMessage: 'Preview changes: Component',
+ description: 'Preview changes modal default title text for components',
+ },
acceptChangesBtn: {
id: 'authoring.course-unit.preview-changes.accept-changes-btn',
defaultMessage: 'Accept changes',