diff --git a/src/library-authoring/components/LibraryComponents.test.tsx b/src/library-authoring/components/LibraryComponents.test.tsx index 83919d94e7..5dc689cb14 100644 --- a/src/library-authoring/components/LibraryComponents.test.tsx +++ b/src/library-authoring/components/LibraryComponents.test.tsx @@ -1,38 +1,60 @@ import React from 'react'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, fireEvent } from '@testing-library/react'; -import LibraryComponents from './LibraryComponents'; +import MockAdapter from 'axios-mock-adapter'; +import fetchMock from 'fetch-mock-jest'; +import type { Store } from 'redux'; +import { getContentSearchConfigUrl } from '../../search-modal/data/api'; +import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json'; +import { SearchContextProvider } from '../../search-modal/manager/SearchManager'; import initializeStore from '../../store'; import { libraryComponentsMock } from '../__mocks__'; +import LibraryComponents from './LibraryComponents'; + +const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; -const mockUseLibraryComponents = jest.fn(); -const mockUseLibraryComponentCount = jest.fn(); const mockUseLibraryBlockTypes = jest.fn(); const mockFetchNextPage = jest.fn(); -let store; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); +const mockUseSearchContext = jest.fn(); const data = { + totalHits: 1, hits: [], isFetching: true, isFetchingNextPage: false, hasNextPage: false, fetchNextPage: mockFetchNextPage, + searchKeywords: '', }; -const countData = { - componentCount: 1, - collectionCount: 0, + +let store: Store; +let axiosMock: MockAdapter; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const returnEmptyResult = (_url: string, 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. + mockEmptyResult.results[0].query = query; + // And fake the required '_formatted' fields; it contains the highlighting ... around matched words + // eslint-disable-next-line no-underscore-dangle, no-param-reassign + mockEmptyResult.results[0]?.hits.forEach((hit: any) => { hit._formatted = { ...hit }; }); + return mockEmptyResult; }; + const blockTypeData = { data: [ { @@ -51,16 +73,21 @@ const blockTypeData = { }; jest.mock('../data/apiHook', () => ({ - useLibraryComponents: () => mockUseLibraryComponents(), - useLibraryComponentCount: () => mockUseLibraryComponentCount(), useLibraryBlockTypes: () => mockUseLibraryBlockTypes(), })); +jest.mock('../../search-modal/manager/SearchManager', () => ({ + ...jest.requireActual('../../search-modal/manager/SearchManager'), + useSearchContext: () => mockUseSearchContext(), +})); + const RootWrapper = (props) => ( - + + + @@ -77,9 +104,18 @@ describe('', () => { }, }); store = initializeStore(); - mockUseLibraryComponents.mockReturnValue(data); - mockUseLibraryComponentCount.mockReturnValue(countData); mockUseLibraryBlockTypes.mockReturnValue(blockTypeData); + mockUseSearchContext.mockReturnValue(data); + + fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); + + // The API method to get the Meilisearch connection details uses Axios: + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(getContentSearchConfigUrl()).reply(200, { + url: 'http://mock.meilisearch.local', + index_name: 'studio', + api_key: 'test-key', + }); }); afterEach(() => { @@ -87,10 +123,11 @@ describe('', () => { }); it('should render empty state', async () => { - mockUseLibraryComponentCount.mockReturnValueOnce({ - ...countData, - componentCount: 0, + mockUseSearchContext.mockReturnValue({ + ...data, + totalHits: 0, }); + render(); expect(await screen.findByText(/you have not added any content to this library yet\./i)); }); @@ -101,7 +138,7 @@ describe('', () => { }); it('should render components in full variant', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: false, @@ -117,7 +154,7 @@ describe('', () => { }); it('should render components in preview variant', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: false, @@ -133,7 +170,7 @@ describe('', () => { }); it('should call `fetchNextPage` on scroll to bottom in full variant', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: false, @@ -151,7 +188,7 @@ describe('', () => { }); it('should not call `fetchNextPage` on croll to bottom in preview variant', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: false, @@ -169,7 +206,7 @@ describe('', () => { }); it('should render content and loading when fetching next page', async () => { - mockUseLibraryComponents.mockReturnValue({ + mockUseSearchContext.mockReturnValue({ ...data, hits: libraryComponentsMock, isFetching: true, diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 1d416a2cc3..372dbb7226 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -8,10 +8,7 @@ import { ComponentCard, ComponentCardLoading } from './ComponentCard'; type LibraryComponentsProps = { libraryId: string, - filter: { - searchKeywords: string, - }, - variant: string, + variant: 'full' | 'preview', }; /** diff --git a/src/library-authoring/data/apiHook.ts b/src/library-authoring/data/apiHook.ts index a6d9b13f5c..b7e6b92cf8 100644 --- a/src/library-authoring/data/apiHook.ts +++ b/src/library-authoring/data/apiHook.ts @@ -1,4 +1,3 @@ -import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { getContentLibrary, getLibraryBlockTypes } from './api'; diff --git a/src/search-modal/manager/SearchManager.ts b/src/search-modal/manager/SearchManager.ts index 7b1204fa7d..eed267b46c 100644 --- a/src/search-modal/manager/SearchManager.ts +++ b/src/search-modal/manager/SearchManager.ts @@ -42,8 +42,8 @@ export const SearchContextProvider: React.FC<{ closeSearchModal?: () => void, }> = ({ extraFilter, ...props }) => { const [searchKeywords, setSearchKeywords] = React.useState(''); - const [blockTypesFilter, setBlockTypesFilter] = React.useState(/** type {string[]} */([])); - const [tagsFilter, setTagsFilter] = React.useState(/** type {string[]} */([])); + const [blockTypesFilter, setBlockTypesFilter] = React.useState([]); + const [tagsFilter, setTagsFilter] = React.useState([]); const canClearFilters = blockTypesFilter.length > 0 || tagsFilter.length > 0; const clearFilters = React.useCallback(() => {