481 lines
23 KiB
TypeScript
481 lines
23 KiB
TypeScript
import React from 'react';
|
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
|
import { initializeMockApp } from '@edx/frontend-platform';
|
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|
import { AppProvider } from '@edx/frontend-platform/react';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import MockAdapter from 'axios-mock-adapter';
|
|
import {
|
|
fireEvent,
|
|
render,
|
|
waitFor,
|
|
within,
|
|
type RenderResult,
|
|
} from '@testing-library/react';
|
|
import fetchMock from 'fetch-mock-jest';
|
|
import type { Store } from 'redux';
|
|
|
|
import initializeStore from '../store';
|
|
import mockResult from './__mocks__/search-result.json';
|
|
import mockEmptyResult from './__mocks__/empty-search-result.json';
|
|
import mockTagsFacetResult from './__mocks__/facet-search.json';
|
|
import mockTagsFacetResultLevel0 from './__mocks__/facet-search-level0.json';
|
|
import mockTagsFacetResultLevel1 from './__mocks__/facet-search-level1.json';
|
|
import mockTagsKeywordSearchResult from './__mocks__/tags-keyword-search.json';
|
|
import SearchUI from './SearchUI';
|
|
import { getContentSearchConfigUrl } from '../search-manager/data/api';
|
|
|
|
// mockResult contains only a single result - this one:
|
|
const mockResultDisplayName = 'Test HTML Block';
|
|
let store: Store;
|
|
|
|
const queryClient = new QueryClient();
|
|
|
|
// Default props for <SearchUI />
|
|
const defaults = {
|
|
courseId: 'course-v1:org+test+123',
|
|
};
|
|
const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
|
|
const facetSearchEndpoint = 'http://mock.meilisearch.local/indexes/studio/facet-search';
|
|
const tagsKeywordSearchEndpoint = 'http://mock.meilisearch.local/indexes/studio/search';
|
|
|
|
const mockNavigate = jest.fn();
|
|
|
|
jest.mock('react-router-dom', () => ({
|
|
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
|
|
useNavigate: () => mockNavigate,
|
|
}));
|
|
|
|
const Wrap: React.FC<{ children:React.ReactNode }> = ({ children }) => (
|
|
<AppProvider store={store}>
|
|
<IntlProvider locale="en" messages={{}}>
|
|
<QueryClientProvider client={queryClient}>
|
|
{children}
|
|
</QueryClientProvider>
|
|
</IntlProvider>
|
|
</AppProvider>
|
|
);
|
|
let axiosMock: MockAdapter;
|
|
|
|
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 <mark>...</mark> 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;
|
|
};
|
|
|
|
describe('<SearchUI />', () => {
|
|
beforeEach(async () => {
|
|
initializeMockApp({
|
|
authenticatedUser: {
|
|
userId: 3,
|
|
username: 'abc123',
|
|
administrator: true,
|
|
roles: [],
|
|
},
|
|
});
|
|
store = initializeStore();
|
|
// 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',
|
|
});
|
|
|
|
// The Meilisearch client-side API uses fetch, not Axios.
|
|
fetchMock.post(searchEndpoint, (_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 Instantsearch will update the UI and change the query,
|
|
// leading to unexpected results in the test cases.
|
|
mockResult.results[0].query = query;
|
|
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
|
|
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
|
mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
|
return mockResult;
|
|
});
|
|
fetchMock.post(tagsKeywordSearchEndpoint, mockTagsKeywordSearchResult);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
fetchMock.mockReset();
|
|
});
|
|
|
|
it('should render an empty state', async () => {
|
|
const { getByText } = render(<Wrap><SearchUI {...defaults} /></Wrap>);
|
|
// Before the results have even loaded, we see this message:
|
|
expect(getByText('Start searching to find content')).toBeInTheDocument();
|
|
// When this UI loads, we do a "placeholder" search to load the filter options
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
|
|
// And that message is still displayed even after the initial results/filters have loaded:
|
|
expect(getByText('Start searching to find content')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render an empty state if no result found', async () => {
|
|
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
|
|
const { getByText, getByRole } = render(<Wrap><SearchUI {...defaults} /></Wrap>);
|
|
// Return an empty result set:
|
|
// Before the results have even loaded, we see this message:
|
|
expect(getByText('Start searching to find content')).toBeInTheDocument();
|
|
// When this UI loads, the UI makes a search, to get the available "block type" facet values.
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
|
|
// And that message is still displayed even after the initial results/filters have loaded:
|
|
expect(getByText('Start searching to find content')).toBeInTheDocument();
|
|
// Enter a keyword - search for 'noresults':
|
|
fireEvent.change(getByRole('searchbox'), { target: { value: 'noresults' } });
|
|
// Wait for the new search request to load all the results:
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
|
|
expect(getByText('We didn\'t find anything matching your search')).toBeInTheDocument();
|
|
});
|
|
|
|
it('defaults to searching "All Courses" if used outside of any particular course', async () => {
|
|
const { getByText, queryByText, getByRole } = render(<Wrap><SearchUI {...defaults} courseId="" /></Wrap>);
|
|
// We default to searching all courses:
|
|
expect(getByText('All courses')).toBeInTheDocument();
|
|
expect(queryByText('This course')).toBeNull();
|
|
// Wait for the initial search request that loads all the filter options:
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
|
|
// Enter a keyword - search for 'giraffe':
|
|
fireEvent.change(getByRole('searchbox'), { target: { value: 'giraffe' } });
|
|
// Wait for the new search request to load all the results:
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
|
|
// Now we should see the results:
|
|
expect(queryByText('Enter a keyword')).toBeNull();
|
|
// The result:
|
|
expect(getByText('6 results found')).toBeInTheDocument();
|
|
expect(getByText(mockResultDisplayName)).toBeInTheDocument();
|
|
// Breadcrumbs showing where the result came from:
|
|
expect(getByText('TheCourse / Section 2 / Subsection 3 / The Little Unit That Could')).toBeInTheDocument();
|
|
});
|
|
|
|
it('defaults to searching "This Course" if used in a course', async () => {
|
|
const { getByText, queryByText, getByRole } = render(<Wrap><SearchUI {...defaults} /></Wrap>);
|
|
// We default to searching all courses:
|
|
expect(getByText('This course')).toBeInTheDocument();
|
|
expect(queryByText('All courses')).toBeNull();
|
|
// Wait for the initial search request that loads all the filter options:
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
|
|
// Enter a keyword - search for 'giraffe':
|
|
fireEvent.change(getByRole('searchbox'), { target: { value: 'giraffe' } });
|
|
// Wait for the new search request to load all the results:
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
|
|
// And make sure the request was limited to this course:
|
|
expect(fetchMock).toHaveLastFetched((_url, req) => {
|
|
const requestData = JSON.parse(req.body?.toString() ?? '');
|
|
const requestedFilter = requestData?.queries[0].filter;
|
|
return requestedFilter?.[1] === 'type = "course_block"'
|
|
&& requestedFilter?.[2] === 'context_key = "course-v1:org+test+123"';
|
|
});
|
|
// Now we should see the results:
|
|
expect(queryByText('Enter a keyword')).toBeNull();
|
|
// The result:
|
|
expect(getByText('6 results found')).toBeInTheDocument();
|
|
expect(getByText(mockResultDisplayName)).toBeInTheDocument();
|
|
// Breadcrumbs showing where the result came from:
|
|
expect(getByText('TheCourse / Section 2 / Subsection 3 / The Little Unit That Could')).toBeInTheDocument();
|
|
});
|
|
|
|
describe('results', () => {
|
|
let rendered: RenderResult;
|
|
beforeEach(async () => {
|
|
rendered = render(<Wrap><SearchUI {...defaults} /></Wrap>);
|
|
const { getByRole } = rendered;
|
|
fireEvent.change(getByRole('searchbox'), { target: { value: 'giraffe' } });
|
|
});
|
|
|
|
test('click section result navigates to the context', async () => {
|
|
const { findAllByRole } = rendered;
|
|
|
|
const [resultItem] = await findAllByRole('button', { name: /Section 1/ });
|
|
|
|
// Clicking the "Open in new window" button should open the result in a new window:
|
|
const { open } = window;
|
|
window.open = jest.fn();
|
|
fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
|
|
expect(window.open).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1'
|
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40chapter%2Bblock%40c7077c8cafcf420dbc0b440bf27bad04',
|
|
'_blank',
|
|
);
|
|
window.open = open;
|
|
|
|
// Clicking in the result should navigate to the result's URL:
|
|
fireEvent.click(resultItem);
|
|
expect(mockNavigate).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1'
|
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40chapter%2Bblock%40c7077c8cafcf420dbc0b440bf27bad04',
|
|
);
|
|
});
|
|
|
|
test('click subsection result navigates to the context', async () => {
|
|
const { findAllByRole } = rendered;
|
|
|
|
const [resultItem] = await findAllByRole('button', { name: /Subsection 1.1/ });
|
|
|
|
// Clicking the "Open in new window" button should open the result in a new window:
|
|
const { open } = window;
|
|
window.open = jest.fn();
|
|
fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
|
|
expect(window.open).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1'
|
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40sequential%2Bblock%4092e3e9ca156c44fa8a735f0e9e7c854f',
|
|
'_blank',
|
|
);
|
|
window.open = open;
|
|
|
|
// Clicking in the result should navigate to the result's URL:
|
|
fireEvent.click(resultItem);
|
|
expect(mockNavigate).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1'
|
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40sequential%2Bblock%4092e3e9ca156c44fa8a735f0e9e7c854f',
|
|
);
|
|
});
|
|
|
|
test('click unit result navigates to the context', async () => {
|
|
const { findAllByRole } = rendered;
|
|
|
|
const [resultItem] = await findAllByRole('button', { name: /Unit 1.1.1/ });
|
|
|
|
// Clicking the "Open in new window" button should open the result in a new window:
|
|
const { open } = window;
|
|
window.open = jest.fn();
|
|
fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
|
|
expect(window.open).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b',
|
|
'_blank',
|
|
);
|
|
window.open = open;
|
|
|
|
// Clicking in the result should navigate to the result's URL:
|
|
fireEvent.click(resultItem);
|
|
expect(mockNavigate).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b',
|
|
);
|
|
});
|
|
|
|
test('click unit component result navigates to the context', async () => {
|
|
const { findAllByRole } = rendered;
|
|
|
|
const [resultItem] = await findAllByRole('button', { name: /Announcement/ });
|
|
|
|
// Clicking the "Open in new window" button should open the result in a new window:
|
|
const { open } = window;
|
|
window.open = jest.fn();
|
|
fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
|
|
expect(window.open).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'
|
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40html%2Bblock%400b2d1c0722f742489602b6d8645205f4',
|
|
'_blank',
|
|
);
|
|
window.open = open;
|
|
|
|
// Clicking in the result should navigate to the result's URL:
|
|
fireEvent.click(resultItem);
|
|
expect(mockNavigate).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'
|
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40html%2Bblock%400b2d1c0722f742489602b6d8645205f4',
|
|
);
|
|
});
|
|
|
|
test('click lib component in unit result navigates to the context of encompassing lib component', async () => {
|
|
const { findAllByRole } = rendered;
|
|
|
|
const [resultItem] = await findAllByRole('button', { name: /Text block in Lib Component/ });
|
|
|
|
// Clicking the "Open in new window" button should open the result in a new window:
|
|
const { open } = window;
|
|
window.open = jest.fn();
|
|
fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
|
|
|
|
expect(window.open).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'
|
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40library_content%2Bblock%40427e5cd03fbe431d9d551c67d4e280ae',
|
|
'_blank',
|
|
);
|
|
window.open = open;
|
|
|
|
// Clicking in the result should navigate to the result's URL:
|
|
fireEvent.click(resultItem);
|
|
expect(mockNavigate).toHaveBeenCalledWith(
|
|
'/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'
|
|
+ '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40library_content%2Bblock%40427e5cd03fbe431d9d551c67d4e280ae',
|
|
);
|
|
});
|
|
|
|
test('click lib component result navigates to course-authoring/library', async () => {
|
|
const { findByRole } = rendered;
|
|
|
|
const resultItem = await findByRole('button', { name: /Library Content/ });
|
|
|
|
// Clicking the "Open in new window" button should open the result in a new window:
|
|
const { open } = window;
|
|
window.open = jest.fn();
|
|
fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
|
|
|
|
expect(window.open).toHaveBeenCalledWith(
|
|
'/library/lib:org1:libafter1',
|
|
'_blank',
|
|
);
|
|
window.open = open;
|
|
|
|
// Clicking in the result should navigate to the result's URL:
|
|
fireEvent.click(resultItem);
|
|
expect(mockNavigate).toHaveBeenCalledWith(
|
|
'/library/lib:org1:libafter1',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('filters', () => {
|
|
let rendered: RenderResult;
|
|
beforeEach(async () => {
|
|
fetchMock.post(facetSearchEndpoint, (_path, req) => {
|
|
const requestData = JSON.parse(req.body?.toString() ?? '');
|
|
switch (requestData.facetName) {
|
|
case 'tags.taxonomy': return mockTagsFacetResult;
|
|
case 'tags.level0': return mockTagsFacetResultLevel0;
|
|
case 'tags.level1': return mockTagsFacetResultLevel1;
|
|
default: throw new Error(`Facet ${requestData.facetName} not mocked for testing`);
|
|
}
|
|
});
|
|
|
|
rendered = render(<Wrap><SearchUI {...defaults} /></Wrap>);
|
|
const { getByRole, getByText } = rendered;
|
|
// Wait for the initial search request that loads all the filter options:
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
|
|
// Enter a keyword - search for 'giraffe':
|
|
fireEvent.change(getByRole('searchbox'), { target: { value: 'giraffe' } });
|
|
// Wait for the new search request to load all the results and the filter options, based on the search so far:
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
|
|
// And make sure the request was limited to this course:
|
|
expect(fetchMock).toHaveLastFetched((_url, req) => {
|
|
const requestData = JSON.parse(req.body?.toString() ?? '');
|
|
const requestedFilter = requestData?.queries[0].filter;
|
|
// the filter is:
|
|
// ['', 'type = "course_block"', 'context_key = "course-v1:org+test+123"']
|
|
return (requestedFilter?.length === 3);
|
|
});
|
|
// Now we should see the results:
|
|
expect(getByText('6 results found')).toBeInTheDocument();
|
|
expect(getByText(mockResultDisplayName)).toBeInTheDocument();
|
|
});
|
|
|
|
it('can filter results by component/XBlock type', async () => {
|
|
const { getByRole, getByText } = rendered;
|
|
// Now open the filters menu:
|
|
fireEvent.click(getByRole('button', { name: 'Type' }), {});
|
|
// The dropdown menu has role="group"
|
|
await waitFor(() => { expect(getByRole('group')).toBeInTheDocument(); });
|
|
const problemFilterCheckbox = getByText(/Problem/i);
|
|
fireEvent.click(problemFilterCheckbox, {});
|
|
await waitFor(() => {
|
|
expect(rendered.getByRole('button', { name: /type: problem/i, hidden: true })).toBeInTheDocument();
|
|
});
|
|
// Now wait for the filter to be applied and the new results to be fetched.
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(3, searchEndpoint, 'post'); });
|
|
// Because we're mocking the results, there's no actual changes to the mock results,
|
|
// but we can verify that the filter was sent in the request
|
|
expect(fetchMock).toHaveLastFetched((_url, req) => {
|
|
const requestData = JSON.parse(req.body?.toString() ?? '');
|
|
const requestedFilter = requestData?.queries[0].filter;
|
|
return JSON.stringify(requestedFilter) === JSON.stringify([
|
|
[
|
|
'block_type = problem',
|
|
'content.problem_types = choiceresponse',
|
|
'content.problem_types = multiplechoiceresponse',
|
|
'content.problem_types = numericalresponse',
|
|
'content.problem_types = optionresponse',
|
|
'content.problem_types = stringresponse',
|
|
],
|
|
'type = "course_block"',
|
|
'context_key = "course-v1:org+test+123"',
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('can filter results by tag', async () => {
|
|
const { getByRole, getByLabelText } = rendered;
|
|
// Now open the filters menu:
|
|
fireEvent.click(getByRole('button', { name: 'Tags' }), {});
|
|
// The dropdown menu in this case doesn't have a role; let's just assume it's displayed.
|
|
const checkboxLabel = /^ESDC Skills and Competencies/i;
|
|
await waitFor(() => { expect(getByLabelText(checkboxLabel)).toBeInTheDocument(); });
|
|
// In addition to the checkbox, there is another button to show the child tags:
|
|
expect(getByLabelText(/Expand to show child tags of "ESDC Skills and Competencies"/i)).toBeInTheDocument();
|
|
const competentciesCheckbox = getByLabelText(checkboxLabel);
|
|
fireEvent.click(competentciesCheckbox, {});
|
|
// Now wait for the filter to be applied and the new results to be fetched.
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(3, searchEndpoint, 'post'); });
|
|
// Because we're mocking the results, there's no actual changes to the mock results,
|
|
// but we can verify that the filter was sent in the request
|
|
expect(fetchMock).toBeDone((_url, req) => {
|
|
const requestData = JSON.parse(req.body?.toString() ?? '');
|
|
const requestedFilter = requestData?.queries?.[0]?.filter;
|
|
return JSON.stringify(requestedFilter) === JSON.stringify([
|
|
[],
|
|
'type = "course_block"',
|
|
'context_key = "course-v1:org+test+123"',
|
|
'tags.taxonomy = "ESDC Skills and Competencies"', // <-- the newly added filter, sent with the request
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('can filter results by a child tag', async () => {
|
|
const { getByRole, getByLabelText, queryByLabelText } = rendered;
|
|
// Now open the filters menu:
|
|
fireEvent.click(getByRole('button', { name: 'Tags' }), {});
|
|
// The dropdown menu in this case doesn't have a role; let's just assume it's displayed.
|
|
const expandButtonLabel = /Expand to show child tags of "ESDC Skills and Competencies"/i;
|
|
await waitFor(() => { expect(getByLabelText(expandButtonLabel)).toBeInTheDocument(); });
|
|
|
|
// First, the child tag is not shown:
|
|
const childTagLabel = /^Abilities/i;
|
|
expect(queryByLabelText(childTagLabel)).toBeNull();
|
|
// Click on the button to show children
|
|
const expandButton = getByLabelText(expandButtonLabel);
|
|
fireEvent.click(expandButton, {});
|
|
// Now the child tag is visible:
|
|
await waitFor(() => { expect(queryByLabelText(childTagLabel)).toBeInTheDocument(); });
|
|
// Click on it:
|
|
const abilitiesTagFilterCheckbox = getByLabelText(childTagLabel);
|
|
fireEvent.click(abilitiesTagFilterCheckbox);
|
|
// Now wait for the filter to be applied and the new results to be fetched.
|
|
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(3, searchEndpoint, 'post'); });
|
|
// Because we're mocking the results, there's no actual changes to the mock results,
|
|
// but we can verify that the filter was sent in the request
|
|
expect(fetchMock).toBeDone((_url, req) => {
|
|
const requestData = JSON.parse(req.body?.toString() ?? '');
|
|
const requestedFilter = requestData?.queries?.[0]?.filter;
|
|
return JSON.stringify(requestedFilter) === JSON.stringify([
|
|
[],
|
|
'type = "course_block"',
|
|
'context_key = "course-v1:org+test+123"',
|
|
'tags.level0 = "ESDC Skills and Competencies > Abilities"',
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('can do a keyword search of the tag options', async () => {
|
|
const { getByRole, getByLabelText, queryByLabelText } = rendered;
|
|
// Now open the filters menu:
|
|
fireEvent.click(getByRole('button', { name: 'Tags' }), {});
|
|
// The dropdown menu in this case doesn't have a role; let's just assume it's displayed.
|
|
const expandButtonLabel = /Expand to show child tags of "ESDC Skills and Competencies"/i;
|
|
await waitFor(() => { expect(getByLabelText(expandButtonLabel)).toBeInTheDocument(); });
|
|
|
|
const input = getByLabelText('Search tags');
|
|
fireEvent.change(input, { target: { value: 'Lightcast' } });
|
|
|
|
await waitFor(() => { expect(queryByLabelText(/^ESDC Skills and Competencies/i)).toBeNull(); });
|
|
expect(queryByLabelText(/^Lightcast/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|