Skip to content

Commit 356a26e

Browse files
committed
refactor: fetch single collection info from meilisearch
1 parent a93ce43 commit 356a26e

File tree

13 files changed

+207
-201
lines changed

13 files changed

+207
-201
lines changed

src/library-authoring/LibraryAuthoringPage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ const LibraryAuthoringPage = () => {
166166
<Container size="xl" className="px-4 mt-4 mb-5 library-authoring-page">
167167
<SearchContextProvider
168168
extraFilter={`context_key = "${libraryId}"`}
169-
fetchCollections
170169
>
171170
<SubHeader
172171
title={<SubHeaderTitle title={libraryData.title} canEditLibrary={libraryData.canEditLibrary} />}

src/library-authoring/LibraryRecentlyModified.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ const LibraryRecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId })
8181
<SearchContextProvider
8282
extraFilter={`context_key = "${libraryId}"`}
8383
overrideSearchSortOrder={SearchSortOption.RECENTLY_MODIFIED}
84-
fetchCollections
8584
>
8685
<RecentlyModified libraryId={libraryId} />
8786
</SearchContextProvider>

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,35 @@
184184
"content.problem_types": {}
185185
},
186186
"facetStats": {}
187+
},
188+
{
189+
"indexUid": "studio_content",
190+
"hits": [
191+
{
192+
"display_name": "My first collection",
193+
"block_id": "my-first-collection",
194+
"description": "A collection for testing",
195+
"id": 1,
196+
"type": "collection",
197+
"breadcrumbs": [
198+
{
199+
"display_name": "CS problems 2"
200+
}
201+
],
202+
"created": 1726740779.564664,
203+
"modified": 1726740811.684142,
204+
"usage_key": "lib-collection:OpenedX:CSPROB2:collection-from-meilisearch",
205+
"context_key": "lib:OpenedX:CSPROB2",
206+
"org": "OpenedX",
207+
"access_id": 16,
208+
"num_children": 5
209+
}
210+
],
211+
"query": "",
212+
"processingTimeMs": 0,
213+
"limit": 1,
214+
"offset": 0,
215+
"estimatedTotalHits": 1
187216
}
188217
]
189218
}

src/library-authoring/collections/CollectionInfoHeader.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Collection } from '../data/api';
1+
import { type CollectionHit } from '../../search-manager/data/api';
22

33
interface CollectionInfoHeaderProps {
4-
collection?: Collection;
4+
collection?: CollectionHit;
55
}
66

77
const CollectionInfoHeader = ({ collection } : CollectionInfoHeaderProps) => (
88
<div className="d-flex flex-wrap">
9-
{collection?.title}
9+
{collection?.displayName}
1010
</div>
1111
);
1212

