import { nextTestSetup } from 'e2e-utils'
import { getTitle, retry, waitFor } from 'next-test-utils'
describe('app dir - navigation', () => {
const { next, isNextDev, isNextStart, isNextDeploy } = nextTestSetup({
files: __dirname,
})
describe('query string', () => {
it('should set query correctly', async () => {
const browser = await next.browser('/')
expect(await browser.elementById('query').text()).toMatchInlineSnapshot(
`"<empty-query>"`
)
await browser.elementById('set-query').click()
await retry(() =>
expect(browser.elementById('query').text()).resolves.toEqual('a=b&c=d')
)
const url = new URL(await browser.url())
expect(url.searchParams.toString()).toMatchInlineSnapshot(`"a=b&c=d"`)
})
it('should set query with semicolon correctly', async () => {
const browser = await next.browser('/redirect/semicolon')
await retry(() =>
expect(browser.elementById('query').text()).resolves.toEqual('a=b%3Bc')
)
const url = new URL(await browser.url())
expect(url.searchParams.toString()).toBe('a=b%3Bc')
})
it('should handle unicode search params', async () => {
const requests: Array<{
pathname: string
ok: boolean
headers: Record<string, string>
}> = []
const browser = await next.browser('/search-params?name=名', {
beforePageLoad(page) {
page.on('response', async (res) => {
requests.push({
pathname: new URL(res.url()).pathname,
ok: res.ok(),
headers: res.headers(),
})
})
},
})
expect(await browser.elementById('name').text()).toBe('名')
await browser.elementById('link').click()
await browser.waitForElementByCss('#set-query')
await retry(() =>
expect(requests).toContainEqual({
pathname: '/',
ok: true,
headers: expect.objectContaining({
'content-type': 'text/x-component',
}),
})
)
})
it('should not reset shallow url updates on prefetch', async () => {
const browser = await next.browser('/search-params/shallow')
const button = await browser.elementByCss('button')
await button.click()
expect(await browser.url()).toMatch(/\?foo=bar$/)
const link = await browser.elementByCss('a')
await link.hover()
// Hovering a prefetch link should keep the URL intact
expect(await browser.url()).toMatch(/\?foo=bar$/)
})
describe('useParams identity between renders', () => {
it.each([
{
router: 'app',
pathname: '/search-params/foo',
// App Router doesn't re-render on initial load (the params are baked
// server side).
waitForNEffects: 1,
},
{
router: 'pages',
pathname: '/search-params-pages/foo',
// Pages Router re-renders on initial load and after hydration, the
// params when initially loaded are null.
waitForNEffects: 2,
},
])(
'should be stable in $router',
async ({ pathname, waitForNEffects }) => {
const browser = await next.browser(pathname)
// Expect to see the params changed message at least twice.
let lastLogIndex = await retry(async () => {
const logs: Array<{ message: string }> = await browser.log()
expect(
logs.filter(({ message }) => message === 'params changed')
).toHaveLength(waitForNEffects)
return logs.length
})
await browser.elementById('rerender-button').click()
await browser.elementById('rerender-button').click()
await browser.elementById('rerender-button').click()
await retry(async () => {
const rerender = await browser.elementById('rerender-button').text()
expect(rerender).toBe('Re-Render 3')
})
let logs: Array<{ message: string }> = await browser.log()
expect(logs.slice(lastLogIndex)).not.toContainEqual(
expect.objectContaining({
message: 'params changed',
})
)
lastLogIndex = logs.length
await browser.elementById('change-params-button').click()
await retry(async () => {
logs = await browser.log()
expect(logs.slice(lastLogIndex)).toContainEqual(
expect.objectContaining({
message: 'params changed',
})
)
})
}
)
})
})
describe('hash', () => {
it('should scroll to the specified hash', async () => {
const rscRequestUrls = new Set<string>()
const browser = await next.browser('/hash', {
beforePageLoad(page) {
page.on('request', (req) => {
const headers = req.headers()
if (headers['rsc']) {
rscRequestUrls.add(req.url())
}
})
},
})
const checkLink = async (
val: number | string,
expectedScroll: number
) => {
await browser.elementByCss(`#link-to-${val.toString()}`).click()
await retry(() =>
expect(browser.eval('window.pageYOffset')).resolves.toEqual(
expectedScroll
)
)
}
if (isNextStart || isNextDeploy) {
await browser.waitForIdleNetwork()
}
// Wait for all network requests to finish, and then initialize the flag
// used to determine if any query-param RSC requests are made.
rscRequestUrls.clear()
await checkLink(6, 128)
await checkLink(50, 744)
await checkLink(160, 2284)
await checkLink(300, 4244)
await checkLink(500, 7044) // this one is hash only (`href="#hash-500"`)
await checkLink('top', 0)
await checkLink('non-existent', 0)
if (!isNextDev) {
// Hash-only navigations should not request the query-param payload.
// In some runtimes, hash-only transitions can still trigger RSC
// requests for /hash itself, so we assert on query-param payloads.
const hasQueryParamRscRequest = Array.from(rscRequestUrls).some((url) =>
url.includes('with-query-param')
)
expect(hasQueryParamRscRequest).toBe(false)
}
await checkLink('query-param', 2284)
await browser.waitForIdleNetwork()
// There should be an RSC request if the query param is changed
const hasQueryParamRscRequest = Array.from(rscRequestUrls).some((url) =>
url.includes('with-query-param')
)
expect(hasQueryParamRscRequest).toBe(true)
})
it('should not scroll to hash when scroll={false} is set', async () => {
const browser = await next.browser('/hash-changes')
const curScroll = await browser.eval('document.documentElement.scrollTop')
await browser.elementByCss('#scroll-to-name-item-400-no-scroll').click()
expect(curScroll).toBe(
await browser.eval('document.documentElement.scrollTop')
)
})
})
describe('hash-with-scroll-offset', () => {
it('should scroll to the specified hash', async () => {
const browser = await next.browser('/hash-with-scroll-offset')
const checkLink = async (
val: number | string,
expectedScroll: number
) => {
await browser.elementByCss(`#link-to-${val.toString()}`).click()
await retry(() =>
expect(browser.eval('window.pageYOffset')).resolves.toEqual(
expectedScroll
)
)
}
await checkLink(6, 108)
await checkLink(50, 724)
await checkLink(160, 2264)
await checkLink(300, 4224)
await checkLink(500, 7024) // this one is hash only (`href="#hash-500"`)
await checkLink('top', 0)
await checkLink('non-existent', 0)
})
})
describe('hash-link-back-to-same-page', () => {
it('should scroll to the specified hash', async () => {
const browser = await next.browser('/hash-link-back-to-same-page')
const checkLink = async (
val: number | string,
expectedScroll: number
) => {
await browser.elementByCss(`#link-to-${val.toString()}`).click()
await retry(() =>
expect(browser.eval('window.pageYOffset')).resolves.toEqual(
expectedScroll
)
)
}
await checkLink(6, 114)
await checkLink(50, 730)
await checkLink(160, 2270)
await browser
.elementByCss('#to-other-page')
// Navigate to other
.click()
// Wait for other ot load
.waitForElementByCss('#link-to-home')
// Navigate back to hash-link-back-to-same-page
.click()
// Wait for hash-link-back-to-same-page to load
.waitForElementByCss('#to-other-page')
await retry(() =>
expect(browser.eval('window.pageYOffset')).resolves.toEqual(0)
)
})
})
describe('relative hashes and queries', () => {
const pathname = '/nested-relative-query-and-hash'
it('should work with a hash-only href', async () => {
const browser = await next.browser(pathname)
await browser.elementByCss('#link-to-h1-hash-only').click()
await retry(() =>
expect(browser.url()).resolves.toEqual(next.url + pathname + '#h1')
)
})
it('should work with a hash-only `router.push(...)`', async () => {
const browser = await next.browser(pathname)
await browser.elementByCss('#button-to-h3-hash-only').click()
await retry(() =>
expect(browser.url()).resolves.toEqual(next.url + pathname + '#h3')
)
})
it('should work with a query-only href', async () => {
const browser = await next.browser(pathname)
await browser.elementByCss('#link-to-dummy-query').click()
await retry(() =>
expect(browser.url()).resolves.toEqual(
next.url + pathname + '?foo=1&bar=2'
)
)
})
it('should work with both relative hashes and queries', async () => {
const browser = await next.browser(pathname)
await browser.elementByCss('#link-to-h2-with-hash-and-query').click()
await retry(() =>
expect(browser.url()).resolves.toEqual(
next.url + pathname + '?here=ok#h2'
)
)
// Only update hash
await browser.elementByCss('#link-to-h1-hash-only').click()
await retry(() =>
expect(browser.url()).resolves.toEqual(
next.url + pathname + '?here=ok#h1'
)
)
// Replace all with new query
await browser.elementByCss('#link-to-dummy-query').click()
await retry(() =>
expect(browser.url()).resolves.toEqual(
next.url + pathname + '?foo=1&bar=2'
)
)
// Add hash to existing query
await browser.elementByCss('#link-to-h1-hash-only').click()
await retry(() =>
expect(browser.url()).resolves.toEqual(
next.url + pathname + '?foo=1&bar=2#h1'
)
)
// Update hash again via `router.push(...)`
await browser.elementByCss('#button-to-h3-hash-only').click()
await retry(() =>
expect(browser.url()).resolves.toEqual(
next.url + pathname + '?foo=1&bar=2#h3'
)
)
})
})
describe('not-found', () => {
it('should trigger not-found in a server component', async () => {
const browser = await next.browser('/not-found/servercomponent')
expect(
await browser.waitForElementByCss('#not-found-component').text()
).toBe('Not Found!')
expect(
await browser
.waitForElementByCss('meta[name="robots"]')
.getAttribute('content')
).toBe('noindex')
})
it('should trigger not-found in a client component', async () => {
const browser = await next.browser('/not-found/clientcomponent')
expect(
await browser.waitForElementByCss('#not-found-component').text()
).toBe('Not Found!')
expect(
await browser
.waitForElementByCss('meta[name="robots"]')
.getAttribute('content')
).toBe('noindex')
})
it('should trigger not-found client-side', async () => {
const browser = await next.browser('/not-found/client-side')
await browser
.elementByCss('button')
.click()
.waitForElementByCss('#not-found-component')
expect(await browser.elementByCss('#not-found-component').text()).toBe(
'Not Found!'
)
expect(
await browser
.waitForElementByCss('meta[name="robots"]')
.getAttribute('content')
).toBe('noindex')
})
it('should trigger not-found while streaming', async () => {
const browser = await next.browser('/not-found/suspense')
expect(
await browser.waitForElementByCss('#not-found-component').text()
).toBe('Not Found!')
expect(
await browser
.waitForElementByCss('meta[name="robots"]')
.getAttribute('content')
).toBe('noindex')
})
})
describe('redirect', () => {
describe('components', () => {
it('should redirect in a server component', async () => {
const browser = await next.browser('/redirect/servercomponent')
await browser.waitForElementByCss('#result-page')
expect(await browser.elementByCss('#result-page').text()).toBe(
'Result Page'
)
})
it('should redirect in a client component', async () => {
const browser = await next.browser('/redirect/clientcomponent')
await browser.waitForElementByCss('#result-page')
expect(await browser.elementByCss('#result-page').text()).toBe(
'Result Page'
)
})
it('should redirect client-side', async () => {
const browser = await next.browser('/redirect/client-side')
await browser
.elementByCss('button')
.click()
.waitForElementByCss('#result-page')
expect(await browser.elementByCss('#result-page').text()).toBe(
'Result Page'
)
})
it('should redirect to external url', async () => {
const browser = await next.browser('/redirect/external')
expect(await browser.waitForElementByCss('h1').text()).toBe(
'Example Domain'
)
})
it('should redirect to external url, initiating only once', async () => {
const storageKey = Math.random()
const browser = await next.browser(
`/redirect/external-log/${storageKey}`
)
expect(await browser.waitForElementByCss('h1').text()).toBe(
'Example Domain'
)
// Now check the logs...
await browser.get(
`${next.url}/redirect/external-log/${storageKey}?read=1`
)
const stored = JSON.parse(await browser.elementByCss('pre').text())
if (stored['navigation-supported'] === 'false') {
// Old browser. Can't know how many times we navigated. Oh well.
return
}
expect(stored['navigation-supported']).toEqual('true')
// This one is a bit flaky during dev, original notes by @sophiebits:
// > Not actually sure why this is '2' in dev. Possibly something
// > related to an update triggered by <HotReload>?
expect(stored['navigate-https://example.vercel.sh/']).toBeOneOf(
isNextDev ? ['1', '2'] : ['1']
)
})
it.each(['/redirect/servercomponent', 'redirect/redirect-with-loading'])(
'should only trigger the redirect once (%s)',
async (path) => {
const requestedPathnames: string[] = []
const browser = await next.browser(path, {
beforePageLoad(page) {
page.on('request', async (req) => {
requestedPathnames.push(new URL(req.url()).pathname)
})
},
})
const initialTimestamp = await browser
.waitForElementByCss('#timestamp')
.text()
let attempts = 0
const maxAttempts = 5
try {
// this ensures the timestamp remains "stable" (ie, we didn't trigger another redirect)
await retry(async () => {
const currentTimestamp = await browser
.elementByCss('#timestamp')
.text()
attempts++
// If the timestamp has changed, throw immediately.
if (currentTimestamp !== initialTimestamp) {
throw new Error(
`Timestamp has changed from the initial '${initialTimestamp}' to '${currentTimestamp}'`
)
}
// If we've reached the last attempt without the timestamp changing, force a retry failure to keep going.
if (attempts < maxAttempts) {
throw new Error('Forcing continue')
}
})
} catch (err) {
// If we catch the "Forcing continue" error, it means our condition held until the end.
// If it's a different error (i.e., the timestamp changed), we rethrow it.
if (err.message !== 'Forcing continue') {
throw err // Rethrow if the error is not our "force continue" error.
}
// If it's our "forcing continue" error, do nothing. This means we succeeded.
}
// Ensure the redirect target page was only requested once.
expect(
requestedPathnames.filter(
(pathname) => pathname === '/redirect/result'
)
).toHaveLength(1)
}
)
})
describe('next.config.js redirects', () => {
it('should redirect from next.config.js', async () => {
const browser = await next.browser('/redirect/a')
expect(await browser.elementByCss('h1').text()).toBe('redirect-dest')
expect(await browser.url()).toBe(next.url + '/redirect-dest')
})
it('should redirect from next.config.js with link navigation', async () => {
const browser = await next.browser('/redirect/next-config-redirect')
await browser
.elementByCss('#redirect-a')
.click()
.waitForElementByCss('h1')
expect(await browser.elementByCss('h1').text()).toBe('redirect-dest')
expect(await browser.url()).toBe(next.url + '/redirect-dest')
})
})
describe('middleware redirects', () => {
it('should redirect from middleware', async () => {
const browser = await next.browser('/redirect-middleware-to-dashboard')
expect(await browser.elementByCss('h1').text()).toBe('redirect-dest')
expect(await browser.url()).toBe(next.url + '/redirect-dest')
})
it('should redirect from middleware with link navigation', async () => {
const browser = await next.browser('/redirect/next-middleware-redirect')
await browser
.elementByCss('#redirect-middleware')
.click()
.waitForElementByCss('h1')
expect(await browser.elementByCss('h1').text()).toBe('redirect-dest')
expect(await browser.url()).toBe(next.url + '/redirect-dest')
})
})
})
describe('external push', () => {
it('should push external url without affecting hooks', async () => {
// Log with sessionStorage to persist across navigations
const storageKey = Math.random()
const browser = await next.browser(`/external-push/${storageKey}`)
await browser.elementByCss('#go').click()
await browser.waitForCondition(
'window.location.origin === "https://example.vercel.sh"'
)
// Now check the logs...
await browser.get(`${next.url}/external-push/${storageKey}`)
const stored = JSON.parse(await browser.elementByCss('pre').text())
let expected = {
// Only one navigation
'navigate-https://example.vercel.sh/stuff?abc=123': '1',
'navigation-supported': 'true',
// Make sure /stuff?abc=123 is not logged here
[`path-/external-push/${storageKey}`]: 'true',
// isPending should have been true until the page unloads
lastIsPending: 'true',
}
if (stored['navigation-supported'] !== 'true') {
// Old browser. Can't know how many times we navigated. Oh well.
expected['navigation-supported'] = 'false'
for (const key in expected) {
if (key.startsWith('navigate-')) {
delete expected[key]
}
}
}
expect(stored).toEqual(expected)
})
})
describe('navigation between pages and app', () => {
it('should not contain _rsc query while navigating from app to pages', async () => {
// Initiate with app
const browser = await next.browser('/assertion/page')
await browser
.elementByCss('#link-to-pages')
.click()
.waitForElementByCss('#link-to-app')
expect(await browser.url()).toBe(next.url + '/some')
await browser
.elementByCss('#link-to-app')
.click()
.waitForElementByCss('#link-to-pages')
expect(await browser.url()).toBe(next.url + '/assertion/page')
})
it('should not contain _rsc query while navigating from pages to app', async () => {
// Initiate with pages
const browser = await next.browser('/some')
await browser
.elementByCss('#link-to-app')
.click()
.waitForElementByCss('#link-to-pages')
expect(await browser.url()).toBe(next.url + '/assertion/page')
await browser
.elementByCss('#link-to-pages')
.click()
.waitForElementByCss('#link-to-app')
expect(await browser.url()).toBe(next.url + '/some')
})
it('should not omit the hash while navigating from app to pages', async () => {
const browser = await next.browser('/hash-link-to-pages-router')
await browser
.elementByCss('#link-to-pages-router')
.click()
.waitForElementByCss('#link-to-app')
await retry(() =>
expect(browser.url()).resolves.toEqual(next.url + '/some#non-existent')
)
})
if (!isNextDev) {
// this test is pretty hard to test in playwright, so most of the heavy lifting is in the page component itself
// it triggers a hover on a link to initiate a prefetch request every second, and so we check that
// it doesn't repeatedly initiate the mpa navigation request
it('should not continously initiate a mpa navigation to the same URL when router state changes', async () => {
let requestCount = 0
await next.browser('/mpa-nav-test', {
beforePageLoad(page) {
page.on('request', (request) => {
const url = new URL(request.url())
// skip rsc prefetches
if (url.pathname === '/slow-page' && !url.search) {
requestCount++
}
})
},
})
// wait a few seconds since prefetches are triggered in 1s intervals in the page component
await waitFor(5000)
expect(requestCount).toBe(1)
})
}
})
describe('nested navigation', () => {
it('should navigate to nested pages', async () => {
const browser = await next.browser('/nested-navigation')
expect(await browser.elementByCss('h1').text()).toBe('Home')
const pages = [
['Electronics', ['Phones', 'Tablets', 'Laptops']],
['Clothing', ['Tops', 'Shorts', 'Shoes']],
['Books', ['Fiction', 'Biography', 'Education']],
] as const
for (const [category, subCategories] of pages) {
expect(
await browser
.elementByCss(
`a[href="/nested-navigation/${category.toLowerCase()}"]`
)
.click()
.waitForElementByCss(`#all-${category.toLowerCase()}`)
.text()
).toBe(`All ${category}`)
for (const subcategory of subCategories) {
expect(
await browser
.elementByCss(
`a[href="/nested-navigation/${category.toLowerCase()}/${subcategory.toLowerCase()}"]`
)
.click()
.waitForElementByCss(`#${subcategory.toLowerCase()}`)
.text()
).toBe(`${subcategory}`)
}
}
})
it('should load chunks correctly without double encoding of url', async () => {
const browser = await next.browser('/router')
await browser
.elementByCss('#dynamic-link')
.click()
.waitForElementByCss('#dynamic-gsp-content')
expect(await browser.elementByCss('#dynamic-gsp-content').text()).toBe(
'slug:1'
)
})
})
describe('SEO', () => {
it('should emit noindex meta tag for not found page when streaming', async () => {
const noIndexTag = '<meta name="robots" content="noindex"/>'
const defaultViewportTag =
'<meta name="viewport" content="width=device-width, initial-scale=1"/>'
const devErrorMetadataTag =
'<meta name="next-error" content="not-found"/>'
const html = await next.render('/not-found/suspense')
expect(html).toContain(noIndexTag)
// only contain once
expect(html.split(noIndexTag).length).toBe(2)
expect(html.split(defaultViewportTag).length).toBe(2)
if (isNextDev) {
// only contain dev error tag once
expect(html.split(devErrorMetadataTag).length).toBe(2)
}
})
it('should emit refresh meta tag for redirect page when streaming', async () => {
const html = await next.render('/redirect/suspense')
expect(html).toContain(
'<meta id="__next-page-redirect" http-equiv="refresh" content="1;url=/redirect/result"/>'
)
})
it('should emit refresh meta tag (permanent) for redirect page when streaming', async () => {
const html = await next.render('/redirect/suspense-2')
expect(html).toContain(
'<meta id="__next-page-redirect" http-equiv="refresh" content="0;url=/redirect/result"/>'
)
})
it('should contain default meta tags in error page', async () => {
const html = await next.render('/not-found/servercomponent')
expect(html).toContain('<meta name="robots" content="noindex"/>')
expect(html).toContain(
'<meta name="viewport" content="width=device-width, initial-scale=1"/>'
)
})
it('should not log 404 errors in ipc server', async () => {
await next.fetch('/this-path-does-not-exist')
expect(next.cliOutput).not.toInclude(
'PageNotFoundError: Cannot find module for page'
)
})
})
describe('navigations when attaching a Proxy to `window.Promise`', () => {
it('should navigate without issue', async () => {
const browser = await next.browser('/nested-navigation')
await browser.eval(`window.Promise = new Proxy(window.Promise, {})`)
expect(await browser.elementByCss('h1').text()).toBe('Home')
const pages = [
['Electronics', ['Phones', 'Tablets', 'Laptops']],
['Clothing', ['Tops', 'Shorts', 'Shoes']],
['Books', ['Fiction', 'Biography', 'Education']],
['Shoes', []],
] as const
for (const [category, subCategories] of pages) {
expect(
await browser
.elementByCss(
`a[href="/nested-navigation/${category.toLowerCase()}"]`
)
.click()
.waitForElementByCss(`#all-${category.toLowerCase()}`)
.text()
).toBe(`All ${category}`)
for (const subcategory of subCategories) {
expect(
await browser
.elementByCss(
`a[href="/nested-navigation/${category.toLowerCase()}/${subcategory.toLowerCase()}"]`
)
.click()
.waitForElementByCss(`#${subcategory.toLowerCase()}`)
.text()
).toBe(`${subcategory}`)
}
}
})
})
describe('scroll restoration', () => {
it('should restore original scroll position when navigating back', async () => {
const browser = await next.browser('/scroll-restoration', {
// throttling the CPU to rule out flakiness based on how quickly the page loads
cpuThrottleRate: 6,
})
const body = await browser.elementByCss('body')
expect(await body.text()).toContain('Item 50')
await browser.elementById('load-more').click()
await browser.elementById('load-more').click()
await browser.elementById('load-more').click()
expect(await body.text()).toContain('Item 200')
// scroll to the bottom of the page
await browser.eval('window.scrollTo(0, document.body.scrollHeight)')
// grab the current position
const scrollPosition = await browser.eval('window.pageYOffset')
await browser.elementByCss("[href='/scroll-restoration/other']").click()
await retry(async () => {
await browser.elementById('back-button').click()
})
const newScrollPosition = await browser.eval('window.pageYOffset')
// confirm that the scroll position was restored
expect(newScrollPosition).toEqual(scrollPosition)
})
})
describe('navigating to a page with async metadata', () => {
it('shows a fallback when prefetch was pending', async () => {
const resolveMetadataDuration = 5000
const browser = await next.browser('/metadata-await-promise')
// Hopefully this click happened before the prefetch was completed.
// TODO: Programmatically trigger prefetch e.g. by mounting the link later.
await browser
.elementByCss("[href='/metadata-await-promise/nested']")
.click()
await waitFor(resolveMetadataDuration + 500)
expect(await browser.elementById('page-content').text()).toBe('Content')
expect(await getTitle(browser)).toBe('Async Title')
})
it('shows a fallback when prefetch completed', async () => {
const resolveMetadataDuration = 5000
const browser = await next.browser('/metadata-await-promise')
if (!isNextDev) {
await waitFor(resolveMetadataDuration + 500)
}
await browser
.elementByCss("[href='/metadata-await-promise/nested']")
.click()
if (!isNextDev) {
expect(
await browser
.waitForElementByCss('title', resolveMetadataDuration + 500)
.text()
).toBe('Async Title')
}
expect(await browser.elementById('page-content').text()).toBe('Content')
})
})
describe('navigating to dynamic params & changing the casing', () => {
it('should load the page correctly', async () => {
const browser = await next.browser('/dynamic-param-casing-change')
// note the casing here capitalizes `ParamA`
await browser
.elementByCss("[href='/dynamic-param-casing-change/ParamA']")
.click()
// note the `paramA` casing has now changed
await browser
.elementByCss("[href='/dynamic-param-casing-change/paramA/noParam']")
.click()
await retry(async () => {
expect(await browser.elementByCss('body').text()).toContain(
'noParam page'
)
})
await browser.back()
await browser
.elementByCss("[href='/dynamic-param-casing-change/paramA/paramB']")
.click()
await retry(async () => {
expect(await browser.elementByCss('body').text()).toContain(
'[paramB] page'
)
})
})
})
describe('browser back to a revalidated page', () => {
it('should load the page', async () => {
const browser = await next.browser('/popstate-revalidate')
expect(await browser.elementByCss('h1').text()).toBe('Home')
await browser.elementByCss("[href='/popstate-revalidate/foo']").click()
await browser.waitForElementByCss('#submit-button')
expect(await browser.elementByCss('h1').text()).toBe('Form')
await browser.elementById('submit-button').click()
await retry(async () => {
expect(await browser.elementByCss('body').text()).toContain(
'Form Submitted.'
)
})
await browser.back()
await retry(async () => {
expect(await browser.elementByCss('h1').text()).toBe('Home')
})
})
})
describe('middleware redirect', () => {
it('should change browser location when router.refresh() gets a redirect response', async () => {
const browser = await next.browser('/redirect-on-refresh/auth')
await retry(async () =>
expect(await browser.url()).toBe(
next.url + '/redirect-on-refresh/dashboard'
)
)
})
})
if (isNextDev) {
describe('locale warnings', () => {
it('should warn about using the `locale` prop with `next/link` in app router', async () => {
const browser = await next.browser('/locale-app')
await retry(async () => {
const logs = await browser.log()
expect(logs).toContainEqual(
expect.objectContaining({
message: expect.stringContaining(
'The `locale` prop is not supported in `next/link` while using the `app` router.'
),
source: 'warning',
})
)
})
})
it('should have no warnings in pages router', async () => {
const browser = await next.browser('/locale-pages')
const logs = await browser.log()
expect(logs.filter((log) => log.source === 'warning')).toHaveLength(0)
})
})
}
describe('useRouter identity between navigations', () => {
it('should preserve identity when navigating to the same page', async () => {
const browser = await next.browser('/use-router/same-page')
expect(await browser.elementByCss('#count-from-server').text()).toBe('0')
expect(
await browser.elementByCss('#count-from-client-state').text()
).toBe('0')
expect(await browser.elementByCss('#router-change-count').text()).toBe(
'0'
)
for (let i = 1; i <= 3; i++) {
await browser.elementByCss('#trigger-push').click()
await retry(async () => {
expect(await browser.elementByCss('#count-from-server').text()).toBe(
`${i}`
)
// the client state is independent from the count we keep in the queryparam.
// we expect it to stay mounted and thus keep its own count.
// if it was getting unmounted, then its count of router changes would always stay at 0.
expect(
await browser.elementByCss('#count-from-client-state').text()
).toBe(`${i}`)
expect(
await browser.elementByCss('#router-change-count').text()
).toBe('0')
})
}
})
it('should preserve identity when navigating between different pages', async () => {
const browser = await next.browser('/use-router/shared-layout/one')
expect(await browser.elementByCss('h1').text()).toBe('One')
expect(
await browser.elementByCss('#count-from-client-state').text()
).toBe('0')
expect(await browser.elementByCss('#router-change-count').text()).toBe(
'0'
)
for (let i = 1; i <= 3; i++) {
await browser.elementByCss('#trigger-push').click()
await retry(async () => {
expect(await browser.elementByCss('h1').text()).toBe(
i % 2 === 0 ? 'One' : 'Two'
)
// we expect the client part to stay mounted and thus keep its own count.
// if it was getting unmounted, then its count of router changes would always be 0.
expect(
await browser.elementByCss('#count-from-client-state').text()
).toBe(`${i}`)
expect(
await browser.elementByCss('#router-change-count').text()
).toBe('0')
})
}
})
})
})