diff --git a/newswires/client/src/context/SearchContext.test.tsx b/newswires/client/src/context/SearchContext.test.tsx index fb427ff..099adc4 100644 --- a/newswires/client/src/context/SearchContext.test.tsx +++ b/newswires/client/src/context/SearchContext.test.tsx @@ -41,6 +41,7 @@ describe('SearchContext', () => { beforeEach(() => { jest.clearAllMocks(); + localStorage.clear(); }); it('should fetch data and initialise the state', async () => { @@ -94,4 +95,68 @@ describe('SearchContext', () => { expect(global.fetch).toHaveBeenCalledTimes(2); }); + + it('should add item ids to the view history on item navigation', async () => { + const contextRef = await renderWithContext(); + + if (!contextRef.current) { + throw new Error('Context ref was null after render.'); + } + + expect(contextRef.current.viewedItemIds).toEqual([]); + + act(() => { + contextRef.current?.handleSelectItem('111'); + }); + + expect(contextRef.current.viewedItemIds).toEqual(['111']); + }); + + it('should store the view history in local storage', async () => { + const contextRef = await renderWithContext(); + + if (!contextRef.current) { + throw new Error('Context ref was null after render.'); + } + + expect(contextRef.current.viewedItemIds).toEqual([]); + + act(() => { + contextRef.current?.handleSelectItem('111'); + }); + + expect(contextRef.current.viewedItemIds).toEqual(['111']); + expect(localStorage.getItem('viewedItemIds')).toEqual('["111"]'); + + // Re-render the component + const newContextRef = await renderWithContext(); + + if (!newContextRef.current) { + throw new Error('Context ref was null after render.'); + } + + expect(newContextRef.current.viewedItemIds).toEqual(['111']); + }); + + it('should deduplicate item ids in the view history', async () => { + const contextRef = await renderWithContext(); + + if (!contextRef.current) { + throw new Error('Context ref was null after render.'); + } + + expect(contextRef.current.viewedItemIds).toEqual([]); + + act(() => { + contextRef.current?.handleSelectItem('1'); + }); + act(() => { + contextRef.current?.handleSelectItem('2'); + }); + act(() => { + contextRef.current?.handleSelectItem('1'); + }); + + expect(contextRef.current.viewedItemIds).toEqual(['1', '2']); + }); }); diff --git a/newswires/client/src/context/SearchContext.tsx b/newswires/client/src/context/SearchContext.tsx index 69a0388..9bfeac0 100644 --- a/newswires/client/src/context/SearchContext.tsx +++ b/newswires/client/src/context/SearchContext.tsx @@ -17,6 +17,7 @@ import { } from '../sharedTypes.ts'; import { configToUrl, defaultConfig, urlToConfig } from '../urlState.ts'; import { fetchResults } from './fetchResults.ts'; +import { loadFromLocalStorage, saveToLocalStorage } from './localStorage.tsx'; import { SearchReducer } from './SearchReducer.ts'; const SearchHistorySchema = z.array( @@ -98,6 +99,7 @@ export type Action = z.infer; export type SearchContextShape = { config: Config; state: State; + viewedItemIds: string[]; handleEnterQuery: (query: Query) => void; handleRetry: () => void; handleSelectItem: (item: string) => void; @@ -111,9 +113,12 @@ export const SearchContext: Context = createContext(null); export function SearchContextProvider({ children }: PropsWithChildren) { - const [currentConfig, setConfig] = useState( + const [currentConfig, setConfig] = useState(() => urlToConfig(window.location), ); + const [viewedItemIds, setViewedItemIds] = useState(() => + loadFromLocalStorage('viewedItemIds', z.array(z.string()), []), + ); const [state, dispatch] = useReducer(SearchReducer, { error: undefined, @@ -137,9 +142,16 @@ export function SearchContextProvider({ children }: PropsWithChildren) { const pushConfigState = useCallback( (config: Config) => { history.pushState(config, '', configToUrl(config)); + if (config.view === 'item') { + const updatedViewedItemIds = Array.from( + new Set([config.itemId, ...viewedItemIds]), + ); + setViewedItemIds(updatedViewedItemIds); + saveToLocalStorage('viewedItemIds', updatedViewedItemIds); + } setConfig(config); }, - [setConfig], + [setConfig, viewedItemIds, setViewedItemIds], ); const popConfigStateCallback = useCallback( @@ -309,6 +321,7 @@ export function SearchContextProvider({ children }: PropsWithChildren) { handlePreviousItem, toggleAutoUpdate, loadMoreResults, + viewedItemIds, }} > {children}