Commit
# Why Adds [URL hash](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) support to Expo Router. This enable 3rd party services that use the hash as part of work flows (e.g authentication) # How The hash is converted into a search parameter called `#`. This was chosen as its not a valid search parameter and the encoded version is unlikely to be in use. # Test Plan <!-- Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction. --> # Checklist <!-- Please check the appropriate items below if they apply to your diff. This is required for changes to Expo modules. --> - [ ] Documentation is up to date to reflect these changes (eg: https://docs.expo.dev and README.md). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). --------- Co-authored-by: Expo Bot <34669131+expo-bot@users.noreply.github.com> Co-authored-by: Aman Mittal <amandeepmittal@live.com>
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import React, { Text } from 'react-native'; | ||
|
||
import { router } from '../exports'; | ||
import { store } from '../global-state/router-store'; | ||
import { act, renderRouter, screen } from '../testing-library'; | ||
|
||
it('can push a hash url', () => { | ||
renderRouter({ | ||
index: () => <Text testID="index" />, | ||
test: () => <Text testID="test" />, | ||
}); | ||
|
||
expect(screen).toHavePathname('/'); | ||
expect(screen.getByTestId('index')).toBeOnTheScreen(); | ||
|
||
act(() => router.push('/test#a')); | ||
expect(screen.getByTestId('test')).toBeOnTheScreen(); | ||
|
||
act(() => router.push('/test#b')); | ||
act(() => router.push('/test#b')); | ||
act(() => router.push('/test#c')); | ||
|
||
expect(store.rootStateSnapshot()).toStrictEqual({ | ||
index: 4, | ||
key: expect.any(String), | ||
routeNames: ['index', 'test', '_sitemap', '+not-found'], | ||
routes: [ | ||
{ | ||
key: expect.any(String), | ||
name: 'index', | ||
params: undefined, | ||
path: '/', | ||
}, | ||
{ | ||
key: expect.any(String), | ||
name: 'test', | ||
params: { | ||
'#': 'a', | ||
}, | ||
path: undefined, | ||
}, | ||
{ | ||
key: expect.any(String), | ||
name: 'test', | ||
params: { | ||
'#': 'b', | ||
}, | ||
path: undefined, | ||
}, | ||
{ | ||
key: expect.any(String), | ||
name: 'test', | ||
params: { | ||
'#': 'b', | ||
}, | ||
path: undefined, | ||
}, | ||
{ | ||
key: expect.any(String), | ||
name: 'test', | ||
params: { | ||
'#': 'c', | ||
}, | ||
path: undefined, | ||
}, | ||
], | ||
stale: false, | ||
type: 'stack', | ||
}); | ||
}); | ||
|
||
it('works alongside with search params', () => { | ||
renderRouter({ | ||
index: () => <Text testID="index" />, | ||
test: () => <Text testID="test" />, | ||
}); | ||
|
||
expect(screen).toHavePathname('/'); | ||
expect(screen.getByTestId('index')).toBeOnTheScreen(); | ||
|
||
// Add a hash | ||
act(() => router.navigate('/test?a=1#hash1')); | ||
expect(screen.getByTestId('test')).toBeOnTheScreen(); | ||
expect(screen).toHaveSegments(['test']); | ||
expect(screen).toHavePathname('/test'); | ||
expect(screen).toHavePathnameWithParams('/test?a=1#hash1'); | ||
expect(screen).toHaveSearchParams({ a: '1', '#': 'hash1' }); | ||
|
||
act(() => router.navigate('/test?a=2#hash2')); | ||
expect(screen).toHaveSegments(['test']); | ||
expect(screen).toHavePathname('/test'); | ||
expect(screen).toHavePathnameWithParams('/test?a=2#hash2'); | ||
expect(screen).toHaveSearchParams({ a: '2', '#': 'hash2' }); | ||
|
||
act(() => router.navigate('/test?a=3')); | ||
expect(screen).toHaveSegments(['test']); | ||
expect(screen).toHavePathname('/test'); | ||
expect(screen).toHavePathnameWithParams('/test?a=3'); | ||
expect(screen).toHaveSearchParams({ a: '3' }); | ||
}); | ||
|
||
it('navigating to the same route with a hash will only rerender the screen', () => { | ||
renderRouter({ | ||
index: () => <Text testID="index" />, | ||
}); | ||
|
||
expect(store.rootStateSnapshot()).toStrictEqual({ | ||
routes: [ | ||
{ | ||
name: 'index', | ||
path: '/', | ||
}, | ||
], | ||
stale: true, | ||
}); | ||
|
||
act(() => router.navigate('/?#hash1')); | ||
|
||
expect(store.rootStateSnapshot()).toStrictEqual({ | ||
index: 0, | ||
key: expect.any(String), | ||
routeNames: ['index', '_sitemap', '+not-found'], | ||
routes: [ | ||
{ | ||
name: 'index', | ||
key: expect.any(String), | ||
path: '/', | ||
params: { | ||
'#': 'hash1', | ||
}, | ||
}, | ||
], | ||
stale: false, | ||
type: 'stack', | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import getPathFromState from '../getPathFromState'; | ||
|
||
describe('hash support', () => { | ||
it('appends hash to the path', () => { | ||
const state = { | ||
index: 0, | ||
key: 'key', | ||
routes: [ | ||
{ | ||
name: 'index', | ||
path: '/', | ||
params: { | ||
'#': 'hash1', | ||
}, | ||
}, | ||
], | ||
stale: true, | ||
type: 'stack', | ||
}; | ||
|
||
const config = { | ||
screens: { | ||
index: '', | ||
_sitemap: '_sitemap', | ||
}, | ||
}; | ||
|
||
expect(getPathFromState(state, config)).toBe('/#hash1'); | ||
}); | ||
|
||
it('works with nested state, existing router and path params', () => { | ||
const state = { | ||
index: 1, | ||
key: 'key', | ||
routeNames: ['index', '[test]', '_sitemap', '+not-found'], | ||
routes: [ | ||
{ | ||
key: 'key', | ||
name: 'index', | ||
params: undefined, | ||
path: '/', | ||
}, | ||
{ | ||
key: 'key', | ||
name: '[test]', | ||
params: { | ||
test: 'hello-world', | ||
query: 'true', | ||
'#': 'a', | ||
}, | ||
path: undefined, | ||
}, | ||
], | ||
stale: false, | ||
type: 'stack', | ||
}; | ||
|
||
const config = { | ||
screens: { | ||
'[test]': ':test', | ||
index: '', | ||
_sitemap: '_sitemap', | ||
}, | ||
}; | ||
|
||
expect(getPathFromState(state, config)).toBe('/hello-world?query=true#a'); | ||
}); | ||
}); |