Skip to content

Commit

Permalink
test: refactor add content library test
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoa committed Jan 21, 2025
1 parent ceb9f39 commit 3dd53d4
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ INVITE_STUDENTS_EMAIL_TO="[email protected]"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
LIBRARY_SUPPORTED_BLOCKS="problem,video,html"
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,other"
74 changes: 31 additions & 43 deletions src/library-authoring/add-content/AddContentContainer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import MockAdapter from 'axios-mock-adapter/types';
import { snakeCaseObject } from '@edx/frontend-platform';
import {
fireEvent,
render as baseRender,
screen,
waitFor,
initializeMocks,
} from '../../testUtils';
import { mockContentLibrary } from '../data/api.mocks';
import { mockContentLibrary, mockXBlockFields } from '../data/api.mocks';
import {
getContentLibraryApiUrl, getCreateLibraryBlockUrl, getLibraryCollectionComponentApiUrl, getLibraryPasteClipboardUrl,
getXBlockFieldsApiUrl,
} from '../data/api';
import { mockBroadcastChannel, mockClipboardEmpty, mockClipboardHtml } from '../../generic/data/api.mock';
import { LibraryProvider } from '../common/context/LibraryContext';
import AddContentContainer from './AddContentContainer';
import { ComponentEditorModal } from '../components/ComponentEditorModal';
import editorCmsApi from '../../editors/data/services/cms/api';
import { ToastActionData } from '../../generic/toast-context';
import * as textEditorHooks from '../../editors/containers/TextEditor/hooks';

mockBroadcastChannel();
// mockCreateLibraryBlock.applyMock();

// Mocks for ComponentEditorModal to work in tests.
jest.mock('frontend-components-tinymce-advanced-plugins', () => ({ a11ycheckerCss: '' }));
Expand Down Expand Up @@ -48,6 +50,7 @@ describe('<AddContentContainer />', () => {
axiosMock = mocks.axiosMock;
mockShowToast = mocks.mockShowToast;
axiosMock.onGet(getContentLibraryApiUrl(libraryId)).reply(200, {});
jest.spyOn(textEditorHooks, 'getContent').mockImplementation(() => () => '<p>Edited HTML content</p>');
});
afterEach(() => {
jest.restoreAllMocks();
Expand All @@ -61,11 +64,11 @@ describe('<AddContentContainer />', () => {
expect(screen.queryByRole('button', { name: /open reponse/i })).not.toBeInTheDocument(); // Excluded from MVP
expect(screen.queryByRole('button', { name: /drag drop/i })).not.toBeInTheDocument(); // Excluded from MVP
expect(screen.queryByRole('button', { name: /video/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /advanced \/ other/i })).not.toBeInTheDocument(); // Excluded from MVP
expect(screen.queryByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument(); // To test not editor supported blocks
expect(screen.queryByRole('button', { name: /copy from clipboard/i })).not.toBeInTheDocument();
});

it('should create a content', async () => {
it('should open the editor modal to create a content when the block is supported', async () => {
mockClipboardEmpty.applyMock();
const url = getCreateLibraryBlockUrl(libraryId);
axiosMock.onPost(url).reply(200);
Expand All @@ -74,7 +77,16 @@ describe('<AddContentContainer />', () => {

const textButton = screen.getByRole('button', { name: /text/i });
fireEvent.click(textButton);
expect(await screen.findByRole('heading', { name: /Text/ })).toBeInTheDocument();
});

it('should create a content when the block is not supported by the editor', async () => {
mockClipboardEmpty.applyMock();
const url = getCreateLibraryBlockUrl(libraryId);
axiosMock.onPost(url).reply(200);
render();
const textButton = screen.getByRole('button', { name: /other/i });
fireEvent.click(textButton);
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
await waitFor(() => expect(axiosMock.history.patch.length).toEqual(0));
});
Expand All @@ -87,13 +99,14 @@ describe('<AddContentContainer />', () => {
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);

render(collectionId);

const textButton = screen.getByRole('button', { name: /text/i });
// Select a block that is not supported by the editor should create the component
const textButton = screen.getByRole('button', { name: /other/i });
fireEvent.click(textButton);

await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
Expand All @@ -105,6 +118,8 @@ describe('<AddContentContainer />', () => {
mockClipboardEmpty.applyMock();
const collectionId = 'some-collection-id';
const url = getCreateLibraryBlockUrl(libraryId);
const usageKey = mockXBlockFields.usageKeyNewHtml;
const updateBlockUrl = getXBlockFieldsApiUrl(usageKey);
const collectionComponentUrl = getLibraryCollectionComponentApiUrl(
libraryId,
collectionId,
Expand All @@ -113,39 +128,26 @@ describe('<AddContentContainer />', () => {
jest.spyOn(editorCmsApi, 'fetchCourseImages').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,
}],
},
}));

Object.defineProperty(window, 'location', {
value: { pathname: `/library/${libraryId}/collection/${collectionId}` },
writable: true,
});
axiosMock.onPost(url).reply(200, {
id: 'lb:OpenedX:CSPROB2:html:1a5efd56-4ee5-4df0-b466-44f08fbbf567',
id: usageKey,
});
const fieldsHtml = {
displayName: 'Introduction to Testing',
data: '<p>This is a text component which uses <strong>HTML</strong>.</p>',
metadata: { displayName: 'Introduction to Testing' },
};
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => (
{ status: 200, data: snakeCaseObject(fieldsHtml) }
));

axiosMock.onPatch(collectionComponentUrl).reply(200);

axiosMock.onPost(updateBlockUrl).reply(200, mockXBlockFields.dataHtml);
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);
// Component should be linked to Collection on saving the changes in the editor.
const saveButton = screen.getByLabelText('Save changes and return to learning context');
fireEvent.click(saveButton);
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));
Expand Down Expand Up @@ -259,20 +261,6 @@ describe('<AddContentContainer />', () => {
expectedError: 'There was an error pasting the content: library cannot have more than 100000 components',
buttonName: /paste from clipboard/i,
},
{
label: 'should handle failure to create content',
mockUrl: getCreateLibraryBlockUrl(libraryId),
mockResponse: undefined,
expectedError: 'There was an error creating the content.',
buttonName: /text/i,
},
{
label: 'should show detailed error in toast on create failure',
mockUrl: getCreateLibraryBlockUrl(libraryId),
mockResponse: 'library cannot have more than 100000 components',
expectedError: 'There was an error creating the content: library cannot have more than 100000 components',
buttonName: /text/i,
},
])('$label', async ({
mockUrl, mockResponse, buttonName, expectedError,
}) => {
Expand Down
26 changes: 20 additions & 6 deletions src/library-authoring/add-content/AddContentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import { v4 as uuid4 } from 'uuid';
import { ToastContext } from '../../generic/toast-context';
import { useCopyToClipboard } from '../../generic/clipboard';
import { getCanEdit } from '../../course-unit/data/selectors';
import { useLibraryPasteClipboard, useAddComponentsToCollection } from '../data/apiHooks';
import { useCreateLibraryBlock, useLibraryPasteClipboard, useAddComponentsToCollection } from '../data/apiHooks';
import { useLibraryContext } from '../common/context/LibraryContext';
import { PickLibraryContentModal } from './PickLibraryContentModal';
import { blockTypes } from '../../editors/data/constants/app';

import messages from './messages';

Expand Down Expand Up @@ -72,6 +73,7 @@ const AddContentContainer = () => {
componentPicker,
} = useLibraryContext();
const updateComponentsMutation = useAddComponentsToCollection(libraryId, collectionId);
const createBlockMutation = useCreateLibraryBlock();
const pasteClipboardMutation = useLibraryPasteClipboard();
const { showToast } = useContext(ToastContext);
const canEdit = useSelector(getCanEdit);
Expand Down Expand Up @@ -190,14 +192,26 @@ const AddContentContainer = () => {
};

const onCreateBlock = (blockType: string) => {
const mfeEditorTypes = ['html', 'problem', 'video'];
if (mfeEditorTypes.includes(blockType)) {
const suportedEditorTypes = Object.values(blockTypes);
if (suportedEditorTypes.includes(blockType)) {
// linkComponent on editor close.
openComponentEditor('', () => {}, blockType);
} else {
// We can't start editing this right away so just show a toast message:
showToast(intl.formatMessage(messages.successCreateMessage));
// linkComponent(data.id);
createBlockMutation.mutateAsync({
libraryId,
blockType,
definitionId: `${uuid4()}`,
}).then((data) => {
// 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(
error,
messages.errorCreateMessageWithDetail,
messages.errorCreateMessage,
));
});
}
};

Expand Down
37 changes: 19 additions & 18 deletions src/library-authoring/add-content/AddContentWorkflow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,17 @@ describe('AddContentWorkflow test', () => {
const newComponentButton = await screen.findByRole('button', { name: /New/ });
fireEvent.click(newComponentButton);

// Click "Text" to create a text component
// Click "Text" to open the editor in creation mode
fireEvent.click(await screen.findByRole('button', { name: /Text/ }));

// Then the editor should open
expect(await screen.findByRole('heading', { name: /New Text Component/ })).toBeInTheDocument();
expect(await screen.findByRole('heading', { name: /Text/ })).toBeInTheDocument();

// Edit the title
fireEvent.click(screen.getByRole('button', { name: /Edit Title/ }));
const titleInput = screen.getByPlaceholderText('Title');
fireEvent.change(titleInput, { target: { value: 'A customized title' } });
fireEvent.blur(titleInput);
await waitFor(() => expect(screen.queryByRole('heading', { name: /New Text Component/ })).not.toBeInTheDocument());
expect(screen.getByRole('heading', { name: /A customized title/ }));

await waitFor(() => expect(screen.queryByRole('heading', { name: /Text/ })).not.toBeInTheDocument());
expect(screen.getByRole('heading', { name: /A customized title/ })).toBeInTheDocument();
// Note that TinyMCE doesn't really load properly in our test environment
// so we can't really edit the text, but we have getContent() mocked to simulate
// using TinyMCE to enter some new HTML.
Expand All @@ -83,10 +80,12 @@ describe('AddContentWorkflow test', () => {
status: 200, data: { id: mockXBlockFields.usageKeyNewHtml },
}));

// Click Save
// Click Save should create the component and then save the content
const saveButton = screen.getByLabelText('Save changes and return to learning context');
fireEvent.click(saveButton);
expect(saveSpy).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(saveSpy).toHaveBeenCalledTimes(1);
});
});

it('can create a Problem component', async () => {
Expand All @@ -96,10 +95,8 @@ describe('AddContentWorkflow test', () => {
const newComponentButton = await screen.findByRole('button', { name: /New/ });
fireEvent.click(newComponentButton);

// Click "Problem" to create a capa problem component
// Click "Problem" to create a capa problem component in the editor
fireEvent.click(await screen.findByRole('button', { name: /Problem/ }));

// Then the editor should open
expect(await screen.findByRole('heading', { name: /Select problem type/ })).toBeInTheDocument();

// Select the type: Numerical Input
Expand All @@ -117,10 +114,12 @@ describe('AddContentWorkflow test', () => {
status: 200, data: { id: mockXBlockFields.usageKeyNewProblem },
}));

// Click Save
// Click Save should create the component and then save the content
const saveButton = screen.getByLabelText('Save changes and return to learning context');
fireEvent.click(saveButton);
expect(saveSpy).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(saveSpy).toHaveBeenCalledTimes(1);
});
});

it('can create a Video component', async () => {
Expand All @@ -133,8 +132,8 @@ describe('AddContentWorkflow test', () => {
// Click "Video" to create a video component
fireEvent.click(await screen.findByRole('button', { name: /Video/ }));

// Then the editor should open - this is the default title of a blank video in our mock
expect(await screen.findByRole('heading', { name: /New Video/ })).toBeInTheDocument();
// Then the editor should open
expect(await screen.findByRole('heading', { name: /Video/ })).toBeInTheDocument();

// Enter the video URL
const urlInput = await screen.findByRole('textbox', { name: 'Video URL' });
Expand All @@ -146,9 +145,11 @@ describe('AddContentWorkflow test', () => {
status: 200, data: { id: mockXBlockFields.usageKeyNewVideo },
}));

// Click Save
// Click Save should create the component and then save the content
const saveButton = screen.getByLabelText('Save changes and return to learning context');
fireEvent.click(saveButton);
expect(saveSpy).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(saveSpy).toHaveBeenCalledTimes(1);
});
});
});

0 comments on commit 3dd53d4

Please sign in to comment.