diff --git a/CHANGELOG.md b/CHANGELOG.md index 0563471..16a93aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Change Log ## [Unreleased] +- feat(atomWithLocation): support location.hash ## [0.5.4] - 2024-02-27 ### Changed diff --git a/__tests__/atomWithLocation_spec.tsx b/__tests__/atomWithLocation_spec.tsx index 3a26cb4..05864ab 100644 --- a/__tests__/atomWithLocation_spec.tsx +++ b/__tests__/atomWithLocation_spec.tsx @@ -12,41 +12,125 @@ function assertPathNameAndHistoryLength( expect(window.history.length).toEqual(expectedHistoryLength); } +function assertHashAndHistoryLength( + expectedHash: string, + expectedHistoryLength: number, +) { + expect(window.location.hash).toEqual(expectedHash); + expect(window.history.length).toEqual(expectedHistoryLength); +} + +function assertSearchParamAndHistoryLength( + expectedSearchParams: URLSearchParams, + expectedHistoryLength: number, +) { + expect(window.location.search).toEqual(`?${expectedSearchParams.toString()}`); + expect(window.history.length).toEqual(expectedHistoryLength); +} + async function assertStartState( startTestHistoryLength: number, findByText: any, + assert: 'pathname' | 'hash' | 'searchParams' | 'all', ) { - await findByText('current pathname in atomWithLocation: /'); - assertPathNameAndHistoryLength('/', startTestHistoryLength); + if (assert === 'pathname' || assert === 'all') { + await findByText('current pathname in atomWithLocation: /'); + assertPathNameAndHistoryLength('/', startTestHistoryLength); + } + if (assert === 'hash' || assert === 'all') { + await findByText('current hash in atomWithLocation: #'); + assertHashAndHistoryLength('', startTestHistoryLength); + } + if (assert === 'searchParams' || assert === 'all') { + await findByText('current searchParams in atomWithLocation:'); + expect(window.location.search).toEqual(''); + expect(window.history.length).toEqual(startTestHistoryLength); + } } function clickButtonAndAssertTemplate(localFindByText: any) { return async function clickButtonAndAssert( target: `button${number}` | 'back' | 'buttonWithReplace' | 'buttonWithPush', historyLength: number, - targetPathName?: string, + { + targetPathName, + targetHash, + targetSearchParams, + }: { + targetPathName?: string; + targetHash?: string; + targetSearchParams?: string; + } = {}, + assert: 'pathname' | 'hash' | 'search' | 'all' = 'pathname', ) { let expectedPathname: string = '/'; + let expectedHash: string = ''; + let expectedSearchParams = new URLSearchParams(); if (target === 'buttonWithReplace') { expectedPathname = '/123'; + expectedHash = '#tab=1'; + expectedSearchParams.set('tab', '1'); } else if (target === 'buttonWithPush') { expectedPathname = '/234'; + expectedHash = '#tab=2'; + expectedSearchParams.set('tab', '2'); } else if (target.startsWith('button')) { expectedPathname = `/${target.slice(-1)}`; - } else if (target === 'back' && targetPathName) { - expectedPathname = targetPathName; + expectedHash = `#tab=${target.slice(-1)}`; + expectedSearchParams.set('tab', target.slice(-1)); + } else if (target === 'back') { + expectedPathname = targetPathName ?? ''; + expectedHash = `#${targetHash ?? ''}`; + expectedSearchParams = new URLSearchParams(); + expectedSearchParams.set('tab', targetSearchParams ?? ''); } await userEvent.click(await localFindByText(target)); - await localFindByText( - `current pathname in atomWithLocation: ${expectedPathname}`, - ); - assertPathNameAndHistoryLength(expectedPathname, historyLength); + if (assert === 'pathname') { + await localFindByText( + `current pathname in atomWithLocation: ${expectedPathname}`, + ); + assertPathNameAndHistoryLength(expectedPathname, historyLength); + } + if (assert === 'hash') { + await localFindByText( + `current hash in atomWithLocation: ${expectedHash}`, + ); + assertHashAndHistoryLength( + expectedHash === '#' ? '' : expectedHash, + historyLength, + ); + } + if (assert === 'search') { + await localFindByText(`${expectedSearchParams.toString()}`); + assertSearchParamAndHistoryLength(expectedSearchParams, historyLength); + } + if (assert === 'all') { + await localFindByText( + `current pathname in atomWithLocation: ${expectedPathname}`, + ); + assertPathNameAndHistoryLength(expectedPathname, historyLength); + await localFindByText( + `current hash in atomWithLocation: ${expectedHash}`, + ); + assertHashAndHistoryLength(expectedHash, historyLength); + await localFindByText( + `current searchParams in atomWithLocation: ${expectedSearchParams}`, + ); + assertSearchParamAndHistoryLength(expectedSearchParams, historyLength); + } }; } -describe('atomWithLocation', () => { +const defaultLocation = { + pathname: '/', + search: '', + hash: '', + state: null, +}; + +describe('atomWithLocation, pathName', () => { beforeEach(() => { - window.history.pushState(null, '', '/'); + resetWindow(); }); it('can replace state', async () => { @@ -73,6 +157,12 @@ describe('atomWithLocation', () => { > button2 + > ); }; @@ -85,10 +175,12 @@ describe('atomWithLocation', () => { const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); const startHistoryLength = window.history.length; - assertStartState(startHistoryLength, findByText); + await assertStartState(startHistoryLength, findByText, 'pathname'); await clickButtonAndAssert('button1', startHistoryLength); await clickButtonAndAssert('button2', startHistoryLength); + + await userEvent.click(await findByText('reset-button')); }); it('can push state', async () => { @@ -115,6 +207,12 @@ describe('atomWithLocation', () => { > button2 + > ); }; @@ -127,11 +225,15 @@ describe('atomWithLocation', () => { const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); const startHistoryLength = window.history.length; - assertStartState(startHistoryLength, findByText); + assertStartState(startHistoryLength, findByText, 'pathname'); await clickButtonAndAssert('button1', startHistoryLength + 1); await clickButtonAndAssert('button2', startHistoryLength + 2); - await clickButtonAndAssert('back', startHistoryLength + 2, '/1'); + await clickButtonAndAssert('back', startHistoryLength + 2, { + targetPathName: '/1', + }); + + await userEvent.click(await findByText('reset-button')); }); it('can override atomOptions, from replace=false to replace=true', async () => { @@ -164,6 +266,12 @@ describe('atomWithLocation', () => { > buttonWithReplace + > ); }; @@ -177,11 +285,13 @@ describe('atomWithLocation', () => { const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); const startHistoryLength = window.history.length; - assertStartState(startHistoryLength, findByText); + assertStartState(startHistoryLength, findByText, 'pathname'); await clickButtonAndAssert('buttonWithPush', startHistoryLength + 1); await clickButtonAndAssert('buttonWithReplace', startHistoryLength + 1); - await clickButtonAndAssert('back', startHistoryLength + 1, '/'); + await clickButtonAndAssert('back', startHistoryLength + 1, { + targetPathName: '/', + }); // This click overwrites the history entry we // went back from. The history length remains the same. @@ -189,6 +299,8 @@ describe('atomWithLocation', () => { // The second click adds a new history entry, which now increments the history length. await clickButtonAndAssert('buttonWithPush', startHistoryLength + 2); + + await userEvent.click(await findByText('reset-button')); }); it('can override atomOptions, from replace=true to replace=false', async () => { @@ -221,6 +333,12 @@ describe('atomWithLocation', () => { > buttonWithPush + > ); }; @@ -233,14 +351,300 @@ describe('atomWithLocation', () => { const clickButtonAndAssert = clickButtonAndAssertTemplate(findByText); const startTestHistoryLength = window.history.length; - assertStartState(startTestHistoryLength, findByText); + assertStartState(startTestHistoryLength, findByText, 'pathname'); await clickButtonAndAssert('buttonWithReplace', startTestHistoryLength); await clickButtonAndAssert('buttonWithPush', startTestHistoryLength + 1); - await clickButtonAndAssert('back', startTestHistoryLength + 1, '/123'); + await clickButtonAndAssert('back', startTestHistoryLength + 1, { + targetPathName: '/123', + }); await clickButtonAndAssert('buttonWithReplace', startTestHistoryLength + 1); await clickButtonAndAssert('buttonWithPush', startTestHistoryLength + 1); await clickButtonAndAssert('buttonWithPush', startTestHistoryLength + 2); + + await userEvent.click(await findByText('reset-button')); + }); +}); + +describe('atomWithLocation, hash', () => { + beforeEach(() => { + resetWindow(); + }); + + it('can push state with hash', async () => { + const locationAtom = atomWithLocation({ replace: false }); + + const Navigation = () => { + const [location, setLocation] = useAtom(locationAtom); + return ( + <> +