diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index ccb1e87c42..1dfe2d4186 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -46,6 +46,21 @@ const returnEmptyResult = (_url, req) => { return mockEmptyResult; }; +const returnLowNumberResults = (_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. + mockResult.results[0].query = query; + // Limit number of results to just 2 + mockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2); + mockResult.results[0].estimatedTotalHits = 2; + // And fake the required '_formatted' fields; it contains the highlighting ... around matched words + // eslint-disable-next-line no-underscore-dangle, no-param-reassign + mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); + return mockResult; +}; + const libraryData = { id: 'lib:org1:lib1', type: 'complex', @@ -152,8 +167,10 @@ describe('', () => { getByRole, getByText, queryByText, getAllByText, } = render(); - // Ensure the search endpoint is called - await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + // Ensure the search endpoint is called: + // Call 1: To fetch searchable/filterable/sortable library data + // Call 2: To fetch the recently modified components only + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); expect(getByText('Content library')).toBeInTheDocument(); expect(getByText(libraryData.title)).toBeInTheDocument(); @@ -197,8 +214,10 @@ describe('', () => { expect(await findByText('Content library')).toBeInTheDocument(); expect(await findByText(libraryData.title)).toBeInTheDocument(); - // Ensure the search endpoint is called - await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + // Ensure the search endpoint is called: + // Call 1: To fetch searchable/filterable/sortable library data + // Call 2: To fetch the recently modified components only + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); expect(getByText('You have not added any content to this library yet.')).toBeInTheDocument(); }); @@ -213,13 +232,16 @@ describe('', () => { expect(await findByText('Content library')).toBeInTheDocument(); expect(await findByText(libraryData.title)).toBeInTheDocument(); - // Ensure the search endpoint is called - await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); + // Ensure the search endpoint is called: + // Call 1: To fetch searchable/filterable/sortable library data + // Call 2: To fetch the recently modified components only + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); fireEvent.change(getByRole('searchbox'), { target: { value: 'noresults' } }); - // Ensure the search endpoint is called again - await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + // 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(3, searchEndpoint, 'post'); }); expect(getByText('No matching components found in this library.')).toBeInTheDocument(); @@ -231,4 +253,75 @@ describe('', () => { // This step is necessary to avoid the url change leak to other tests fireEvent.click(getByRole('tab', { name: 'Home' })); }); + + it('show the "View All" button when viewing library with many components', async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + + const { + getByRole, getByText, queryByText, getAllByText, + } = render(); + + // Ensure the search endpoint is called: + // Call 1: To fetch searchable/filterable/sortable library data + // Call 2: To fetch the recently modified components only + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + + expect(getByText('Content library')).toBeInTheDocument(); + expect(getByText(libraryData.title)).toBeInTheDocument(); + + expect(queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument(); + + expect(getByText('Recently Modified')).toBeInTheDocument(); + expect(getByText('Collections (0)')).toBeInTheDocument(); + expect(getByText('Components (6)')).toBeInTheDocument(); + expect(getAllByText('Test HTML Block')[0]).toBeInTheDocument(); + + // There should only be one "View All" button, since the Components count + // are above the preview limit (4) + expect(getByText('View All')).toBeInTheDocument(); + + // Clicking on "View All" button should navigate to the Components tab + fireEvent.click(getByText('View All')); + expect(queryByText('Recently Modified')).not.toBeInTheDocument(); + expect(queryByText('Collections (0)')).not.toBeInTheDocument(); + expect(queryByText('Components (6)')).not.toBeInTheDocument(); + expect(getAllByText('Test HTML Block')[0]).toBeInTheDocument(); + + // Go back to Home tab + // This step is necessary to avoid the url change leak to other tests + fireEvent.click(getByRole('tab', { name: 'Home' })); + expect(getByText('Recently Modified')).toBeInTheDocument(); + expect(getByText('Collections (0)')).toBeInTheDocument(); + expect(getByText('Components (6)')).toBeInTheDocument(); + }); + + it('should not show the "View All" button when viewing library with low number of components', async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + fetchMock.post(searchEndpoint, returnLowNumberResults, { overwriteRoutes: true }); + + const { + getByText, queryByText, getAllByText, + } = render(); + + // Ensure the search endpoint is called: + // Call 1: To fetch searchable/filterable/sortable library data + // Call 2: To fetch the recently modified components only + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + + expect(getByText('Content library')).toBeInTheDocument(); + expect(getByText(libraryData.title)).toBeInTheDocument(); + + expect(queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument(); + + expect(getByText('Recently Modified')).toBeInTheDocument(); + expect(getByText('Collections (0)')).toBeInTheDocument(); + expect(getByText('Components (2)')).toBeInTheDocument(); + expect(getAllByText('Test HTML Block')[0]).toBeInTheDocument(); + + // There should not be any "View All" button on page since Components count + // is less than the preview limit (4) + expect(queryByText('View All')).not.toBeInTheDocument(); + }); }); diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts index 613e0b9e3c..c7a39a3410 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -78,12 +78,12 @@ const messages = defineMessages({ collectionsSectionTitle: { id: 'course-authoring.library-authoring.collections-section-title', defaultMessage: 'Collections ({collectionCount})', - description: 'Title for the Recently Modified section in library home', + description: 'Title for the Collections section in library home', }, componentsSectionTitle: { - id: 'course-authoring.library-authoring.recently-modified-section-title', + id: 'course-authoring.library-authoring.components-section-title', defaultMessage: 'Components ({componentCount})', - description: 'Title for the Recently Modified section in library home', + description: 'Title for the Components section in library home', }, }); diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index 8c59aed4d0..f947603727 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -103,7 +103,7 @@ export const SearchContextProvider: React.FC<{ // Note: SearchSortOption.RELEVANCE is special, it means "no custom sorting", // so we send it to useContentSearchResults as an empty array. const sort: SearchSortOption[] = (overrideSearchSortOrder && [overrideSearchSortOrder]) - || searchSortOrder === SearchSortOption.RELEVANCE ? [] : [searchSortOrder]; + || (searchSortOrder === SearchSortOption.RELEVANCE ? [] : [searchSortOrder]); const canClearFilters = blockTypesFilter.length > 0 || tagsFilter.length > 0; const clearFilters = React.useCallback(() => {