Files
frontend-app-authoring/src/library-authoring/LibraryAuthoringPage.test.tsx
Navin Karkera e2f1aedf9a feat: import analysis step (#2657)
Shows course analysis information in review import details step in course import stepper page. Also handles alerts based on the import status, like, reimport or unsupported number of blocks.
2025-11-18 11:41:27 -05:00

1121 lines
45 KiB
TypeScript

import { getConfig } from '@edx/frontend-platform';
import fetchMock from 'fetch-mock-jest';
import { Helmet } from 'react-helmet';
import {
fireEvent,
initializeMocks,
render,
screen,
waitFor,
within,
} from '@src/testUtils';
import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock';
import { mockGetMigrationStatus } from '@src/data/api.mocks';
import mockEmptyResult from '@src/search-modal/__mocks__/empty-search-result.json';
import { mockContentSearchConfig } from '@src/search-manager/data/api.mock';
import { getStudioHomeApiUrl } from '@src/studio-home/data/api';
import mockResult from './__mocks__/library-search.json';
import {
mockContentLibrary,
mockGetCollectionMetadata,
mockGetContainerMetadata,
mockGetLibraryTeam,
mockXBlockFields,
} from './data/api.mocks';
import { LibraryLayout } from '.';
import { getLibraryCollectionsApiUrl, getLibraryContainersApiUrl } from './data/api';
let axiosMock;
let mockShowToast;
mockGetCollectionMetadata.applyMock();
mockGetContainerMetadata.applyMock();
mockContentSearchConfig.applyMock();
mockContentLibrary.applyMock();
mockGetLibraryTeam.applyMock();
mockXBlockFields.applyMock();
mockGetMigrationStatus.applyMock();
const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
/**
* Returns 0 components from the search query.
*/
const returnEmptyResult = (_url, req) => {
const requestData = JSON.parse(req.body?.toString() ?? '');
const query = requestData?.queries[0]?.q ?? '';
// We have to replace the query (search keywords) in the mock results with the actual query,
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
mockEmptyResult.results[0].query = query;
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
mockEmptyResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
return mockEmptyResult;
};
const path = '/library/:libraryId/*';
const libraryTitle = mockContentLibrary.libraryData.title;
describe('<LibraryAuthoringPage />', () => {
beforeAll(() => {
jest.useFakeTimers();
});
beforeEach(async () => {
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
mockShowToast = mocks.mockShowToast;
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
// The Meilisearch client-side API uses fetch, not Axios.
fetchMock.mockReset();
fetchMock.post(searchEndpoint, (_url, req) => {
const requestData = JSON.parse((req.body ?? '') as string);
const query = requestData?.queries[0]?.q ?? '';
// We have to replace the query (search keywords) in the mock results with the actual query,
// because otherwise Instantsearch will update the UI and change the query,
// leading to unexpected results in the test cases.
const newMockResult = { ...mockResult };
newMockResult.results[0].query = query;
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
newMockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
return newMockResult;
});
});
afterAll(() => {
jest.useRealTimers();
});
const renderLibraryPage = async () => {
render(<LibraryLayout />, { path, params: { libraryId: mockContentLibrary.libraryId } });
// Ensure the search endpoint is called:
// Call 1: To fetch searchable/filterable/sortable library data
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
};
it('shows the spinner before the query is complete', () => {
// This mock will never return data about the library (it loads forever):
const libraryId = mockContentLibrary.libraryIdThatNeverLoads;
render(<LibraryLayout />, { path, params: { libraryId } });
const spinner = screen.getByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});
it('shows an error component if no library returned', async () => {
// This mock will simulate a 404 error:
const libraryId = mockContentLibrary.library404;
render(<LibraryLayout />, { path, params: { libraryId } });
expect(await screen.findByTestId('notFoundAlert')).toBeInTheDocument();
});
it('shows library data', async () => {
await renderLibraryPage();
expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
const browserTabTitle = Helmet.peek().title.join('');
const siteName = getConfig().SITE_NAME;
expect(browserTabTitle).toEqual(`${libraryTitle} | ${siteName}`);
expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect((await screen.findAllByText('Introduction to Testing'))[0]).toBeInTheDocument();
// Search box should not have focus on page load
const searchBox = screen.getByRole('searchbox');
expect(searchBox).not.toHaveFocus();
// Navigate to the components tab
fireEvent.click(screen.getByRole('tab', { name: 'Components' }));
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
// Navigate to the collections tab
fireEvent.click(screen.getByRole('tab', { name: 'Collections' }));
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('There are 10 components in this library')).not.toBeInTheDocument();
expect((await screen.findAllByText('Collection 1'))[0]).toBeInTheDocument();
// Go back to Home tab
// This step is necessary to avoid the url change leak to other tests
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
});
it('shows a library without components and collections', async () => {
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
await renderLibraryPage();
expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: 'Collections' }));
expect(await screen.findByText('You have not added any collections to this library yet.')).toBeInTheDocument();
// Open Create collection modal
const addCollectionButton = screen.getByRole('button', { name: /add collection/i });
fireEvent.click(addCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();
// Click on Cancel button
const cancelButton = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancelButton);
expect(collectionModalHeading).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
expect(await screen.findByText('You have not added any content to this library yet.')).toBeInTheDocument();
const addComponentButton = screen.getByRole('button', { name: /add component/i });
fireEvent.click(addComponentButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();
});
it('shows the new content button', async () => {
await renderLibraryPage();
expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument();
expect(screen.queryByText('Read Only')).not.toBeInTheDocument();
});
it('shows an empty read-only library, without a "create component" button', async () => {
// Use a library mock that is read-only:
const libraryId = mockContentLibrary.libraryIdReadOnly;
// Update search mock so it returns no results:
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
render(<LibraryLayout />, { path, params: { libraryId } });
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
expect(await screen.findByText('Content library')).toBeInTheDocument();
expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /add component/i })).not.toBeInTheDocument();
expect(screen.getByText('Read Only')).toBeInTheDocument();
});
it('show a library without search results', async () => {
// Update search mock so it returns no results:
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
await renderLibraryPage();
expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'noresults' } });
// Ensure the search endpoint is called again, only once more since the recently modified call
// should not be impacted by the search
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
expect(await screen.findByText('No matching components found in this library.')).toBeInTheDocument();
// Navigate to the components tab
const componentsTab = screen.getByRole('tab', { name: 'Components' });
fireEvent.click(componentsTab);
expect(componentsTab).toHaveAttribute('aria-selected', 'true');
expect(await screen.findByText('No matching components found in this library.')).toBeInTheDocument();
// Navigate to the collections tab
const collectionsTab = screen.getByRole('tab', { name: 'Collections' });
fireEvent.click(collectionsTab);
expect(collectionsTab).toHaveAttribute('aria-selected', 'true');
expect(await screen.findByText('No matching collections found in this library.')).toBeInTheDocument();
// Navigate to the units tab
const unitsTab = screen.getByRole('tab', { name: 'Units' });
fireEvent.click(unitsTab);
expect(unitsTab).toHaveAttribute('aria-selected', 'true');
expect(await screen.findByText('No matching components found in this library.')).toBeInTheDocument();
// Navigate to the subsections tab
const subsectionsTab = screen.getByRole('tab', { name: 'Subsections' });
fireEvent.click(subsectionsTab);
expect(subsectionsTab).toHaveAttribute('aria-selected', 'true');
expect(await screen.findByText('No matching components found in this library.')).toBeInTheDocument();
// Navigate to the subsections tab
const sectionsTab = screen.getByRole('tab', { name: 'Sections' });
fireEvent.click(sectionsTab);
expect(sectionsTab).toHaveAttribute('aria-selected', 'true');
expect(await screen.findByText('No matching components found in this library.')).toBeInTheDocument();
// Go back to Home tab
// This step is necessary to avoid the url change leak to other tests
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
});
it('should open and close new content sidebar', async () => {
await renderLibraryPage();
expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();
const closeButton = screen.getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
});
it('should open Library Info by default', async () => {
await renderLibraryPage();
expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[1]).toBeInTheDocument();
expect(screen.getByText('Draft')).toBeInTheDocument();
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
// Draft saved on date:
expect(screen.getByText('July 22, 2024')).toBeInTheDocument();
expect(screen.getByText(mockContentLibrary.libraryData.org)).toBeInTheDocument();
// Updated:
expect(screen.getByText('July 20, 2024')).toBeInTheDocument();
// Created:
expect(screen.getByText('June 26, 2024')).toBeInTheDocument();
});
it('should close and open Library Info', async () => {
await renderLibraryPage();
expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[1]).toBeInTheDocument();
// Open by default; close the library info sidebar
const closeButton = screen.getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
expect(screen.queryByText('Draft')).not.toBeInTheDocument();
expect(screen.queryByText('(Never Published)')).not.toBeInTheDocument();
// Open library info sidebar with 'Library info' button
const libraryInfoButton = screen.getByRole('button', { name: /library info/i });
fireEvent.click(libraryInfoButton);
expect(screen.getByText('Draft')).toBeInTheDocument();
expect(screen.getByText('(Never Published)')).toBeInTheDocument();
// Close library info sidebar with 'Library info' button
fireEvent.click(libraryInfoButton);
expect(screen.queryByText('Draft')).not.toBeInTheDocument();
expect(screen.queryByText('(Never Published)')).not.toBeInTheDocument();
});
it('should show Library Team button in Library Info that opens the Library Team modal', async () => {
await renderLibraryPage();
const manageAccess = await screen.findByRole('button', { name: /Library Team/i });
expect(manageAccess).not.toBeDisabled();
fireEvent.click(manageAccess);
expect(await screen.findByRole('heading', { name: 'Library Team' })).toBeInTheDocument();
});
it('should not show "Library Team" button in Library Info to users who cannot edit the library', async () => {
const libraryId = mockContentLibrary.libraryIdReadOnly;
render(<LibraryLayout />, { path, params: { libraryId } });
const manageAccess = screen.queryByRole('button', { name: /Library Team/i });
expect(manageAccess).not.toBeInTheDocument();
});
it('sorts library components', async () => {
await renderLibraryPage();
expect(await screen.findByTitle('Sort search results')).toBeInTheDocument();
const testSortOption = (async (optionText, sortBy, isDefault) => {
// Open the drop-down menu
fireEvent.click(screen.getByTitle('Sort search results'));
// Click the option with the given text
// Since the sort drop-down also shows the selected sort
// option in its toggle button, we need to make sure we're
// clicking on the last one found.
const options = screen.getAllByText(optionText);
expect(options.length).toBeGreaterThan(0);
fireEvent.click(options[options.length - 1]);
let bodyText;
// Did the search happen with the expected sort option?
if (Array.isArray(sortBy)) {
bodyText = `"sort":[${sortBy.map(item => `"${item}"`).join(',')}]`;
} else {
bodyText = sortBy ? `"sort":["${sortBy}"]` : '"sort":[]';
}
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining(bodyText),
method: 'POST',
headers: expect.anything(),
});
});
// Is the sort option stored in the query string?
// Note: we can't easily check this at the moment with <MemoryRouter>
// const searchText = isDefault ? '' : `?sort=${encodeURIComponent(sortBy)}`;
// expect(window.location.href).toEqual(searchText);
// Is the selected sort option shown in the toggle button (if not default)
// as well as in the drop-down menu?
expect(screen.getAllByText(optionText).length).toEqual(isDefault ? 1 : 2);
});
await testSortOption('Title, A-Z', 'display_name:asc', false);
await testSortOption('Title, Z-A', 'display_name:desc', false);
await testSortOption('Newest', 'created:desc', false);
await testSortOption('Oldest', 'created:asc', false);
// Sorting by Recently Published also sorts unpublished components by recently modified
await testSortOption('Recently Published', ['last_published:desc', 'modified:desc'], false);
// Re-selecting the previous sort option resets sort to default "Recently Modified"
await testSortOption('Recently Published', 'modified:desc', true);
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
// Enter a keyword into the search box
const searchBox = screen.getByRole('searchbox');
fireEvent.change(searchBox, { target: { value: 'words to find' } });
// Default sort option changes to "Most Relevant"
expect((await screen.findAllByText('Most Relevant')).length).toEqual(2);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining('"sort":[]'),
method: 'POST',
headers: expect.anything(),
});
});
}, 10000);
it('should open, close and re-open the component sidebar', async () => {
const mockResult0 = { ...mockResult }.results[0].hits[0];
const displayName = 'Introduction to Testing';
expect(mockResult0.display_name).toStrictEqual(displayName);
await renderLibraryPage();
fireEvent.click((await screen.findAllByText(displayName))[0]);
const sidebar = screen.getByTestId('library-sidebar');
const { getByRole, getByText } = within(sidebar);
await waitFor(() => expect(getByText(displayName)).toBeInTheDocument());
const closeButton = getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
fireEvent.click((await screen.findAllByText(displayName))[0]);
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).toBeInTheDocument());
});
it('should open component sidebar, showing manage tab on clicking add to collection menu item - component', async () => {
const mockResult0 = { ...mockResult }.results[0].hits[0];
const displayName = 'Introduction to Testing';
expect(mockResult0.display_name).toStrictEqual(displayName);
await renderLibraryPage();
waitFor(() => expect(screen.getAllByTestId('component-card-menu-toggle').length).toBeGreaterThan(0));
// Open menu
fireEvent.click((await screen.findAllByTestId('component-card-menu-toggle'))[0]);
// Click add to collection
fireEvent.click(screen.getByRole('button', { name: 'Add to collection' }));
const sidebar = screen.getByTestId('library-sidebar');
const { getByRole, findByText } = within(sidebar);
expect(await findByText(displayName)).toBeInTheDocument();
jest.advanceTimersByTime(300);
expect(getByRole('tab', { selected: true })).toHaveTextContent('Manage');
const closeButton = getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});
it('should open component sidebar, showing manage tab on clicking add to collection menu item - unit', async () => {
const displayName = 'Test Unit';
await renderLibraryPage();
waitFor(() => expect(screen.getAllByTestId('container-card-menu-toggle').length).toBeGreaterThan(0));
// Open menu
fireEvent.click((await screen.findAllByTestId('container-card-menu-toggle'))[0]);
// Click add to collection
fireEvent.click(screen.getByRole('button', { name: 'Add to collection' }));
const sidebar = screen.getByTestId('library-sidebar');
const { getByRole, findByText } = within(sidebar);
expect(await findByText(displayName)).toBeInTheDocument();
jest.advanceTimersByTime(300);
expect(getByRole('tab', { selected: true })).toHaveTextContent('Manage');
const closeButton = getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});
it('should open and close the collection sidebar', async () => {
await renderLibraryPage();
// Click on the first collection
fireEvent.click((await screen.findByText('Collection 1')));
const sidebar = screen.getByTestId('library-sidebar');
const { getByRole, getByText } = within(sidebar);
// The mock data for the sidebar has a title of "Test Collection"
await waitFor(() => expect(getByText('Test Collection')).toBeInTheDocument());
const closeButton = getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});
it('should open and close the unit sidebar', async () => {
await renderLibraryPage();
// Click on the first unit
fireEvent.click((await screen.findByText('Test Unit')));
const sidebar = screen.getByTestId('library-sidebar');
const { getByRole, getByText } = within(sidebar);
// The mock data for the sidebar has a title of "Test Unit"
await waitFor(() => expect(getByText('Test Unit')).toBeInTheDocument());
const closeButton = getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});
it('should preserve the tab while switching from a component to a collection', async () => {
await renderLibraryPage();
// Click on the first collection
fireEvent.click((await screen.findByText('Collection 1')));
// Click on the Details tab
fireEvent.click(screen.getByRole('tab', { name: 'Details' }));
// Change to a component
fireEvent.click((await screen.findAllByText('Introduction to Testing'))[0]);
// Check that the Details tab is still selected
expect(screen.getByRole('tab', { name: 'Details' })).toHaveAttribute('aria-selected', 'true');
// Click on the Previews tab
fireEvent.click(screen.getByRole('tab', { name: 'Preview' }));
// Switch back to the collection
fireEvent.click((await screen.findByText('Collection 1')));
// The Details (default) tab should be selected because the collection does not have a Preview tab
expect(screen.getByRole('tab', { name: 'Details' })).toHaveAttribute('aria-selected', 'true');
});
const problemTypes = {
'Multiple Choice': 'choiceresponse',
Checkboxes: 'multiplechoiceresponse',
'Numerical Input': 'numericalresponse',
Dropdown: 'optionresponse',
'Text Input': 'stringresponse',
};
it.each(Object.keys(problemTypes))('can filter by capa problem type (%s)', async (submenuText) => {
await renderLibraryPage();
// Ensure the search endpoint is called
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
const filterButton = screen.getByRole('button', { name: /type/i });
fireEvent.click(filterButton);
const problemFilterCheckbox = screen.getByRole('checkbox', { name: /problem/i });
const problemFilterMenuItem = problemFilterCheckbox.parentElement; // div.pgn__menu-item
const showProbTypesSubmenuBtn = problemFilterMenuItem!.querySelector('button[aria-label="Open problem types filters"]');
expect(showProbTypesSubmenuBtn).not.toBeNull();
fireEvent.click(showProbTypesSubmenuBtn!);
const submenu = screen.getByText(submenuText);
expect(submenu).toBeInTheDocument();
fireEvent.click(submenu);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining(`content.problem_types = ${problemTypes[submenuText]}`),
method: 'POST',
headers: expect.anything(),
});
});
fireEvent.click(submenu);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.not.stringContaining(`content.problem_types = ${problemTypes[submenuText]}`),
method: 'POST',
headers: expect.anything(),
});
});
});
it('can filter by block type', async () => {
await renderLibraryPage();
// Ensure the search endpoint is called
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
const filterButton = screen.getByRole('button', { name: /type/i });
fireEvent.click(filterButton);
// Validate click on Problem type
const problemMenu = screen.getAllByText('Problem')[0];
expect(problemMenu).toBeInTheDocument();
fireEvent.click(problemMenu);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining('block_type = problem'),
method: 'POST',
headers: expect.anything(),
});
});
fireEvent.click(problemMenu);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.not.stringContaining('block_type = problem'),
method: 'POST',
headers: expect.anything(),
});
});
// Validate clear filters
fireEvent.click(problemMenu);
const clearFitlersButton = screen.getByText('Clear Filters');
fireEvent.click(clearFitlersButton);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.not.stringContaining('block_type = problem'),
method: 'POST',
headers: expect.anything(),
});
});
});
it('has an empty type filter when there are no results', async () => {
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
await renderLibraryPage();
const filterButton = screen.getByRole('button', { name: /type/i });
fireEvent.click(filterButton);
expect(screen.getByText(/no matching components/i)).toBeInTheDocument();
});
it('should create a collection', async () => {
await renderLibraryPage();
const title = 'This is a Test';
const description = 'This is the description of the Test';
const url = getLibraryCollectionsApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(200, {
id: '1',
slug: 'this-is-a-test',
title,
description,
});
expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();
// Open New collection Modal
const sidebar = screen.getByTestId('library-sidebar');
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();
// Click on Cancel button
const cancelButton = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancelButton);
expect(collectionModalHeading).not.toBeInTheDocument();
// Open new collection modal again and create a collection
fireEvent.click(newCollectionButton);
const createButton = screen.getByRole('button', { name: /create/i });
const nameField = screen.getByRole('textbox', { name: /name your collection/i });
const descriptionField = screen.getByRole('textbox', { name: /add a description \(optional\)/i });
fireEvent.change(nameField, { target: { value: title } });
fireEvent.change(descriptionField, { target: { value: description } });
fireEvent.click(createButton);
// Check success toast
await waitFor(() => expect(axiosMock.history.post.length).toBe(1));
expect(axiosMock.history.post[0].url).toBe(url);
expect(axiosMock.history.post[0].data).toContain(`"title":"${title}"`);
expect(axiosMock.history.post[0].data).toContain(`"description":"${description}"`);
expect(mockShowToast).toHaveBeenCalledWith('Collection created successfully');
});
it('should show validations in create collection', async () => {
await renderLibraryPage();
const title = 'This is a Test';
const description = 'This is the description of the Test';
const url = getLibraryCollectionsApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(200, {
id: '1',
slug: 'this-is-a-test',
title,
description,
});
expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();
// Open New collection Modal
const sidebar = screen.getByTestId('library-sidebar');
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();
const nameField = screen.getByRole('textbox', { name: /name your collection/i });
fireEvent.focus(nameField);
fireEvent.blur(nameField);
// Click on create with an empty name
const createButton = screen.getByRole('button', { name: /create/i });
fireEvent.click(createButton);
expect(await screen.findByText(/collection name is required/i)).toBeInTheDocument();
});
it('should show error on create collection', async () => {
await renderLibraryPage();
const title = 'This is a Test';
const description = 'This is the description of the Test';
const url = getLibraryCollectionsApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(500);
expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();
// Open New collection Modal
const sidebar = screen.getByTestId('library-sidebar');
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();
// Create a normal collection
const createButton = screen.getByRole('button', { name: /create/i });
const nameField = screen.getByRole('textbox', { name: /name your collection/i });
const descriptionField = screen.getByRole('textbox', { name: /add a description \(optional\)/i });
fireEvent.change(nameField, { target: { value: title } });
fireEvent.change(descriptionField, { target: { value: description } });
fireEvent.click(createButton);
// Check error toast
await waitFor(() => expect(axiosMock.history.post.length).toBe(1));
expect(mockShowToast).toHaveBeenCalledWith('There is an error when creating the library collection');
});
test.each([
{
label: 'should create a unit',
containerType: 'unit',
},
{
label: 'should create a section',
containerType: 'section',
},
{
label: 'should create a subsection',
containerType: 'subsection',
},
])('$label', async ({ containerType }) => {
await renderLibraryPage();
const title = `This is a Test ${containerType}`;
const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(200, {
id: `lct:org:libId:${containerType}:1`,
slug: 'this-is-a-test',
title,
});
expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();
// Open New container Modal
const sidebar = screen.getByTestId('library-sidebar');
const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0];
fireEvent.click(newContainerButton);
const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') });
expect(containerModalHeading).toBeInTheDocument();
// Click on Cancel button
const cancelButton = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancelButton);
expect(containerModalHeading).not.toBeInTheDocument();
// Open new container modal again and create a container
fireEvent.click(newContainerButton);
const createButton = screen.getByRole('button', { name: /create/i });
const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') });
fireEvent.change(nameField, { target: { value: title } });
fireEvent.click(createButton);
// Check success
await waitFor(() => expect(axiosMock.history.post.length).toBe(1));
expect(axiosMock.history.post[0].url).toBe(url);
expect(axiosMock.history.post[0].data).toContain(`"display_name":"${title}"`);
expect(axiosMock.history.post[0].data).toContain(`"container_type":"${containerType}"`);
expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${containerType} created successfully`, 'i')));
});
test.each([
{
label: 'should show validations in create unit',
containerType: 'unit',
},
{
label: 'should show validations in create section',
containerType: 'section',
},
{
label: 'should show validations in create subsection',
containerType: 'subsection',
},
])('$label', async ({ containerType }) => {
await renderLibraryPage();
const title = `This is a Test ${containerType}`;
const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(200, {
id: '1',
slug: 'this-is-a-test',
title,
});
expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();
// Open New container Modal
const sidebar = screen.getByTestId('library-sidebar');
const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0];
fireEvent.click(newContainerButton);
const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') });
expect(containerModalHeading).toBeInTheDocument();
const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') });
fireEvent.focus(nameField);
fireEvent.blur(nameField);
// Click on create with an empty name
const createButton = screen.getByRole('button', { name: /create/i });
fireEvent.click(createButton);
expect(await screen.findByText(new RegExp(`${containerType} name is required`, 'i'))).toBeInTheDocument();
});
test.each([
{
label: 'should show error on create unit',
containerType: 'unit',
},
{
label: 'should show error on create section',
containerType: 'section',
},
{
label: 'should show error on create subsection',
containerType: 'subsection',
},
])('$label', async ({ containerType }) => {
await renderLibraryPage();
const displayName = `This is a Test ${containerType}`;
const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(500);
expect(await screen.findByRole('heading')).toBeInTheDocument();
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
// Open Add content sidebar
const newButton = screen.getByRole('button', { name: /new/i });
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();
// Open New container Modal
const sidebar = screen.getByTestId('library-sidebar');
const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0];
fireEvent.click(newContainerButton);
const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') });
expect(containerModalHeading).toBeInTheDocument();
// Create a container
const createButton = screen.getByRole('button', { name: /create/i });
const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') });
fireEvent.change(nameField, { target: { value: displayName } });
fireEvent.click(createButton);
// Check error toast
await waitFor(() => expect(axiosMock.history.post.length).toBe(1));
expect(mockShowToast).toHaveBeenCalledWith(
expect.stringMatching(new RegExp(`There is an error when creating the library ${containerType}`, 'i')),
);
});
it('shows a single block when usageKey query param is set', async () => {
render(<LibraryLayout />, {
path,
routerProps: {
initialEntries: [
`/library/${mockContentLibrary.libraryId}/components?usageKey=${mockXBlockFields.usageKeyHtml}`,
],
},
});
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining(mockXBlockFields.usageKeyHtml),
headers: expect.anything(),
method: 'POST',
});
});
expect(screen.queryByPlaceholderText('Displaying single block, clear filters to search')).toBeInTheDocument();
const { displayName } = mockXBlockFields.dataHtml;
const sidebar = screen.getByTestId('library-sidebar');
const { getByText } = within(sidebar);
// should display the component with passed param: usageKey in the sidebar
expect(getByText(displayName)).toBeInTheDocument();
// clear usageKey filter
const clearFitlersButton = screen.getByRole('button', { name: /clear filters/i });
fireEvent.click(clearFitlersButton);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.not.stringContaining(mockXBlockFields.usageKeyHtml),
method: 'POST',
headers: expect.anything(),
});
});
});
it('filters by publish status', async () => {
await renderLibraryPage();
// Open the publish status filter dropdown
const filterButton = screen.getByRole('button', { name: /publish status/i });
fireEvent.click(filterButton);
// Test each publish status filter option
const publishedCheckbox = screen.getByRole('checkbox', { name: /^published \d+$/i });
const modifiedCheckbox = screen.getByRole('checkbox', { name: /^modified since publish \d+$/i });
const neverPublishedCheckbox = screen.getByRole('checkbox', { name: /^never published \d+$/i });
// Verify initial state - no clear filters button
expect(screen.queryByRole('button', { name: /clear filters/i })).not.toBeInTheDocument();
// Test Published filter
fireEvent.click(publishedCheckbox);
// Wait for both the API call and the UI update
await waitFor(() => {
// Check that the API was called with the correct filter
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining('"publish_status = published"'),
method: 'POST',
headers: expect.anything(),
});
});
// Wait for the clear filters button to appear
await waitFor(() => {
const clearFiltersButton = screen.getByText('Clear Filters');
expect(clearFiltersButton).toBeInTheDocument();
});
// Test Modified filter
fireEvent.click(modifiedCheckbox);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining('"publish_status = modified"'),
method: 'POST',
headers: expect.anything(),
});
});
// Test Never Published filter
fireEvent.click(neverPublishedCheckbox);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining('"publish_status = never"'),
method: 'POST',
headers: expect.anything(),
});
});
// Test clearing filters
const clearFiltersButton = screen.getByText('Clear Filters');
fireEvent.click(clearFiltersButton);
await waitFor(() => {
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
body: expect.stringContaining('"filter":[[],'), // Empty filter array
method: 'POST',
headers: expect.anything(),
});
});
});
it('Disables Type filter on Collections and Units tab', async () => {
await renderLibraryPage();
expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
expect((await screen.findAllByText('Introduction to Testing'))[0]).toBeInTheDocument();
expect((await screen.findAllByText('Collection 1'))[0]).toBeInTheDocument();
// Filter by Text block type
fireEvent.click(screen.getByRole('button', { name: /type/i }));
fireEvent.click(screen.getByRole('checkbox', { name: /text/i }));
// Escape to close the Types filter drop-down and re-enable the tabs
fireEvent.keyDown(screen.getByRole('button', { name: /type/i }), { key: 'Escape' });
// Navigate to the collections tab
fireEvent.click(await screen.findByRole('tab', { name: 'Collections' }));
expect((await screen.findAllByText('Collection 1'))[0]).toBeInTheDocument();
// No Types filter shown
expect(screen.queryByRole('button', { name: /type/i })).not.toBeInTheDocument();
// Navigate to the units tab
fireEvent.click(await screen.findByRole('tab', { name: 'Units' }));
expect((await screen.findAllByText('Test Unit'))[0]).toBeInTheDocument();
// No Types filter shown
expect(screen.queryByRole('button', { name: /type/i })).not.toBeInTheDocument();
// Navigate to the components tab
fireEvent.click(screen.getByRole('tab', { name: 'Components' }));
// Text components should be shown
expect((await screen.findAllByText('Introduction to Testing'))[0]).toBeInTheDocument();
// Types filter is shown
expect(screen.getByRole('button', { name: /type/i })).toBeInTheDocument();
});
it('Shows an error if libraries V2 is disabled', async () => {
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, {
...studioHomeMock,
libraries_v2_enabled: false,
});
render(<LibraryLayout />, { path, params: { libraryId: mockContentLibrary.libraryId } });
expect(await screen.findByRole('alert')).toHaveTextContent(
'This page cannot be shown: Libraries v2 are disabled.',
);
});
it('Should show success in migration legacy libraries', async () => {
render(<LibraryLayout />, {
path,
routerProps: {
initialEntries: [
`/library/${mockContentLibrary.libraryId}?migration_task=${mockGetMigrationStatus.migrationId}`,
],
},
});
await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('The migration of legacy libraries has been completed successfully.'));
});
it('Should show fail in migration legacy libraries', async () => {
render(<LibraryLayout />, {
path,
routerProps: {
initialEntries: [
`/library/${mockContentLibrary.libraryId}?migration_task=${mockGetMigrationStatus.migrationIdFailed}`,
],
},
});
await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('Legacy libraries migration have failed'));
});
it('Should show fail multiple legacy libraries in a migration', async () => {
render(<LibraryLayout />, {
path,
routerProps: {
initialEntries: [
`/library/${mockContentLibrary.libraryId}?migration_task=${mockGetMigrationStatus.migrationIdMultiple}`,
],
},
});
await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('Multiple legacy libraries have failed'));
});
it('Should show fail one legacy library in a migration', async () => {
render(<LibraryLayout />, {
path,
routerProps: {
initialEntries: [
`/library/${mockContentLibrary.libraryId}?migration_task=${mockGetMigrationStatus.migrationIdOneLibrary}`,
],
},
});
await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('The legacy library with this key has failed: legacy-lib-1'));
});
});