Skip to content

Commit

Permalink
feat: course libraries page [FC-0076] (#1641)
Browse files Browse the repository at this point in the history
Adds Libraries page that lists all library components being used in the current course to Content > Libraries
  • Loading branch information
navinkarkera authored Feb 18, 2025
1 parent 59243b0 commit 8275bbe
Show file tree
Hide file tree
Showing 17 changed files with 1,283 additions and 1 deletion.
5 changes: 5 additions & 0 deletions src/CourseAuthoringRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import CourseImportPage from './import-page/CourseImportPage';
import { DECODED_ROUTES } from './constants';
import CourseChecklist from './course-checklist';
import GroupConfigurations from './group-configurations';
import CourseLibraries from './course-libraries';

/**
* As of this writing, these routes are mounted at a path prefixed with the following:
Expand Down Expand Up @@ -56,6 +57,10 @@ const CourseAuthoringRoutes = () => {
path="course_info"
element={<PageWrap><CourseUpdates courseId={courseId} /></PageWrap>}
/>
<Route
path="libraries"
element={<PageWrap><CourseLibraries courseId={courseId} /></PageWrap>}
/>
<Route
path="assets"
element={<PageWrap><FilesPage courseId={courseId} /></PageWrap>}
Expand Down
148 changes: 148 additions & 0 deletions src/course-libraries/CourseLibraries.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import fetchMock from 'fetch-mock-jest';
import { cloneDeep } from 'lodash';

import userEvent from '@testing-library/user-event';
import {
initializeMocks,
render,
screen,
within,
} from '../testUtils';
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
import mockInfoResult from './__mocks__/courseBlocksInfo.json';
import CourseLibraries from './CourseLibraries';
import { mockGetEntityLinksByDownstreamContext } from './data/api.mocks';

mockContentSearchConfig.applyMock();
mockGetEntityLinksByDownstreamContext.applyMock();

const searchEndpoint = 'http://mock.meilisearch.local/indexes/studio/search';

jest.mock('../studio-home/hooks', () => ({
useStudioHome: () => ({
isLoadingPage: false,
isFailedLoadingPage: false,
librariesV2Enabled: true,
}),
}));

describe('<CourseLibraries />', () => {
beforeEach(() => {
initializeMocks();
fetchMock.mockReset();

// The Meilisearch client-side API uses fetch, not Axios.
fetchMock.post(searchEndpoint, (_url, req) => {
const requestData = JSON.parse(req.body?.toString() ?? '');
const filter = requestData?.filter[1];
const mockInfoResultCopy = cloneDeep(mockInfoResult);
const resp = mockInfoResultCopy.filter((o: { filter: string }) => o.filter === filter)[0] || {
result: {
hits: [],
query: '',
processingTimeMs: 0,
limit: 4,
offset: 0,
estimatedTotalHits: 0,
},
};
const { result } = resp;
return result;
});
});

const renderCourseLibrariesPage = async (courseKey?: string) => {
const courseId = courseKey || mockGetEntityLinksByDownstreamContext.courseKey;
render(<CourseLibraries courseId={courseId} />);
};

it('shows the spinner before the query is complete', async () => {
// This mock will never return data (it loads forever):
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyLoading);
const spinner = await screen.findByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});

it('shows empty state wheen no links are present', async () => {
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyEmpty);
const emptyMsg = await screen.findByText('This course does not use any content from libraries.');
expect(emptyMsg).toBeInTheDocument();
});

it('shows alert when out of sync components are present', async () => {
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
const alert = await screen.findByRole('alert');
expect(await within(alert).findByText(
'1 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
expect(allTab).toHaveAttribute('aria-selected', 'true');

const reviewBtn = await screen.findByRole('button', { name: 'Review' });
userEvent.click(reviewBtn);

expect(allTab).toHaveAttribute('aria-selected', 'false');
expect(await screen.findByRole('tab', { name: 'Review Content Updates (1)' })).toHaveAttribute('aria-selected', 'true');
expect(alert).not.toBeInTheDocument();

// go back to all tab
userEvent.click(allTab);
// alert should not be back
expect(alert).not.toBeInTheDocument();
expect(allTab).toHaveAttribute('aria-selected', 'true');

// review updates button
const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' });
userEvent.click(reviewActionBtn);
expect(await screen.findByRole('tab', { name: 'Review Content Updates (1)' })).toHaveAttribute('aria-selected', 'true');
});

it('hide alert on dismiss', async () => {
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
const alert = await screen.findByRole('alert');
expect(await within(alert).findByText(
'1 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
userEvent.click(dismissBtn);
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
expect(allTab).toHaveAttribute('aria-selected', 'true');

expect(alert).not.toBeInTheDocument();
});

it('shows links split by library', async () => {
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
const msg = await screen.findByText('This course contains content from these libraries.');
expect(msg).toBeInTheDocument();
const allButtons = await screen.findAllByRole('button');
// total 3 components used from lib 1
const expectedLib1Blocks = 3;
// total 4 components used from lib 1
const expectedLib2Blocks = 4;
// 1 component has updates.
const expectedLib2ToUpdate = 1;

const libraryCards = allButtons.filter((el) => el.classList.contains('collapsible-trigger'));
expect(libraryCards.length).toEqual(2);
expect(await within(libraryCards[0]).findByText('CS problems 2')).toBeInTheDocument();
expect(await within(libraryCards[0]).findByText(`${expectedLib1Blocks} components applied`)).toBeInTheDocument();
expect(await within(libraryCards[0]).findByText('All components up to date')).toBeInTheDocument();

const libParent1 = libraryCards[0].parentElement;
expect(libParent1).not.toBeNull();
userEvent.click(libraryCards[0]);
const xblockCards1 = libParent1!.querySelectorAll('div.card');
expect(xblockCards1.length).toEqual(expectedLib1Blocks);

expect(await within(libraryCards[1]).findByText('CS problems 3')).toBeInTheDocument();
expect(await within(libraryCards[1]).findByText(`${expectedLib2Blocks} components applied`)).toBeInTheDocument();
expect(await within(libraryCards[1]).findByText(`${expectedLib2ToUpdate} component out of sync`)).toBeInTheDocument();

const libParent2 = libraryCards[1].parentElement;
expect(libParent2).not.toBeNull();
userEvent.click(libraryCards[1]);
const xblockCards2 = libParent2!.querySelectorAll('div.card');
expect(xblockCards2.length).toEqual(expectedLib2Blocks);
});
});
Loading

0 comments on commit 8275bbe

Please sign in to comment.