Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve collection sidebar #1320

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ import {
} from '../testUtils';
import mockResult from './__mocks__/library-search.json';
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
import { mockContentLibrary, mockLibraryBlockTypes, mockXBlockFields } from './data/api.mocks';
import {
mockContentLibrary,
mockGetCollectionMetadata,
mockLibraryBlockTypes,
mockXBlockFields,
} from './data/api.mocks';
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
import { mockBroadcastChannel } from '../generic/data/api.mock';
import { LibraryLayout } from '.';
import { getLibraryCollectionsApiUrl } from './data/api';

mockGetCollectionMetadata.applyMock();
mockContentSearchConfig.applyMock();
mockContentLibrary.applyMock();
mockLibraryBlockTypes.applyMock();
Expand Down Expand Up @@ -458,6 +464,25 @@ describe('<LibraryAuthoringPage />', () => {
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});

it('should open and close the collection sidebar', async () => {
await renderLibraryPage();

// Click on the first component. It could appear twice, in both "Recently Modified" and "Collections"
fireEvent.click((await screen.findAllByText('Collection 1'))[0]);

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('can filter by capa problem type', async () => {
const problemTypes = {
'Multiple Choice': 'choiceresponse',
Expand Down
2 changes: 1 addition & 1 deletion src/library-authoring/__mocks__/collection-search.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
}
],
"created": 1726740779.564664,
"modified": 1726740811.684142,
"modified": 1726840811.684142,
"usage_key": "lib-collection:OpenedX:CSPROB2:collection-from-meilisearch",
"context_key": "lib:OpenedX:CSPROB2",
"org": "OpenedX",
Expand Down
1 change: 1 addition & 0 deletions src/library-authoring/__mocks__/library-search.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@
"hits": [
{
"display_name": "Collection 1",
"block_id": "col1",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer.",
"id": 1,
"type": "collection",
Expand Down
161 changes: 161 additions & 0 deletions src/library-authoring/collections/CollectionDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type MockAdapter from 'axios-mock-adapter';
import fetchMock from 'fetch-mock-jest';

import { mockContentSearchConfig, mockGetBlockTypes } from '../../search-manager/data/api.mock';
import {
initializeMocks,
fireEvent,
render,
screen,
waitFor,
within,
} from '../../testUtils';
import * as api from '../data/api';
import { mockContentLibrary, mockGetCollectionMetadata } from '../data/api.mocks';
import CollectionDetails from './CollectionDetails';

let axiosMock: MockAdapter;
let mockShowToast: (message: string) => void;

mockGetCollectionMetadata.applyMock();
mockContentSearchConfig.applyMock();
mockGetBlockTypes.applyMock();

const { collectionId } = mockGetCollectionMetadata;
const { description: originalDescription } = mockGetCollectionMetadata.collectionData;

const library = mockContentLibrary.libraryData;

describe('<CollectionDetails />', () => {
beforeEach(() => {
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
mockShowToast = mocks.mockShowToast;
});

afterEach(() => {
jest.clearAllMocks();
axiosMock.restore();
fetchMock.mockReset();
});

it('should render Collection Details', async () => {
render(<CollectionDetails library={library} collectionId={collectionId} />);

// Collection Description
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();
expect(screen.getByText(originalDescription)).toBeInTheDocument();

// Collection History
expect(screen.getByText('Collection History')).toBeInTheDocument();
// Modified date
expect(screen.getByText('September 20, 2024')).toBeInTheDocument();
// Created date
expect(screen.getByText('September 19, 2024')).toBeInTheDocument();
});

it('should allow modifying the description', async () => {
render(<CollectionDetails library={library} collectionId={collectionId} />);
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();

expect(screen.getByText(originalDescription)).toBeInTheDocument();

const url = api.getLibraryCollectionApiUrl(library.id, collectionId);
axiosMock.onPatch(url).reply(200);

const textArea = screen.getByRole('textbox');

// Change the description to the same value
fireEvent.focus(textArea);
fireEvent.change(textArea, { target: { value: originalDescription } });
fireEvent.blur(textArea);

await waitFor(() => {
expect(axiosMock.history.patch).toHaveLength(0);
expect(mockShowToast).not.toHaveBeenCalled();
});

// Change the description to a new value
fireEvent.focus(textArea);
fireEvent.change(textArea, { target: { value: 'New description' } });
fireEvent.blur(textArea);

await waitFor(() => {
expect(axiosMock.history.patch).toHaveLength(1);
expect(axiosMock.history.patch[0].url).toEqual(url);
expect(axiosMock.history.patch[0].data).toEqual(JSON.stringify({ description: 'New description' }));
expect(mockShowToast).toHaveBeenCalledWith('Collection updated successfully.');
});
});

it('should show error while modifing the description', async () => {
render(<CollectionDetails library={library} collectionId={collectionId} />);
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();

expect(screen.getByText(originalDescription)).toBeInTheDocument();

const url = api.getLibraryCollectionApiUrl(library.id, collectionId);
axiosMock.onPatch(url).reply(500);

const textArea = screen.getByRole('textbox');

// Change the description to a new value
fireEvent.focus(textArea);
fireEvent.change(textArea, { target: { value: 'New description' } });
fireEvent.blur(textArea);

await waitFor(() => {
expect(axiosMock.history.patch).toHaveLength(1);
expect(axiosMock.history.patch[0].url).toEqual(url);
expect(axiosMock.history.patch[0].data).toEqual(JSON.stringify({ description: 'New description' }));
expect(mockShowToast).toHaveBeenCalledWith('Failed to update collection.');
});
});

it('should render Collection stats', async () => {
mockGetBlockTypes('someBlocks');
render(<CollectionDetails library={library} collectionId={collectionId} />);
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();

expect(screen.getByText('Collection Stats')).toBeInTheDocument();
expect(await screen.findByText('Total')).toBeInTheDocument();

[
{ blockType: 'Total', count: 3 },
{ blockType: 'Text', count: 2 },
{ blockType: 'Problem', count: 1 },
].forEach(({ blockType, count }) => {
const blockCount = screen.getByText(blockType).closest('div') as HTMLDivElement;
expect(within(blockCount).getByText(count.toString())).toBeInTheDocument();
});
});

it('should render Collection stats for empty collection', async () => {
mockGetBlockTypes('noBlocks');
render(<CollectionDetails library={library} collectionId={collectionId} />);
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();

expect(screen.getByText('Collection Stats')).toBeInTheDocument();
expect(await screen.findByText('This collection is currently empty.')).toBeInTheDocument();
});

it('should render Collection stats for big collection', async () => {
mockGetBlockTypes('moreBlocks');
render(<CollectionDetails library={library} collectionId={collectionId} />);
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();

expect(screen.getByText('Collection Stats')).toBeInTheDocument();
expect(await screen.findByText('36')).toBeInTheDocument();

[
{ blockType: 'Total', count: 36 },
{ blockType: 'Video', count: 8 },
{ blockType: 'Problem', count: 7 },
{ blockType: 'Text', count: 6 },
{ blockType: 'Other', count: 15 },
].forEach(({ blockType, count }) => {
const blockCount = screen.getByText(blockType).closest('div') as HTMLDivElement;
expect(within(blockCount).getByText(count.toString())).toBeInTheDocument();
});
});
});
Loading