Skip to content

Commit 4d67e8b

Browse files
rpenidopomegranitedChrisChV
authored
feat: improve collection sidebar (#1320)
* feat: improve collection sidebar * feat: add comments to splice blockTypesArray code Co-authored-by: Jillian <[email protected]> --------- Co-authored-by: Jillian <[email protected]> Co-authored-by: Chris Chávez <[email protected]>
1 parent c80483c commit 4d67e8b

29 files changed

+1154
-138
lines changed

src/library-authoring/LibraryAuthoringPage.test.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import {
1111
} from '../testUtils';
1212
import mockResult from './__mocks__/library-search.json';
1313
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
14-
import { mockContentLibrary, mockLibraryBlockTypes, mockXBlockFields } from './data/api.mocks';
14+
import {
15+
mockContentLibrary,
16+
mockGetCollectionMetadata,
17+
mockLibraryBlockTypes,
18+
mockXBlockFields,
19+
} from './data/api.mocks';
1520
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
1621
import { mockBroadcastChannel } from '../generic/data/api.mock';
1722
import { LibraryLayout } from '.';
1823
import { getLibraryCollectionsApiUrl } from './data/api';
1924

25+
mockGetCollectionMetadata.applyMock();
2026
mockContentSearchConfig.applyMock();
2127
mockContentLibrary.applyMock();
2228
mockLibraryBlockTypes.applyMock();
@@ -458,6 +464,25 @@ describe('<LibraryAuthoringPage />', () => {
458464
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
459465
});
460466

467+
it('should open and close the collection sidebar', async () => {
468+
await renderLibraryPage();
469+
470+
// Click on the first component. It could appear twice, in both "Recently Modified" and "Collections"
471+
fireEvent.click((await screen.findAllByText('Collection 1'))[0]);
472+
473+
const sidebar = screen.getByTestId('library-sidebar');
474+
475+
const { getByRole, getByText } = within(sidebar);
476+
477+
// The mock data for the sidebar has a title of "Test Collection"
478+
await waitFor(() => expect(getByText('Test Collection')).toBeInTheDocument());
479+
480+
const closeButton = getByRole('button', { name: /close/i });
481+
fireEvent.click(closeButton);
482+
483+
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
484+
});
485+
461486
it('can filter by capa problem type', async () => {
462487
const problemTypes = {
463488
'Multiple Choice': 'choiceresponse',

src/library-authoring/__mocks__/collection-search.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@
200200
}
201201
],
202202
"created": 1726740779.564664,
203-
"modified": 1726740811.684142,
203+
"modified": 1726840811.684142,
204204
"usage_key": "lib-collection:OpenedX:CSPROB2:collection-from-meilisearch",
205205
"context_key": "lib:OpenedX:CSPROB2",
206206
"org": "OpenedX",

src/library-authoring/__mocks__/library-search.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@
284284
"hits": [
285285
{
286286
"display_name": "Collection 1",
287+
"block_id": "col1",
287288
"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.",
288289
"id": 1,
289290
"type": "collection",
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import type MockAdapter from 'axios-mock-adapter';
2+
import fetchMock from 'fetch-mock-jest';
3+
4+
import { mockContentSearchConfig, mockGetBlockTypes } from '../../search-manager/data/api.mock';
5+
import {
6+
initializeMocks,
7+
fireEvent,
8+
render,
9+
screen,
10+
waitFor,
11+
within,
12+
} from '../../testUtils';
13+
import * as api from '../data/api';
14+
import { mockContentLibrary, mockGetCollectionMetadata } from '../data/api.mocks';
15+
import CollectionDetails from './CollectionDetails';
16+
17+
let axiosMock: MockAdapter;
18+
let mockShowToast: (message: string) => void;
19+
20+
mockGetCollectionMetadata.applyMock();
21+
mockContentSearchConfig.applyMock();
22+
mockGetBlockTypes.applyMock();
23+
24+
const { collectionId } = mockGetCollectionMetadata;
25+
const { description: originalDescription } = mockGetCollectionMetadata.collectionData;
26+
27+
const library = mockContentLibrary.libraryData;
28+
29+
describe('<CollectionDetails />', () => {
30+
beforeEach(() => {
31+
const mocks = initializeMocks();
32+
axiosMock = mocks.axiosMock;
33+
mockShowToast = mocks.mockShowToast;
34+
});
35+
36+
afterEach(() => {
37+
jest.clearAllMocks();
38+
axiosMock.restore();
39+
fetchMock.mockReset();
40+
});
41+
42+
it('should render Collection Details', async () => {
43+
render(<CollectionDetails library={library} collectionId={collectionId} />);
44+
45+
// Collection Description
46+
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();
47+
expect(screen.getByText(originalDescription)).toBeInTheDocument();
48+
49+
// Collection History
50+
expect(screen.getByText('Collection History')).toBeInTheDocument();
51+
// Modified date
52+
expect(screen.getByText('September 20, 2024')).toBeInTheDocument();
53+
// Created date
54+
expect(screen.getByText('September 19, 2024')).toBeInTheDocument();
55+
});
56+
57+
it('should allow modifying the description', async () => {
58+
render(<CollectionDetails library={library} collectionId={collectionId} />);
59+
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();
60+
61+
expect(screen.getByText(originalDescription)).toBeInTheDocument();
62+
63+
const url = api.getLibraryCollectionApiUrl(library.id, collectionId);
64+
axiosMock.onPatch(url).reply(200);
65+
66+
const textArea = screen.getByRole('textbox');
67+
68+
// Change the description to the same value
69+
fireEvent.focus(textArea);
70+
fireEvent.change(textArea, { target: { value: originalDescription } });
71+
fireEvent.blur(textArea);
72+
73+
await waitFor(() => {
74+
expect(axiosMock.history.patch).toHaveLength(0);
75+
expect(mockShowToast).not.toHaveBeenCalled();
76+
});
77+
78+
// Change the description to a new value
79+
fireEvent.focus(textArea);
80+
fireEvent.change(textArea, { target: { value: 'New description' } });
81+
fireEvent.blur(textArea);
82+
83+
await waitFor(() => {
84+
expect(axiosMock.history.patch).toHaveLength(1);
85+
expect(axiosMock.history.patch[0].url).toEqual(url);
86+
expect(axiosMock.history.patch[0].data).toEqual(JSON.stringify({ description: 'New description' }));
87+
expect(mockShowToast).toHaveBeenCalledWith('Collection updated successfully.');
88+
});
89+
});
90+
91+
it('should show error while modifing the description', async () => {
92+
render(<CollectionDetails library={library} collectionId={collectionId} />);
93+
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();
94+
95+
expect(screen.getByText(originalDescription)).toBeInTheDocument();
96+
97+
const url = api.getLibraryCollectionApiUrl(library.id, collectionId);
98+
axiosMock.onPatch(url).reply(500);
99+
100+
const textArea = screen.getByRole('textbox');
101+
102+
// Change the description to a new value
103+
fireEvent.focus(textArea);
104+
fireEvent.change(textArea, { target: { value: 'New description' } });
105+
fireEvent.blur(textArea);
106+
107+
await waitFor(() => {
108+
expect(axiosMock.history.patch).toHaveLength(1);
109+
expect(axiosMock.history.patch[0].url).toEqual(url);
110+
expect(axiosMock.history.patch[0].data).toEqual(JSON.stringify({ description: 'New description' }));
111+
expect(mockShowToast).toHaveBeenCalledWith('Failed to update collection.');
112+
});
113+
});
114+
115+
it('should render Collection stats', async () => {
116+
mockGetBlockTypes('someBlocks');
117+
render(<CollectionDetails library={library} collectionId={collectionId} />);
118+
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();
119+
120+
expect(screen.getByText('Collection Stats')).toBeInTheDocument();
121+
expect(await screen.findByText('Total')).toBeInTheDocument();
122+
123+
[
124+
{ blockType: 'Total', count: 3 },
125+
{ blockType: 'Text', count: 2 },
126+
{ blockType: 'Problem', count: 1 },
127+
].forEach(({ blockType, count }) => {
128+
const blockCount = screen.getByText(blockType).closest('div') as HTMLDivElement;
129+
expect(within(blockCount).getByText(count.toString())).toBeInTheDocument();
130+
});
131+
});
132+
133+
it('should render Collection stats for empty collection', async () => {
134+
mockGetBlockTypes('noBlocks');
135+
render(<CollectionDetails library={library} collectionId={collectionId} />);
136+
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();
137+
138+
expect(screen.getByText('Collection Stats')).toBeInTheDocument();
139+
expect(await screen.findByText('This collection is currently empty.')).toBeInTheDocument();
140+
});
141+
142+
it('should render Collection stats for big collection', async () => {
143+
mockGetBlockTypes('moreBlocks');
144+
render(<CollectionDetails library={library} collectionId={collectionId} />);
145+
expect(await screen.findByText('Description / Card Preview Text')).toBeInTheDocument();
146+
147+
expect(screen.getByText('Collection Stats')).toBeInTheDocument();
148+
expect(await screen.findByText('36')).toBeInTheDocument();
149+
150+
[
151+
{ blockType: 'Total', count: 36 },
152+
{ blockType: 'Video', count: 8 },
153+
{ blockType: 'Problem', count: 7 },
154+
{ blockType: 'Text', count: 6 },
155+
{ blockType: 'Other', count: 15 },
156+
].forEach(({ blockType, count }) => {
157+
const blockCount = screen.getByText(blockType).closest('div') as HTMLDivElement;
158+
expect(within(blockCount).getByText(count.toString())).toBeInTheDocument();
159+
});
160+
});
161+
});

0 commit comments

Comments
 (0)