-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1118 from ral-facilities/bugfix/jump-to-anchor-#1115
- Loading branch information
Showing
8 changed files
with
216 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import type { MockStoreCreator } from 'redux-mock-store'; | ||
import configureStore from 'redux-mock-store'; | ||
import type { DeepPartial } from 'redux'; | ||
import * as React from 'react'; | ||
import { mount } from 'enzyme'; | ||
import { Provider } from 'react-redux'; | ||
import { useLocation } from 'react-router'; | ||
import { createLocation } from 'history'; | ||
import useAnchor from './useAnchor'; | ||
import { StateType } from '../state/state.types'; | ||
|
||
/** | ||
* A simple React component that uses useAnchor for testing purposes. | ||
*/ | ||
function TestComponent(): JSX.Element { | ||
useAnchor(); | ||
return <></>; | ||
} | ||
|
||
/** | ||
* A mock value for what useLocation from react-router would return | ||
*/ | ||
const MOCK_REACT_ROUTER_LOCATION: Partial<Location> = { | ||
hash: '#fragment', | ||
}; | ||
|
||
// mock implementation of useLocation to return the mock URL | ||
jest.mock('react-router', () => ({ | ||
__esModule: true, | ||
...jest.requireActual('react-router'), | ||
useLocation: jest.fn(), | ||
})); | ||
|
||
describe('useAnchor', () => { | ||
let createMockStore: MockStoreCreator<DeepPartial<StateType>>; | ||
|
||
beforeEach(() => { | ||
// use fake timers bc useAnchor uses setTimeout under the hood | ||
jest.useFakeTimers(); | ||
(useLocation as jest.Mock).mockReturnValue(MOCK_REACT_ROUTER_LOCATION); | ||
createMockStore = configureStore(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it('should scroll the element into view if the fragment in URL matches an element', () => { | ||
const mockStore = createMockStore({ | ||
scigateway: { | ||
siteLoading: false, | ||
}, | ||
router: { location: createLocation('/') }, | ||
}); | ||
|
||
const mockScrollIntoView = jest.fn(); | ||
// pretend an element is found that matches the fragment | ||
// the weird type cast is to get around TypeScript error saying | ||
// the object is missing a bunch of other properties | ||
// we obviously don't care about them so there's no point in stubbing them. | ||
jest.spyOn(document, 'getElementById').mockReturnValueOnce({ | ||
scrollIntoView: mockScrollIntoView, | ||
} as unknown as HTMLDivElement); | ||
|
||
mount( | ||
<Provider store={mockStore}> | ||
<TestComponent /> | ||
</Provider> | ||
); | ||
|
||
jest.runAllTimers(); | ||
|
||
// fragment matches an element, should be scrolled into view | ||
expect(mockScrollIntoView).toBeCalledTimes(1); | ||
}); | ||
|
||
it('should do nothing if the fragment in URL does not match any element', () => { | ||
const mockStore = createMockStore({ | ||
scigateway: { | ||
siteLoading: false, | ||
}, | ||
router: { location: createLocation('/') }, | ||
}); | ||
|
||
const mockScrollIntoView = jest.fn(); | ||
// pretend no element with #fragment is found | ||
// and pretend there is other elements with IDs != fragment | ||
jest.spyOn(document, 'getElementById').mockImplementation((id) => | ||
id === 'fragment' | ||
? null | ||
: ({ | ||
scrollIntoView: mockScrollIntoView, | ||
} as unknown as HTMLDivElement) | ||
); | ||
// another element with ID "other", which is obv != fragment | ||
const otherElem = document.getElementById('other'); | ||
|
||
mount( | ||
<Provider store={mockStore}> | ||
<TestComponent /> | ||
</Provider> | ||
); | ||
|
||
jest.runAllTimers(); | ||
|
||
// fragment doesn't match any element, useAnchor should not randomly | ||
// jump to other elements | ||
expect(otherElem.scrollIntoView).not.toBeCalled(); | ||
}); | ||
|
||
it('should do nothing even when fragment matches an element when website is loading', function () { | ||
const mockStore = createMockStore({ | ||
scigateway: { | ||
siteLoading: true, | ||
}, | ||
router: { location: createLocation('/') }, | ||
}); | ||
|
||
const mockScrollIntoView = jest.fn(); | ||
// pretend an element is found that matches the fragment | ||
// the weird type cast is to get around TypeScript error saying | ||
// the object is missing a bunch of other properties | ||
// we obviously don't care about them so there's no point in stubbing them. | ||
jest.spyOn(document, 'getElementById').mockReturnValueOnce({ | ||
scrollIntoView: mockScrollIntoView, | ||
} as unknown as HTMLDivElement); | ||
|
||
mount( | ||
<Provider store={mockStore}> | ||
<TestComponent /> | ||
</Provider> | ||
); | ||
|
||
jest.runAllTimers(); | ||
|
||
// fragment matches an element but website still loading | ||
expect(mockScrollIntoView).not.toBeCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { useLocation } from 'react-router'; | ||
import { useEffect } from 'react'; | ||
import { useSelector } from 'react-redux'; | ||
import type { StateType } from '../state/state.types'; | ||
|
||
/** | ||
* A React hook that detects fragments in the current URL, and makes the page jump to the element with the corresponding ID if required. | ||
* | ||
* For example, | ||
* https://example.com#section causes useAnchor to jump to the element with ID 'section' | ||
*/ | ||
function useAnchor(): void { | ||
// get the current fragment of the URL from react-router | ||
const { hash } = useLocation(); | ||
const isSiteLoading = useSelector<StateType>( | ||
(state) => state.scigateway.siteLoading | ||
); | ||
|
||
useEffect(() => { | ||
// need to make sure the website is not loading, | ||
// if the website is not done loading and the hook runs prematurely, | ||
// document.getElementById will return null because the page is not rendered fully. | ||
// once the website is done loading, the page should be fully rendered, and the target element should be available. | ||
if (hash && !isSiteLoading) { | ||
const elemId = hash.replace('#', ''); | ||
// find the element with the ID specified by the fragment | ||
// scroll to the element if found | ||
setTimeout(() => { | ||
document.getElementById(elemId)?.scrollIntoView(true); | ||
}, 0); | ||
} | ||
}, [hash, isSiteLoading]); | ||
} | ||
|
||
export default useAnchor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters