5309c30c7d
### What Following an anchor link to a hash param, and then attempting to use `history.pushState` or `history.replaceState`, would result in an MPA navigation to the targeted URL. ### Why In #61822, a guard was added to prevent calling `ACTION_RESTORE` with a missing tree, to match other call-sites where we do the same. This was to prevent the app from crashing in the case where app router internals weren't available in the history state. The original assumption was that this is a rare / unlikely edge case. However the above scenario is a very probable case where this can happen, and triggering an MPA navigation isn't ideal. ### How This updates `ACTION_RESTORE` to be resilient to an undefined router state tree. When this happens, we'll still trigger the restore action to sync params, but use the existing flight router state. Closes NEXT-2502
495 lines
16 KiB
TypeScript
495 lines
16 KiB
TypeScript
import { createNextDescribe } from 'e2e-utils'
|
|
import { check } from 'next-test-utils'
|
|
|
|
createNextDescribe(
|
|
'shallow-routing',
|
|
{
|
|
files: __dirname,
|
|
},
|
|
({ next }) => {
|
|
describe('pushState', () => {
|
|
it('should support setting data', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-data')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-data')
|
|
.text()
|
|
).toBe('PushState Data')
|
|
await browser
|
|
.elementByCss('#push-state')
|
|
.click()
|
|
.waitForElementByCss('#state-updated')
|
|
.elementByCss('#get-latest')
|
|
.click()
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
`{"foo":"bar"}`
|
|
)
|
|
})
|
|
|
|
it('should support setting a different pathname reflected on usePathname', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-new-pathname')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-pathname')
|
|
.text()
|
|
).toBe('PushState Pathname')
|
|
|
|
await browser.elementByCss('#push-pathname').click()
|
|
|
|
// Check usePathname value is the new pathname
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
'/my-non-existent-path'
|
|
)
|
|
|
|
// Check current url is the new pathname
|
|
expect(await browser.url()).toBe(`${next.url}/my-non-existent-path`)
|
|
})
|
|
|
|
it('should support setting a different searchParam reflected on useSearchParams', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-new-searchparams')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-searchparams')
|
|
.text()
|
|
).toBe('PushState SearchParams')
|
|
|
|
await browser.elementByCss('#push-searchparams').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-new-searchparams?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#push-searchparams').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo-added')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-new-searchparams?query=foo-added`
|
|
)
|
|
})
|
|
|
|
it('should support setting a different url using a string', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-string-url')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-string-url')
|
|
.text()
|
|
).toBe('PushState String Url')
|
|
|
|
await browser.elementByCss('#push-string-url').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#push-string-url').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo-added')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url?query=foo-added`
|
|
)
|
|
})
|
|
|
|
it('should work when given a null state value', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-string-url')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-string-url')
|
|
.text()
|
|
).toBe('PushState String Url')
|
|
|
|
await browser.elementByCss('#push-string-url-null').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#push-string-url-null').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo-added')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url?query=foo-added`
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should work when given an undefined state value', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-string-url')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-string-url')
|
|
.text()
|
|
).toBe('PushState String Url')
|
|
|
|
await browser.elementByCss('#push-string-url-undefined').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#push-string-url-undefined').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo-added')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url?query=foo-added`
|
|
)
|
|
})
|
|
|
|
describe('replaceState', () => {
|
|
it('should support setting data', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-replacestate-data')
|
|
.click()
|
|
.waitForElementByCss('#replacestate-data')
|
|
.text()
|
|
).toBe('ReplaceState Data')
|
|
await browser
|
|
.elementByCss('#replace-state')
|
|
.click()
|
|
.waitForElementByCss('#state-updated')
|
|
.elementByCss('#get-latest')
|
|
.click()
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
`{"foo":"bar"}`
|
|
)
|
|
})
|
|
|
|
it('should support setting a different pathname reflected on usePathname', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-replacestate-new-pathname')
|
|
.click()
|
|
.waitForElementByCss('#replacestate-pathname')
|
|
.text()
|
|
).toBe('ReplaceState Pathname')
|
|
|
|
await browser.elementByCss('#replace-pathname').click()
|
|
|
|
// Check usePathname value is the new pathname
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
'/my-non-existent-path'
|
|
)
|
|
|
|
// Check current url is the new pathname
|
|
expect(await browser.url()).toBe(`${next.url}/my-non-existent-path`)
|
|
})
|
|
|
|
it('should support setting a different searchParam reflected on useSearchParams', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-replacestate-new-searchparams')
|
|
.click()
|
|
.waitForElementByCss('#replacestate-searchparams')
|
|
.text()
|
|
).toBe('ReplaceState SearchParams')
|
|
|
|
await browser.elementByCss('#replace-searchparams').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/replacestate-new-searchparams?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#replace-searchparams').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo-added')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/replacestate-new-searchparams?query=foo-added`
|
|
)
|
|
})
|
|
|
|
it('should support setting a different url using a string', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-replacestate-string-url')
|
|
.click()
|
|
.waitForElementByCss('#replacestate-string-url')
|
|
.text()
|
|
).toBe('ReplaceState String Url')
|
|
|
|
await browser.elementByCss('#replace-string-url').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/replacestate-string-url?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#replace-string-url').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo-added')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/replacestate-string-url?query=foo-added`
|
|
)
|
|
})
|
|
|
|
it('should work when given a null state value', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-replacestate-string-url')
|
|
.click()
|
|
.waitForElementByCss('#replacestate-string-url')
|
|
.text()
|
|
).toBe('ReplaceState String Url')
|
|
|
|
await browser.elementByCss('#replace-string-url-null').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/replacestate-string-url?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#replace-string-url-null').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo-added')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/replacestate-string-url?query=foo-added`
|
|
)
|
|
})
|
|
|
|
it('should work when given an undefined state value', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-replacestate-string-url')
|
|
.click()
|
|
.waitForElementByCss('#replacestate-string-url')
|
|
.text()
|
|
).toBe('ReplaceState String Url')
|
|
|
|
await browser.elementByCss('#replace-string-url-undefined').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/replacestate-string-url?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#replace-string-url-undefined').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo-added')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/replacestate-string-url?query=foo-added`
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('back and forward', () => {
|
|
describe('client-side navigation', () => {
|
|
it('should support setting a different pathname reflected on usePathname and then still support navigating back and forward', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-new-pathname')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-pathname')
|
|
.text()
|
|
).toBe('PushState Pathname')
|
|
|
|
await browser.elementByCss('#push-pathname').click()
|
|
|
|
// Check usePathname value is the new pathname
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
'/my-non-existent-path'
|
|
)
|
|
|
|
// Check current url is the new pathname
|
|
expect(await browser.url()).toBe(`${next.url}/my-non-existent-path`)
|
|
|
|
// Navigate back
|
|
await browser.back()
|
|
|
|
// Check usePathname value is the old pathname
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
'/pushstate-new-pathname'
|
|
)
|
|
|
|
await browser.forward()
|
|
|
|
// Check usePathname value is the old pathname
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
'/my-non-existent-path'
|
|
)
|
|
})
|
|
})
|
|
|
|
// Browser navigation using `<a>` and such.
|
|
describe('mpa navigation', () => {
|
|
it('should support setting data and then still support navigating back and forward', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-data')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-data')
|
|
.text()
|
|
).toBe('PushState Data')
|
|
await browser
|
|
.elementByCss('#push-state')
|
|
.click()
|
|
.waitForElementByCss('#state-updated')
|
|
.elementByCss('#get-latest')
|
|
.click()
|
|
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
`{"foo":"bar"}`
|
|
)
|
|
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-a-mpa')
|
|
.click()
|
|
.waitForElementByCss('#page-a')
|
|
.text()
|
|
).toBe('Page A')
|
|
|
|
// Navigate back
|
|
await browser.back()
|
|
|
|
// Check usePathname value is the old pathname
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
`{"foo":"bar"}`
|
|
)
|
|
|
|
await browser.forward()
|
|
|
|
await check(
|
|
() =>
|
|
browser
|
|
.elementByCss('#to-a-mpa')
|
|
.click()
|
|
.waitForElementByCss('#page-a')
|
|
.text(),
|
|
'Page A'
|
|
)
|
|
})
|
|
|
|
it('should support hash navigations while continuing to work for pushState/replaceState APIs', async () => {
|
|
const browser = await next.browser('/a')
|
|
expect(
|
|
await browser
|
|
.elementByCss('#to-pushstate-string-url')
|
|
.click()
|
|
.waitForElementByCss('#pushstate-string-url')
|
|
.text()
|
|
).toBe('PushState String Url')
|
|
|
|
await browser.elementByCss('#hash-navigation').click()
|
|
|
|
// Check current url contains the hash
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url#content`
|
|
)
|
|
|
|
await browser.elementByCss('#push-string-url').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(() => browser.elementByCss('#my-data').text(), 'foo')
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url?query=foo`
|
|
)
|
|
|
|
// Same cycle a second time
|
|
await browser.elementByCss('#push-string-url').click()
|
|
|
|
// Check useSearchParams value is the new searchparam
|
|
await check(
|
|
() => browser.elementByCss('#my-data').text(),
|
|
'foo-added'
|
|
)
|
|
|
|
// Check current url is the new searchparams
|
|
expect(await browser.url()).toBe(
|
|
`${next.url}/pushstate-string-url?query=foo-added`
|
|
)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
)
|