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',