src/library-authoring/collections/LibraryCollectionPage.test.tsx

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fetchMock from 'fetch-mock-jest';
2+
import { cloneDeep } from 'lodash';
23
import {
34
fireEvent,
45
initializeMocks,
@@ -8,41 +9,29 @@ import {
89
within,
910
} from '../../testUtils';
1011
import mockResult from '../__mocks__/collection-search.json';
11-
import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json';
1212
import {
13-
mockCollection, mockContentLibrary, mockLibraryBlockTypes, mockXBlockFields,
13+
mockContentLibrary, mockLibraryBlockTypes, mockXBlockFields,
1414
} from '../data/api.mocks';
1515
import { mockContentSearchConfig } from '../../search-manager/data/api.mock';
1616
import { mockBroadcastChannel } from '../../generic/data/api.mock';
1717
import { LibraryLayout } from '..';
1818

1919
mockContentSearchConfig.applyMock();
2020
mockContentLibrary.applyMock();
21-
mockCollection.applyMock();
2221
mockLibraryBlockTypes.applyMock();
2322
mockXBlockFields.applyMock();
2423
mockBroadcastChannel();
2524

2625
const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
27-
28-
/**
29-
* Returns 0 components from the search query.
30-
*/
31-
const returnEmptyResult = (_url, req) => {
32-
const requestData = JSON.parse(req.body?.toString() ?? '');
33-
const query = requestData?.queries[0]?.q ?? '';
34-
// We have to replace the query (search keywords) in the mock results with the actual query,
35-
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
36-
mockEmptyResult.results[0].query = query;
37-
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
38-
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
39-
mockEmptyResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
40-
return mockEmptyResult;
41-
};
42-
4326
const path = '/library/:libraryId/*';
4427
const libraryTitle = mockContentLibrary.libraryData.title;
45-
const collectionTitle = mockCollection.collectionData.title;
28+
const mockCollection = {
29+
collectionId: mockResult.results[2].hits[0].block_id,
30+
collectionNeverLoads: 'collection-always-loading',
31+
collectionEmpty: 'collection-no-data',
32+
collectionNoComponents: 'collection-no-components',
33+
title: mockResult.results[2].hits[0].display_name,
34+
};
4635

4736
describe('<LibraryCollectionPage />', () => {
4837
beforeEach(() => {
@@ -52,14 +41,33 @@ describe('<LibraryCollectionPage />', () => {
5241
fetchMock.post(searchEndpoint, (_url, req) => {
5342
const requestData = JSON.parse(req.body?.toString() ?? '');
5443
const query = requestData?.queries[0]?.q ?? '';
44+
const mockResultCopy = cloneDeep(mockResult);
5545
// We have to replace the query (search keywords) in the mock results with the actual query,
5646
// because otherwise Instantsearch will update the UI and change the query,
5747
// leading to unexpected results in the test cases.
58-
mockResult.results[0].query = query;
48+
mockResultCopy.results[0].query = query;
49+
mockResultCopy.results[2].query = query;
5950
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
6051
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
61-
mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
62-
return mockResult;
52+
mockResultCopy.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
53+
const collectionQueryId = requestData?.queries[2]?.filter[2]?.split('block_id = "')[1].split('"')[0];
54+
switch (collectionQueryId) {
55+
case mockCollection.collectionNeverLoads:
56+
return new Promise<any>(() => {});
57+
case mockCollection.collectionEmpty:
58+
mockResultCopy.results[2].hits = [];
59+
mockResultCopy.results[2].estimatedTotalHits = 0;
60+
break;
61+
case mockCollection.collectionNoComponents:
62+
mockResultCopy.results[0].hits = [];
63+
mockResultCopy.results[0].estimatedTotalHits = 0;
64+
mockResultCopy.results[1].facetDistribution.block_type = {};
65+
mockResultCopy.results[2].hits[0].num_children = 0;
66+
break;
67+
default:
68+
break;
69+
}
70+
return mockResultCopy;
6371
});
6472
});
6573

@@ -69,34 +77,39 @@ describe('<LibraryCollectionPage />', () => {
6977
});
7078

7179
const renderLibraryCollectionPage = async (collectionId?: string, libraryId?: string) => {
72-
const libId = libraryId || mockCollection.libraryId;
80+
const libId = libraryId || mockContentLibrary.libraryId;
7381
const colId = collectionId || mockCollection.collectionId;
7482
render(<LibraryLayout />, {
7583
path,
7684
routerProps: {
7785
initialEntries: [`/library/${libId}/collection/${colId}`],
7886
},
7987
});
88+
89+
if (colId !== mockCollection.collectionNeverLoads) {
90+
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
91+
}
8092
};
8193

8294
it('shows the spinner before the query is complete', async () => {
8395
// This mock will never return data about the collection (it loads forever):
84-
await renderLibraryCollectionPage(mockCollection.collectionIdThatNeverLoads);
96+
await renderLibraryCollectionPage(mockCollection.collectionNeverLoads);
8597
const spinner = screen.getByRole('status');
8698
expect(spinner.textContent).toEqual('Loading...');
8799
});
88100

89101
it('shows an error component if no collection returned', async () => {
90-
// This mock will simulate a 404 error:
91-
await renderLibraryCollectionPage(mockCollection.collection404);
102+
// This mock will simulate incorrect collection id
103+
await renderLibraryCollectionPage(mockCollection.collectionEmpty);
104+
screen.debug();
92105
expect(await screen.findByTestId('notFoundAlert')).toBeInTheDocument();
93106
});
94107

95108
it('shows collection data', async () => {
96109
await renderLibraryCollectionPage();
97110
expect(await screen.findByText('All Collections')).toBeInTheDocument();
98111
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
99-
expect((await screen.findAllByText(collectionTitle))[0]).toBeInTheDocument();
112+
expect((await screen.findAllByText(mockCollection.title))[0]).toBeInTheDocument();
100113

101114
expect(screen.queryByText('This collection is currently empty.')).not.toBeInTheDocument();
102115

@@ -108,12 +121,11 @@ describe('<LibraryCollectionPage />', () => {
108121
});
109122

110123
it('shows a collection without associated components', async () => {
111-
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
112-
await renderLibraryCollectionPage();
124+
await renderLibraryCollectionPage(mockCollection.collectionNoComponents);
113125

114126
expect(await screen.findByText('All Collections')).toBeInTheDocument();
115127
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
116-
expect((await screen.findAllByText(collectionTitle))[0]).toBeInTheDocument();
128+
expect((await screen.findAllByText(mockCollection.title))[0]).toBeInTheDocument();
117129

118130
expect(screen.getByText('This collection is currently empty.')).toBeInTheDocument();
119131

@@ -125,7 +137,7 @@ describe('<LibraryCollectionPage />', () => {
125137
it('shows the new content button', async () => {
126138
await renderLibraryCollectionPage();
127139

128-
expect(await screen.findByRole('heading')).toBeInTheDocument();
140+
expect(await screen.findByText('All Collections')).toBeInTheDocument();
129141
expect(await screen.findByText('Content (5)')).toBeInTheDocument();
130142
expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument();
131143
expect(screen.queryByText('Read Only')).not.toBeInTheDocument();
@@ -135,9 +147,7 @@ describe('<LibraryCollectionPage />', () => {
135147
// Use a library mock that is read-only:
136148
const libraryId = mockContentLibrary.libraryIdReadOnly;
137149
// Update search mock so it returns no results:
138-
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
139-
await renderLibraryCollectionPage(mockCollection.collectionId, libraryId);
140-
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
150+
await renderLibraryCollectionPage(mockCollection.collectionNoComponents, libraryId);
141151

142152
expect(await screen.findByText('All Collections')).toBeInTheDocument();
143153
expect(screen.getByText('This collection is currently empty.')).toBeInTheDocument();
@@ -147,27 +157,23 @@ describe('<LibraryCollectionPage />', () => {
147157

148158
it('show a collection without search results', async () => {
149159
// Update search mock so it returns no results:
150-
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
151-
await renderLibraryCollectionPage();
160+
await renderLibraryCollectionPage(mockCollection.collectionNoComponents);
152161

153162
expect(await screen.findByText('All Collections')).toBeInTheDocument();
154163
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
155-
expect((await screen.findAllByText(collectionTitle))[0]).toBeInTheDocument();
156-
157-
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
164+
expect((await screen.findAllByText(mockCollection.title))[0]).toBeInTheDocument();
158165

159166
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'noresults' } });
160167

161168
// Ensure the search endpoint is called again, only once more since the recently modified call
162169
// should not be impacted by the search
163170
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
164171

165-
expect(screen.getByText('No matching components found in this collections.')).toBeInTheDocument();
172+
expect(screen.queryByText('No matching components found in this collections.')).toBeInTheDocument();
166173
});
167174

168175
it('should open and close new content sidebar', async () => {
169176
await renderLibraryCollectionPage();
170-
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
171177

172178
expect(await screen.findByText('All Collections')).toBeInTheDocument();
173179
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
@@ -188,8 +194,8 @@ describe('<LibraryCollectionPage />', () => {
188194

189195
expect(await screen.findByText('All Collections')).toBeInTheDocument();
190196
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
191-
expect((await screen.findAllByText(collectionTitle))[0]).toBeInTheDocument();
192-
expect((await screen.findAllByText(collectionTitle))[1]).toBeInTheDocument();
197+
expect((await screen.findAllByText(mockCollection.title))[0]).toBeInTheDocument();
198+
expect((await screen.findAllByText(mockCollection.title))[1]).toBeInTheDocument();
193199

194200
expect(screen.getByText('Manage')).toBeInTheDocument();
195201
expect(screen.getByText('Details')).toBeInTheDocument();
@@ -200,8 +206,8 @@ describe('<LibraryCollectionPage />', () => {
200206

201207
expect(await screen.findByText('All Collections')).toBeInTheDocument();
202208
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
203-
expect((await screen.findAllByText(collectionTitle))[0]).toBeInTheDocument();
204-
expect((await screen.findAllByText(collectionTitle))[1]).toBeInTheDocument();
209+
expect((await screen.findAllByText(mockCollection.title))[0]).toBeInTheDocument();
210+
expect((await screen.findAllByText(mockCollection.title))[1]).toBeInTheDocument();
205211

206212
// Open by default; close the library info sidebar
207213
const closeButton = screen.getByRole('button', { name: /close/i });
@@ -218,7 +224,6 @@ describe('<LibraryCollectionPage />', () => {
218224

219225
it('sorts collection components', async () => {
220226
await renderLibraryCollectionPage();
221-
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
222227

223228
expect(await screen.findByTitle('Sort search results')).toBeInTheDocument();
224229

@@ -310,9 +315,7 @@ describe('<LibraryCollectionPage />', () => {
310315
});
311316

312317
it('has an empty type filter when there are no results', async () => {
313-
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
314-
await renderLibraryCollectionPage();
315-
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
318+
await renderLibraryCollectionPage(mockCollection.collectionNoComponents);
316319

317320
const filterButton = screen.getByRole('button', { name: /type/i });
318321
fireEvent.click(filterButton);

0 commit comments

Comments
 (0)