diff --git a/src/hooks.test.ts b/src/hooks.test.ts
index e4b06b16bf..fbec7eda88 100644
--- a/src/hooks.test.ts
+++ b/src/hooks.test.ts
@@ -85,6 +85,8 @@ describe('Custom Hooks', () => {
fireEvent.scroll(window);
+ // Called on scroll once and then due to content being less than screen height
+ // and hasNextPage being true.
expect(fetchNextPage).toHaveBeenCalledTimes(2);
});
diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx
index 48a8c6bab4..990cbcb4b5 100644
--- a/src/library-authoring/LibraryAuthoringPage.test.tsx
+++ b/src/library-authoring/LibraryAuthoringPage.test.tsx
@@ -9,10 +9,6 @@ import {
waitFor,
within,
} from '../testUtils';
-import { executeThunk } from '../utils';
-import initializeStore from '../store';
-import { getApiWaffleFlagsUrl } from '../data/api';
-import { fetchWaffleFlags } from '../data/thunks';
import mockResult from './__mocks__/library-search.json';
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
import {
@@ -27,6 +23,7 @@ import { getStudioHomeApiUrl } from '../studio-home/data/api';
import { mockBroadcastChannel } from '../generic/data/api.mock';
import { LibraryLayout } from '.';
import { getLibraryCollectionsApiUrl } from './data/api';
+import { getApiWaffleFlagsUrl } from '../data/api';
mockGetCollectionMetadata.applyMock();
mockContentSearchConfig.applyMock();
@@ -54,17 +51,12 @@ const returnEmptyResult = (_url, req) => {
const path = '/library/:libraryId/*';
const libraryTitle = mockContentLibrary.libraryData.title;
-let store;
describe('', () => {
beforeEach(async () => {
const { axiosMock } = initializeMocks();
- store = initializeStore();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
- axiosMock
- .onGet(getApiWaffleFlagsUrl())
- .reply(200, {});
- await executeThunk(fetchWaffleFlags(), store.dispatch);
+ axiosMock.onGet(getApiWaffleFlagsUrl()).reply(200, {});
// The Meilisearch client-side API uses fetch, not Axios.
fetchMock.mockReset();
@@ -689,17 +681,15 @@ describe('', () => {
it('Shows an error if libraries V2 is disabled', async () => {
const { axiosMock } = initializeMocks();
+ axiosMock.onGet(getApiWaffleFlagsUrl()).reply(200, {});
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, {
...studioHomeMock,
libraries_v2_enabled: false,
});
- axiosMock
- .onGet(getApiWaffleFlagsUrl())
- .reply(200, {});
- await executeThunk(fetchWaffleFlags(), store.dispatch);
render(, { path, params: { libraryId: mockContentLibrary.libraryId } });
- await waitFor(() => { expect(axiosMock.history.get.length).toBe(4); });
- expect(screen.getByRole('alert')).toHaveTextContent('This page cannot be shown: Libraries v2 are disabled.');
+ expect(await screen.findByRole('alert')).toHaveTextContent(
+ 'This page cannot be shown: Libraries v2 are disabled.',
+ );
});
});
diff --git a/src/library-authoring/add-content/AddContentContainer.test.tsx b/src/library-authoring/add-content/AddContentContainer.test.tsx
index 9587576a98..4798d271ad 100644
--- a/src/library-authoring/add-content/AddContentContainer.test.tsx
+++ b/src/library-authoring/add-content/AddContentContainer.test.tsx
@@ -1,3 +1,5 @@
+import MockAdapter from 'axios-mock-adapter/types';
+import { snakeCaseObject } from '@edx/frontend-platform';
import {
fireEvent,
render as baseRender,
@@ -6,13 +8,21 @@ import {
initializeMocks,
} from '../../testUtils';
import { mockContentLibrary } from '../data/api.mocks';
-import { getCreateLibraryBlockUrl, getLibraryCollectionComponentApiUrl, getLibraryPasteClipboardUrl } from '../data/api';
+import {
+ getContentLibraryApiUrl, getCreateLibraryBlockUrl, getLibraryCollectionComponentApiUrl, getLibraryPasteClipboardUrl,
+} from '../data/api';
import { mockBroadcastChannel, mockClipboardEmpty, mockClipboardHtml } from '../../generic/data/api.mock';
import { LibraryProvider } from '../common/context';
import AddContentContainer from './AddContentContainer';
+import { ComponentEditorModal } from '../components/ComponentEditorModal';
+import editorCmsApi from '../../editors/data/services/cms/api';
+import { ToastActionData } from '../../generic/toast-context';
mockBroadcastChannel();
+// Mocks for ComponentEditorModal to work in tests.
+jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCss: '' }));
+
const { libraryId } = mockContentLibrary;
const render = (collectionId?: string) => {
const params: { libraryId: string, collectionId?: string } = { libraryId };
@@ -26,15 +36,27 @@ const render = (collectionId?: string) => {
{ children }
+ >
+ { children }
+
),
});
};
+let axiosMock: MockAdapter;
+let mockShowToast: (message: string, action?: ToastActionData | undefined) => void;
describe('', () => {
+ beforeEach(() => {
+ const mocks = initializeMocks();
+ axiosMock = mocks.axiosMock;
+ mockShowToast = mocks.mockShowToast;
+ axiosMock.onGet(getContentLibraryApiUrl(libraryId)).reply(200, {});
+ });
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
it('should render content buttons', () => {
- initializeMocks();
mockClipboardEmpty.applyMock();
render();
expect(screen.queryByRole('button', { name: /collection/i })).toBeInTheDocument();
@@ -48,7 +70,6 @@ describe('', () => {
});
it('should create a content', async () => {
- const { axiosMock } = initializeMocks();
mockClipboardEmpty.applyMock();
const url = getCreateLibraryBlockUrl(libraryId);
axiosMock.onPost(url).reply(200);
@@ -62,8 +83,7 @@ describe('', () => {
await waitFor(() => expect(axiosMock.history.patch.length).toEqual(0));
});
- it('should create a content in a collection', async () => {
- const { axiosMock } = initializeMocks();
+ it('should create a content in a collection for non-editable blocks', async () => {
mockClipboardEmpty.applyMock();
const collectionId = 'some-collection-id';
const url = getCreateLibraryBlockUrl(libraryId);
@@ -71,6 +91,7 @@ describe('', () => {
libraryId,
collectionId,
);
+ // having id of block which is not video, html or problem will not trigger editor.
axiosMock.onPost(url).reply(200, { id: 'some-component-id' });
axiosMock.onPatch(collectionComponentUrl).reply(200);
@@ -84,8 +105,57 @@ describe('', () => {
await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(collectionComponentUrl));
});
+ it('should create a content in a collection for editable blocks', async () => {
+ mockClipboardEmpty.applyMock();
+ const collectionId = 'some-collection-id';
+ const url = getCreateLibraryBlockUrl(libraryId);
+ const collectionComponentUrl = getLibraryCollectionComponentApiUrl(
+ libraryId,
+ collectionId,
+ );
+ // Mocks for ComponentEditorModal to work in tests.
+ jest.spyOn(editorCmsApi, 'fetchImages').mockImplementation(async () => ( // eslint-disable-next-line
+ { data: { assets: [], start: 0, end: 0, page: 0, pageSize: 50, totalCount: 0 } }
+ ));
+ jest.spyOn(editorCmsApi, 'fetchByUnitId').mockImplementation(async () => ({
+ status: 200,
+ data: {
+ ancestors: [{
+ id: 'block-v1:Org+TS100+24+type@vertical+block@parent',
+ display_name: 'You-Knit? The Test Unit',
+ category: 'vertical',
+ has_children: true,
+ }],
+ },
+ }));
+
+ axiosMock.onPost(url).reply(200, {
+ id: 'lb:OpenedX:CSPROB2:html:1a5efd56-4ee5-4df0-b466-44f08fbbf567',
+ });
+ const fieldsHtml = {
+ displayName: 'Introduction to Testing',
+ data: '
This is a text component which uses HTML.
',
+ metadata: { displayName: 'Introduction to Testing' },
+ };
+ jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => (
+ { status: 200, data: snakeCaseObject(fieldsHtml) }
+ ));
+ axiosMock.onPatch(collectionComponentUrl).reply(200);
+
+ render(collectionId);
+
+ const textButton = screen.getByRole('button', { name: /text/i });
+ fireEvent.click(textButton);
+
+ // Component should be linked to Collection on closing editor.
+ const closeButton = await screen.findByRole('button', { name: 'Exit the editor' });
+ fireEvent.click(closeButton);
+ await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
+ await waitFor(() => expect(axiosMock.history.patch.length).toEqual(1));
+ await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(collectionComponentUrl));
+ });
+
it('should render paste button if clipboard contains pastable xblock', async () => {
- initializeMocks();
// Simulate having an HTML block in the clipboard:
const getClipboardSpy = mockClipboardHtml.applyMock();
render();
@@ -94,7 +164,6 @@ describe('', () => {
});
it('should paste content', async () => {
- const { axiosMock } = initializeMocks();
// Simulate having an HTML block in the clipboard:
const getClipboardSpy = mockClipboardHtml.applyMock();
@@ -112,7 +181,6 @@ describe('', () => {
});
it('should paste content inside a collection', async () => {
- const { axiosMock } = initializeMocks();
// Simulate having an HTML block in the clipboard:
const getClipboardSpy = mockClipboardHtml.applyMock();
@@ -138,7 +206,6 @@ describe('', () => {
});
it('should show error toast on linking failure', async () => {
- const { axiosMock, mockShowToast } = initializeMocks();
// Simulate having an HTML block in the clipboard:
const getClipboardSpy = mockClipboardHtml.applyMock();
@@ -165,7 +232,6 @@ describe('', () => {
});
it('should stop user from pasting unsupported blocks and show toast', async () => {
- const { axiosMock, mockShowToast } = initializeMocks();
// Simulate having an HTML block in the clipboard:
mockClipboardHtml.applyMock('openassessment');
@@ -214,7 +280,6 @@ describe('', () => {
])('$label', async ({
mockUrl, mockResponse, buttonName, expectedError,
}) => {
- const { axiosMock, mockShowToast } = initializeMocks();
axiosMock.onPost(mockUrl).reply(400, mockResponse);
// Simulate having an HTML block in the clipboard:
diff --git a/src/library-authoring/add-content/AddContentContainer.tsx b/src/library-authoring/add-content/AddContentContainer.tsx
index 0dbb2da4c0..0afcd43309 100644
--- a/src/library-authoring/add-content/AddContentContainer.tsx
+++ b/src/library-authoring/add-content/AddContentContainer.tsx
@@ -166,9 +166,7 @@ const AddContentContainer = () => {
}
const linkComponent = (usageKey: string) => {
- updateComponentsMutation.mutateAsync([usageKey]).then(() => {
- showToast(intl.formatMessage(messages.successAssociateComponentMessage));
- }).catch(() => {
+ updateComponentsMutation.mutateAsync([usageKey]).catch(() => {
showToast(intl.formatMessage(messages.errorAssociateComponentMessage));
});
};
@@ -199,13 +197,14 @@ const AddContentContainer = () => {
blockType,
definitionId: `${uuid4()}`,
}).then((data) => {
- linkComponent(data.id);
const hasEditor = canEditComponent(data.id);
if (hasEditor) {
- openComponentEditor(data.id);
+ // linkComponent on editor close.
+ openComponentEditor(data.id, () => linkComponent(data.id));
} else {
// We can't start editing this right away so just show a toast message:
showToast(intl.formatMessage(messages.successCreateMessage));
+ linkComponent(data.id);
}
}).catch((error) => {
showToast(parseErrorMsg(
@@ -228,14 +227,11 @@ const AddContentContainer = () => {
}
};
+ /* istanbul ignore next */
if (pasteClipboardMutation.isLoading) {
showToast(intl.formatMessage(messages.pastingClipboardMessage));
}
- if (updateComponentsMutation.isLoading) {
- showToast(intl.formatMessage(messages.linkingComponentMessage));
- }
-
return (
{collectionId ? (
diff --git a/src/library-authoring/add-content/AddContentWorkflow.test.tsx b/src/library-authoring/add-content/AddContentWorkflow.test.tsx
index 92f67f2d5e..2e0818172b 100644
--- a/src/library-authoring/add-content/AddContentWorkflow.test.tsx
+++ b/src/library-authoring/add-content/AddContentWorkflow.test.tsx
@@ -17,14 +17,11 @@ import {
mockCreateLibraryBlock,
mockXBlockFields,
} from '../data/api.mocks';
-import initializeStore from '../../store';
-import { executeThunk } from '../../utils';
import { mockBroadcastChannel, mockClipboardEmpty } from '../../generic/data/api.mock';
import { mockContentSearchConfig, mockSearchResult } from '../../search-manager/data/api.mock';
import { studioHomeMock } from '../../studio-home/__mocks__';
import { getStudioHomeApiUrl } from '../../studio-home/data/api';
import { getApiWaffleFlagsUrl } from '../../data/api';
-import { fetchWaffleFlags } from '../../data/thunks';
import LibraryLayout from '../LibraryLayout';
mockContentSearchConfig.applyMock();
@@ -51,17 +48,11 @@ const renderOpts = {
routerProps: { initialEntries: [`/library/${libraryId}/components`] },
};
-let store;
-
describe('AddContentWorkflow test', () => {
beforeEach(async () => {
const { axiosMock } = initializeMocks();
- store = initializeStore();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
- axiosMock
- .onGet(getApiWaffleFlagsUrl())
- .reply(200, {});
- await executeThunk(fetchWaffleFlags(), store.dispatch);
+ axiosMock.onGet(getApiWaffleFlagsUrl()).reply(200, {});
});
it('can create an HTML component', async () => {
diff --git a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx
index aa9f2c6c1f..97d37f63ad 100644
--- a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx
+++ b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx
@@ -6,8 +6,6 @@ import {
screen,
initializeMocks,
} from '../../testUtils';
-import initializeStore from '../../store';
-import { executeThunk } from '../../utils';
import { studioHomeMock } from '../../studio-home/__mocks__';
import { getStudioHomeApiUrl } from '../../studio-home/data/api';
import mockResult from '../__mocks__/library-search.json';
@@ -20,7 +18,6 @@ import {
} from '../data/api.mocks';
import { PickLibraryContentModal } from './PickLibraryContentModal';
import { getApiWaffleFlagsUrl } from '../../data/api';
-import { fetchWaffleFlags } from '../../data/thunks';
mockContentSearchConfig.applyMock();
mockContentLibrary.applyMock();
@@ -31,7 +28,6 @@ const { libraryId } = mockContentLibrary;
const onClose = jest.fn();
let mockShowToast: (message: string) => void;
-let store;
const render = () => baseRender(, {
path: '/library/:libraryId/collection/:collectionId/*',
@@ -50,13 +46,9 @@ const render = () => baseRender(', () => {
beforeEach(async () => {
const mocks = initializeMocks();
- store = initializeStore();
mockShowToast = mocks.mockShowToast;
mocks.axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
- mocks.axiosMock
- .onGet(getApiWaffleFlagsUrl())
- .reply(200, {});
- await executeThunk(fetchWaffleFlags(), store.dispatch);
+ mocks.axiosMock.onGet(getApiWaffleFlagsUrl()).reply(200, {});
});
it('can pick components from the modal', async () => {
diff --git a/src/library-authoring/add-content/messages.ts b/src/library-authoring/add-content/messages.ts
index a720c12ce6..898073eb49 100644
--- a/src/library-authoring/add-content/messages.ts
+++ b/src/library-authoring/add-content/messages.ts
@@ -74,11 +74,6 @@ const messages = defineMessages({
+ ' The {detail} text provides more information about the error.'
),
},
- linkingComponentMessage: {
- id: 'course-authoring.library-authoring.linking-collection-content.progress.text',
- defaultMessage: 'Adding component to collection...',
- description: 'Message when component is being linked to collection in library',
- },
successAssociateComponentMessage: {
id: 'course-authoring.library-authoring.associate-collection-content.success.text',
defaultMessage: 'Content linked successfully.',
diff --git a/src/library-authoring/common/context.tsx b/src/library-authoring/common/context.tsx
index be229065f5..ef1fb7ef1c 100644
--- a/src/library-authoring/common/context.tsx
+++ b/src/library-authoring/common/context.tsx
@@ -68,6 +68,11 @@ export interface SidebarComponentInfo {
additionalAction?: SidebarAdditionalActions;
}
+export interface ComponentEditorInfo {
+ usageKey: string;
+ onClose?: () => void;
+}
+
export enum SidebarAdditionalActions {
JumpToAddCollections = 'jump-to-add-collections',
}
@@ -99,9 +104,10 @@ export type LibraryContextData = {
// Current collection
openCollectionInfoSidebar: (collectionId: string, additionalAction?: SidebarAdditionalActions) => void;
// Editor modal - for editing some component
- /** If the editor is open and the user is editing some component, this is its usageKey */
- componentBeingEdited: string | undefined;
- openComponentEditor: (usageKey: string) => void;
+ /** If the editor is open and the user is editing some component, this is the component being edited. */
+ componentBeingEdited: ComponentEditorInfo | undefined;
+ /** If an onClose callback is provided, it will be called when the editor is closed. */
+ openComponentEditor: (usageKey: string, onClose?: () => void) => void;
closeComponentEditor: () => void;
resetSidebarAdditionalActions: () => void;
} & ComponentPickerType;
@@ -174,8 +180,16 @@ export const LibraryProvider = ({
);
const [isLibraryTeamModalOpen, openLibraryTeamModal, closeLibraryTeamModal] = useToggle(false);
const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false);
- const [componentBeingEdited, openComponentEditor] = useState();
- const closeComponentEditor = useCallback(() => openComponentEditor(undefined), []);
+ const [componentBeingEdited, setComponentBeingEdited] = useState();
+ const closeComponentEditor = useCallback(() => {
+ setComponentBeingEdited((prev) => {
+ prev?.onClose?.();
+ return undefined;
+ });
+ }, []);
+ const openComponentEditor = useCallback((usageKey: string, onClose?: () => void) => {
+ setComponentBeingEdited({ usageKey, onClose });
+ }, []);
const [selectedComponents, setSelectedComponents] = useState([]);
diff --git a/src/library-authoring/components/ComponentEditorModal.tsx b/src/library-authoring/components/ComponentEditorModal.tsx
index 9dfcec1637..0883ab6dc5 100644
--- a/src/library-authoring/components/ComponentEditorModal.tsx
+++ b/src/library-authoring/components/ComponentEditorModal.tsx
@@ -28,18 +28,18 @@ export const ComponentEditorModal: React.FC> = () => {
if (componentBeingEdited === undefined) {
return null;
}
- const blockType = getBlockType(componentBeingEdited);
+ const blockType = getBlockType(componentBeingEdited.usageKey);
const onClose = () => {
closeComponentEditor();
- invalidateComponentData(queryClient, libraryId, componentBeingEdited);
+ invalidateComponentData(queryClient, libraryId, componentBeingEdited.usageKey);
};
return (