diff --git a/package-lock.json b/package-lock.json index 961982605..b4dc57cb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9645,9 +9645,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001515", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz", - "integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==", + "version": "1.0.30001519", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", + "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==", "funding": [ { "type": "opencollective", @@ -30731,9 +30731,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001515", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz", - "integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==" + "version": "1.0.30001519", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", + "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==" }, "capital-case": { "version": "1.0.4", diff --git a/src/components/organisms/searchResults.tsx b/src/components/organisms/searchResults.tsx index 8df487ef6..77cae6690 100644 --- a/src/components/organisms/searchResults.tsx +++ b/src/components/organisms/searchResults.tsx @@ -4,7 +4,6 @@ import { FormattedMessage, useIntl } from 'react-intl'; import LineHeading from '~components/atoms/lineHeading'; import Button from '~components/molecules/button'; -import { CardRecordingFragment } from '~components/molecules/card/__generated__/recording'; import CardInferred, { InferrableEntity, } from '~components/molecules/card/inferred'; @@ -110,14 +109,16 @@ function Section({ const normalize = (s: string) => s.replace(/[\p{P}\p{S}\s]/gu, '').toLowerCase(); -const isRecording = (e: InferrableEntity): e is CardRecordingFragment => - e.__typename === 'Recording'; +const getTitle = (e: InferrableEntity) => + e.__typename === 'Person' ? e.name : e.title; -function hasExactMatch(term: string, sections: AugmentedFilter[]): boolean { - return !!sections - .find((s) => s.id === 'teachings') - ?.nodes.slice(0, 3) - .find((e) => isRecording(e) && normalize(e.title) === normalize(term)); +function sortSections(term: string, sections: AugmentedFilter[]) { + const t = normalize(term); + const a = sections.filter((s) => + s.nodes.slice(0, 3).map(getTitle).map(normalize).includes(t) + ); + const b = sections.filter((s) => !a.find((hs) => hs.id === s.id)); + return [...a, ...b]; } export default function Search({ @@ -134,8 +135,7 @@ export default function Search({ const { visible, loadMore, isLoading } = useSearch(entityType, t); const endRef = useRef(null); const endReached = useOnScreen(endRef); - const hasExactTeaching = hasExactMatch(t, visible); - const shouldHoistTeachings = hasExactTeaching && entityType === 'all'; + const sections = sortSections(t, visible); useEffect(() => { if (entityType !== 'all' && endReached && !isLoading) loadMore(); @@ -149,29 +149,14 @@ export default function Search({ ) : ( <> - {shouldHoistTeachings && ( + {sections.map((s) => (
s.id === 'teachings') as AugmentedFilter - } + key={s.id} + section={s} entityType={entityType} onEntityTypeChange={onEntityTypeChange} /> - )} - {visible.map((s) => { - if (s.id === 'teachings' && shouldHoistTeachings) { - return null; - } - return ( -
- ); - })} + ))}
)} diff --git a/src/components/templates/andNavigation.spec.tsx b/src/components/templates/andNavigation.spec.tsx index 8e1151148..ccd1a5454 100644 --- a/src/components/templates/andNavigation.spec.tsx +++ b/src/components/templates/andNavigation.spec.tsx @@ -5,20 +5,25 @@ import { CardPersonFragment } from '~components/molecules/card/__generated__/per import { CardRecordingFragment } from '~components/molecules/card/__generated__/recording'; import { GetSearchAudiobooksDocument, - GetSearchPersonsDocument, GetSearchRecordingsDocument, } from '~components/organisms/__generated__/searchResults'; import { fetchApi } from '~lib/api/fetchApi'; +import { SequenceContentType } from '~src/__generated__/graphql'; +import { + buildGetSearchPersonsLoader, + buildGetSearchRecordingsLoader, + buildGetSearchSeriesLoader, +} from '~src/__generated__/loaders'; -import { buildLoader } from '../../lib/test/buildLoader'; import { buildRenderer } from '../../lib/test/buildRenderer'; +import { CardSequenceFragment } from '../molecules/card/__generated__/sequence'; import AndNavigation from './andNavigation'; const renderTemplate = buildRenderer(AndNavigation); const teaching: CardRecordingFragment = { __typename: 'Recording', - title: 'test', + title: 'the_teaching_title', canonicalPath: '', sequenceIndex: null, id: '', @@ -36,7 +41,7 @@ const teaching: CardRecordingFragment = { const person: CardPersonFragment = { __typename: 'Person', - name: 'test', + name: 'the_person_title', recordings: { aggregate: { count: 0, @@ -47,7 +52,30 @@ const person: CardPersonFragment = { image: null, }; -const loadTeachings = buildLoader(GetSearchRecordingsDocument, { +const series: CardSequenceFragment = { + __typename: 'Sequence', + title: 'the_series_title', + canonicalPath: '', + id: '', + contentType: SequenceContentType.Series, + duration: 0, + summary: '', + speakers: { + nodes: [], + }, + sequenceWriters: { + nodes: [], + }, + allRecordings: { + aggregate: { + count: 0, + }, + nodes: [], + }, + collection: null, +}; + +const loadTeachings = buildGetSearchRecordingsLoader({ sermons: { aggregate: { count: 1, @@ -59,7 +87,8 @@ const loadTeachings = buildLoader(GetSearchRecordingsDocument, { }, }, }); -const loadPresenters = buildLoader(GetSearchPersonsDocument, { + +const loadPresenters = buildGetSearchPersonsLoader({ persons: { aggregate: { count: 1, @@ -72,10 +101,24 @@ const loadPresenters = buildLoader(GetSearchPersonsDocument, { }, }); +const loadSeries = buildGetSearchSeriesLoader({ + serieses: { + aggregate: { + count: 1, + }, + nodes: [series], + pageInfo: { + hasNextPage: false, + endCursor: null, + }, + }, +}); + describe('AndNavigation', () => { beforeEach(() => { loadTeachings(); loadPresenters(); + loadSeries(); }); it('hoists teachings on exact match', async () => { @@ -84,7 +127,7 @@ describe('AndNavigation', () => { const searchInputs = screen.getAllByPlaceholderText('Search'); const search = searchInputs[0]; - await userEvent.type(search, 'test'); + await userEvent.type(search, 'the_teaching_title'); const teachingsHeading = await screen.findByRole('heading', { name: 'Teachings', @@ -102,7 +145,7 @@ describe('AndNavigation', () => { const searchInputs = screen.getAllByPlaceholderText('Search'); const search = searchInputs[0]; - await userEvent.type(search, 'Test'); + await userEvent.type(search, 'The_Teaching_Title'); const teachingsHeading = await screen.findByRole('heading', { name: 'Teachings', @@ -120,7 +163,7 @@ describe('AndNavigation', () => { const searchInputs = screen.getAllByPlaceholderText('Search'); const search = searchInputs[0]; - await userEvent.type(search, 'test!'); + await userEvent.type(search, 'the_teaching_title!'); const teachingsHeading = await screen.findByRole('heading', { name: 'Teachings', @@ -212,4 +255,64 @@ describe('AndNavigation', () => { }) ); }); + + it('hoists series on exact match', async () => { + await renderTemplate(); + + const searchInputs = screen.getAllByPlaceholderText('Search'); + const search = searchInputs[0]; + + await userEvent.type(search, 'the_series_title'); + + const seriesHeading = ( + await screen.findAllByRole('heading', { + name: 'Series', + }) + )[0]; + const presentersHeading = await screen.findByRole('heading', { + name: 'Presenters', + }); + + expect(seriesHeading).toAppearBefore(presentersHeading); + }); + + it('only checks first three entities in section when hoisting', async () => { + loadTeachings({ + sermons: { + aggregate: { + count: 4, + }, + nodes: [ + { ...teaching, id: '1' }, + { ...teaching, id: '2' }, + { ...teaching, id: '3' }, + { + ...teaching, + id: '4', + title: 'the_teaching_title_2', + }, + ], + pageInfo: { + hasNextPage: false, + endCursor: null, + }, + }, + }); + + await renderTemplate(); + + const searchInputs = screen.getAllByPlaceholderText('Search'); + const search = searchInputs[0]; + + await userEvent.type(search, 'the_teaching_title_2'); + + const teachingsHeading = await screen.findByRole('heading', { + name: 'Teachings', + }); + const presentersHeading = await screen.findByRole('heading', { + name: 'Presenters', + }); + + expect(presentersHeading).toAppearBefore(teachingsHeading); + }); }); diff --git a/src/containers/search/index.spec.tsx b/src/containers/search/index.spec.tsx index 9391d97da..1d5ddd461 100644 --- a/src/containers/search/index.spec.tsx +++ b/src/containers/search/index.spec.tsx @@ -1,31 +1,22 @@ import { screen, waitFor } from '@testing-library/react'; import { __loadQuery } from 'next/router'; -import { - GetSearchAudiobooksDocument, - GetSearchAudiobooksQuery, - GetSearchConferencesDocument, - GetSearchConferencesQuery, - GetSearchMusicTracksDocument, - GetSearchMusicTracksQuery, - GetSearchPersonsDocument, - GetSearchPersonsQuery, - GetSearchRecordingsDocument, - GetSearchRecordingsQuery, - GetSearchSeriesDocument, - GetSearchSeriesQuery, - GetSearchSponsorsDocument, - GetSearchSponsorsQuery, - GetSearchStoryProgramsDocument, - GetSearchStoryProgramsQuery, -} from '~components/organisms/__generated__/searchResults'; -import { buildLoader } from '~lib/test/buildLoader'; import { buildRenderer } from '~lib/test/buildRenderer'; import Search, { getStaticPaths, getStaticProps, } from '~pages/[language]/search'; import { RecordingContentType } from '~src/__generated__/graphql'; +import { + buildGetSearchAudiobooksLoader, + buildGetSearchConferencesLoader, + buildGetSearchMusicTracksLoader, + buildGetSearchPersonsLoader, + buildGetSearchRecordingsLoader, + buildGetSearchSeriesLoader, + buildGetSearchSponsorsLoader, + buildGetSearchStoryProgramsLoader, +} from '~src/__generated__/loaders'; jest.mock('next/head'); @@ -47,58 +38,37 @@ const empty = { }, }; -const loadRecordings = buildLoader( - GetSearchRecordingsDocument, - { - sermons: empty, - } -); +const loadRecordings = buildGetSearchRecordingsLoader({ + sermons: empty, +}); -const loadSeries = buildLoader(GetSearchSeriesDocument, { +const loadSeries = buildGetSearchSeriesLoader({ serieses: empty, }); -const loadConferences = buildLoader( - GetSearchConferencesDocument, - { - conferences: empty, - } -); - -const loadSponsors = buildLoader( - GetSearchSponsorsDocument, - { - sponsors: empty, - } -); - -const loadPersons = buildLoader( - GetSearchPersonsDocument, - { - persons: empty, - } -); - -const loadAudiobooks = buildLoader( - GetSearchAudiobooksDocument, - { - audiobooks: empty, - } -); - -const loadMusicTracks = buildLoader( - GetSearchMusicTracksDocument, - { - musicTracks: empty, - } -); - -const loadStoryPrograms = buildLoader( - GetSearchStoryProgramsDocument, - { - storyPrograms: empty, - } -); +const loadConferences = buildGetSearchConferencesLoader({ + conferences: empty, +}); + +const loadSponsors = buildGetSearchSponsorsLoader({ + sponsors: empty, +}); + +const loadPersons = buildGetSearchPersonsLoader({ + persons: empty, +}); + +const loadAudiobooks = buildGetSearchAudiobooksLoader({ + audiobooks: empty, +}); + +const loadMusicTracks = buildGetSearchMusicTracksLoader({ + musicTracks: empty, +}); + +const loadStoryPrograms = buildGetSearchStoryProgramsLoader({ + storyPrograms: empty, +}); describe('search', () => { beforeEach(() => {