UNPKG

@nadeshikon/plugin-nextjs

Version:
1,145 lines (963 loc) 92.2 kB
import { createNext, FileRef } from 'e2e-utils' import crypto from 'crypto' import { NextInstance } from 'test/lib/next-modes/base' import { check, fetchViaHTTP, getRedboxHeader, hasRedbox, renderViaHTTP, waitFor } from 'next-test-utils' import path from 'path' import cheerio from 'cheerio' import webdriver from 'next-webdriver' describe('app dir', () => { const isDev = (global as any).isNextDev let next: NextInstance function runTests() { beforeAll(async () => { next = await createNext({ files: new FileRef(path.join(__dirname, 'app')), dependencies: { swr: '2.0.0-rc.0', react: 'latest', 'react-dom': 'latest', sass: 'latest', }, }) }, 600000) afterAll(async () => { await next.destroy() }) if (!(global as any).isNextDeploy) { it('should not share edge workers', async () => { const controller1 = new AbortController() const controller2 = new AbortController() fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { signal: controller1.signal, }).catch(() => {}) fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { signal: controller2.signal, }).catch(() => {}) await waitFor(1000) controller1.abort() const controller3 = new AbortController() fetchViaHTTP(next.url, '/slow-page-no-loading', undefined, { signal: controller3.signal, }).catch(() => {}) await waitFor(1000) controller2.abort() controller3.abort() const res = await fetchViaHTTP(next.url, '/slow-page-no-loading') expect(res.status).toBe(200) expect(await res.text()).toContain('hello from slow page') expect(next.cliOutput).not.toContain('A separate worker must be used for each render') }) } if ((global as any).isNextStart) { it('should generate build traces correctly', async () => { const trace = JSON.parse( await next.readFile('.next/server/app/dashboard/deployments/[id]/page.js.nft.json'), ) as { files: string[] } expect(trace.files.some((file) => file.endsWith('data.json'))).toBe(true) }) } it('should use application/octet-stream for flight', async () => { const res = await fetchViaHTTP( next.url, '/dashboard/deployments/123', {}, { headers: { ['RSC'.toString()]: '1', }, }, ) expect(res.headers.get('Content-Type')).toBe('application/octet-stream') }) it('should use application/octet-stream for flight with edge runtime', async () => { const res = await fetchViaHTTP( next.url, '/dashboard', {}, { headers: { ['RSC'.toString()]: '1', }, }, ) expect(res.headers.get('Content-Type')).toBe('application/octet-stream') }) it('should pass props from getServerSideProps in root layout', async () => { const html = await renderViaHTTP(next.url, '/dashboard') const $ = cheerio.load(html) expect($('title').text()).toBe('hello world') }) it('should serve from pages', async () => { const html = await renderViaHTTP(next.url, '/') expect(html).toContain('hello from pages/index') // esm imports should work fine in pages/ expect(html).toContain('swr-index') }) it('should serve dynamic route from pages', async () => { const html = await renderViaHTTP(next.url, '/blog/first') expect(html).toContain('hello from pages/blog/[slug]') }) it('should serve from public', async () => { const html = await renderViaHTTP(next.url, '/hello.txt') expect(html).toContain('hello world') }) it('should serve from app', async () => { const html = await renderViaHTTP(next.url, '/dashboard') expect(html).toContain('hello from app/dashboard') }) if (!(global as any).isNextDeploy) { it('should serve /index as separate page', async () => { const html = await renderViaHTTP(next.url, '/dashboard/index') expect(html).toContain('hello from app/dashboard/index') // should load chunks generated via async import correctly with React.lazy expect(html).toContain('hello from lazy') // should support `dynamic` in both server and client components expect(html).toContain('hello from dynamic on server') expect(html).toContain('hello from dynamic on client') }) it('should serve polyfills for browsers that do not support modules', async () => { const html = await renderViaHTTP(next.url, '/dashboard/index') expect(html).toMatch(/<script src="\/_next\/static\/chunks\/polyfills(-\w+)?\.js" nomodule="">/) }) } // TODO-APP: handle css modules fouc in dev it.skip('should handle css imports in next/dynamic correctly', async () => { const browser = await webdriver(next.url, '/dashboard/index') expect( await browser.eval(`window.getComputedStyle(document.querySelector('#css-text-dynamic-server')).color`), ).toBe('rgb(0, 0, 255)') expect(await browser.eval(`window.getComputedStyle(document.querySelector('#css-text-lazy')).color`)).toBe( 'rgb(128, 0, 128)', ) }) it('should include layouts when no direct parent layout', async () => { const html = await renderViaHTTP(next.url, '/dashboard/integrations') const $ = cheerio.load(html) // Should not be nested in dashboard expect($('h1').text()).toBe('Dashboard') // Should include the page text expect($('p').text()).toBe('hello from app/dashboard/integrations') }) // TODO-APP: handle new root layout it.skip('should not include parent when not in parent directory with route in directory', async () => { const html = await renderViaHTTP(next.url, '/dashboard/hello') const $ = cheerio.load(html) // new root has to provide it's own custom root layout or the default // is used instead expect(html).toContain('<html') expect(html).toContain('<body') expect($('html').hasClass('this-is-the-document-html')).toBeFalsy() expect($('body').hasClass('this-is-the-document-body')).toBeFalsy() // Should not be nested in dashboard expect($('h1').text()).toBeFalsy() // Should render the page text expect($('p').text()).toBe('hello from app/dashboard/rootonly/hello') }) // NTL Skip it.skip('should use new root layout when provided', async () => { const html = await renderViaHTTP(next.url, '/dashboard/another') const $ = cheerio.load(html) // new root has to provide it's own custom root layout or the default // is used instead expect($('html').hasClass('this-is-another-document-html')).toBeTruthy() expect($('body').hasClass('this-is-another-document-body')).toBeTruthy() // Should not be nested in dashboard expect($('h1').text()).toBeFalsy() // Should render the page text expect($('p').text()).toBe('hello from newroot/dashboard/another') }) it('should not create new root layout when nested (optional)', async () => { const html = await renderViaHTTP(next.url, '/dashboard/deployments/breakdown') const $ = cheerio.load(html) // new root has to provide it's own custom root layout or the default // is used instead expect($('html').hasClass('this-is-the-document-html')).toBeTruthy() expect($('body').hasClass('this-is-the-document-body')).toBeTruthy() // Should be nested in dashboard expect($('h1').text()).toBe('Dashboard') expect($('h2').text()).toBe('Custom dashboard') // Should render the page text expect($('p').text()).toBe('hello from app/dashboard/(custom)/deployments/breakdown') }) it('should include parent document when no direct parent layout', async () => { const html = await renderViaHTTP(next.url, '/dashboard/integrations') const $ = cheerio.load(html) expect($('html').hasClass('this-is-the-document-html')).toBeTruthy() expect($('body').hasClass('this-is-the-document-body')).toBeTruthy() }) it('should not include parent when not in parent directory', async () => { const html = await renderViaHTTP(next.url, '/dashboard/changelog') const $ = cheerio.load(html) // Should not be nested in dashboard expect($('h1').text()).toBeFalsy() // Should include the page text expect($('p').text()).toBe('hello from app/dashboard/changelog') }) it('should serve nested parent', async () => { const html = await renderViaHTTP(next.url, '/dashboard/deployments/123') const $ = cheerio.load(html) // Should be nested in dashboard expect($('h1').text()).toBe('Dashboard') // Should be nested in deployments expect($('h2').text()).toBe('Deployments hello') }) it('should serve dynamic parameter', async () => { const html = await renderViaHTTP(next.url, '/dashboard/deployments/123') const $ = cheerio.load(html) // Should include the page text with the parameter expect($('p').text()).toBe('hello from app/dashboard/deployments/[id]. ID is: 123') }) // TODO-APP: fix to ensure behavior matches on deploy if (!(global as any).isNextDeploy) { it('should serve page as a segment name correctly', async () => { const html = await renderViaHTTP(next.url, '/dashboard/page') expect(html).toContain('hello dashboard/page!') }) } it('should include document html and body', async () => { const html = await renderViaHTTP(next.url, '/dashboard') const $ = cheerio.load(html) expect($('html').hasClass('this-is-the-document-html')).toBeTruthy() expect($('body').hasClass('this-is-the-document-body')).toBeTruthy() }) it('should not serve when layout is provided but no folder index', async () => { const res = await fetchViaHTTP(next.url, '/dashboard/deployments') expect(res.status).toBe(404) expect(await res.text()).toContain('This page could not be found') }) // TODO-APP: do we want to make this only work for /root or is it allowed // to work for /pages as well? it.skip('should match partial parameters', async () => { const html = await renderViaHTTP(next.url, '/partial-match-123') expect(html).toContain('hello from app/partial-match-[id]. ID is: 123') }) describe('rewrites', () => { // TODO-APP: rewrite url is broken it.skip('should support rewrites on initial load', async () => { const browser = await webdriver(next.url, '/rewritten-to-dashboard') expect(await browser.elementByCss('h1').text()).toBe('Dashboard') expect(await browser.url()).toBe(`${next.url}/rewritten-to-dashboard`) }) it('should support rewrites on client-side navigation from pages to app with existing pages path', async () => { const browser = await webdriver(next.url, '/link-to-rewritten-path') try { // Click the link. await browser.elementById('link-to-rewritten-path').click() await browser.waitForElementByCss('#from-dashboard') // Check to see that we were rewritten and not redirected. // TODO-APP: rewrite url is broken // expect(await browser.url()).toBe(`${next.url}/rewritten-to-dashboard`) // Check to see that the page we navigated to is in fact the dashboard. expect(await browser.elementByCss('#from-dashboard').text()).toBe('hello from app/dashboard') } finally { await browser.close() } }) it.skip('should support rewrites on client-side navigation', async () => { const browser = await webdriver(next.url, '/rewrites') try { // Click the link. await browser.elementById('link').click() await browser.waitForElementByCss('#from-dashboard') // Check to see that we were rewritten and not redirected. expect(await browser.url()).toBe(`${next.url}/rewritten-to-dashboard`) // Check to see that the page we navigated to is in fact the dashboard. expect(await browser.elementByCss('#from-dashboard').text()).toBe('hello from app/dashboard') } finally { await browser.close() } }) }) // TODO-APP: Enable in development ;(isDev ? it.skip : it)( 'should not rerender layout when navigating between routes in the same layout', async () => { const browser = await webdriver(next.url, '/same-layout/first') try { // Get the render id from the dom and click the first link. const firstRenderID = await browser.elementById('render-id').text() await browser.elementById('link').click() await browser.waitForElementByCss('#second-page') // Get the render id from the dom again, it should be the same! const secondRenderID = await browser.elementById('render-id').text() expect(secondRenderID).toBe(firstRenderID) // Navigate back to the first page again by clicking the link. await browser.elementById('link').click() await browser.waitForElementByCss('#first-page') // Get the render id from the dom again, it should be the same! const thirdRenderID = await browser.elementById('render-id').text() expect(thirdRenderID).toBe(firstRenderID) } finally { await browser.close() } }, ) it('should handle hash in initial url', async () => { const browser = await webdriver(next.url, '/dashboard#abc') try { // Check if hash is preserved expect(await browser.eval('window.location.hash')).toBe('#abc') await waitFor(1000) // Check again to be sure as it might be timed different expect(await browser.eval('window.location.hash')).toBe('#abc') } finally { await browser.close() } }) describe('parallel routes', () => { if (!(global as any).isNextDeploy) { it('should match parallel routes', async () => { const html = await renderViaHTTP(next.url, '/parallel/nested') expect(html).toContain('parallel/layout') expect(html).toContain('parallel/@foo/nested/layout') expect(html).toContain('parallel/@foo/nested/@a/page') expect(html).toContain('parallel/@foo/nested/@b/page') expect(html).toContain('parallel/@bar/nested/layout') expect(html).toContain('parallel/@bar/nested/@a/page') expect(html).toContain('parallel/@bar/nested/@b/page') expect(html).toContain('parallel/nested/page') }) } it('should match parallel routes in route groups', async () => { const html = await renderViaHTTP(next.url, '/parallel/nested-2') expect(html).toContain('parallel/layout') expect(html).toContain('parallel/(new)/layout') expect(html).toContain('parallel/(new)/@baz/nested/page') }) }) describe('<Link />', () => { it('should hard push', async () => { const browser = await webdriver(next.url, '/link-hard-push/123') try { // Click the link on the page, and verify that the history entry was // added. expect(await browser.eval('window.history.length')).toBe(2) await browser.elementById('link').click() await browser.waitForElementByCss('#render-id-456') expect(await browser.eval('window.history.length')).toBe(3) // Get the id on the rendered page. const firstID = await browser.elementById('render-id-456').text() // Go back, and redo the navigation by clicking the link. await browser.back() await browser.elementById('link').click() await browser.waitForElementByCss('#render-id-456') // Get the id again, and compare, they should not be the same. const secondID = await browser.elementById('render-id-456').text() expect(secondID).not.toBe(firstID) } finally { await browser.close() } }) it('should hard replace', async () => { const browser = await webdriver(next.url, '/link-hard-replace/123') try { // Click the link on the page, and verify that the history entry was NOT // added. expect(await browser.eval('window.history.length')).toBe(2) await browser.elementById('link').click() await browser.waitForElementByCss('#render-id-456') expect(await browser.eval('window.history.length')).toBe(2) // Get the date again, and compare, they should not be the same. const firstId = await browser.elementById('render-id-456').text() // Navigate to the subpage, verify that the history entry was NOT added. await browser.elementById('link').click() await browser.waitForElementByCss('#render-id-123') expect(await browser.eval('window.history.length')).toBe(2) // Navigate back again, verify that the history entry was NOT added. await browser.elementById('link').click() await browser.waitForElementByCss('#render-id-456') expect(await browser.eval('window.history.length')).toBe(2) // Get the date again, and compare, they should not be the same. const secondId = await browser.elementById('render-id-456').text() expect(firstId).not.toBe(secondId) } finally { await browser.close() } }) // TODO-APP: Re-enable this test. it('should soft push', async () => { const browser = await webdriver(next.url, '/link-soft-push') try { // Click the link on the page, and verify that the history entry was // added. expect(await browser.eval('window.history.length')).toBe(2) await browser.elementById('link').click() await browser.waitForElementByCss('#render-id') expect(await browser.eval('window.history.length')).toBe(3) // Get the id on the rendered page. const firstID = await browser.elementById('render-id').text() // Go back, and redo the navigation by clicking the link. await browser.back() await browser.elementById('link').click() // Get the date again, and compare, they should be the same. const secondID = await browser.elementById('render-id').text() expect(firstID).toBe(secondID) } finally { await browser.close() } }) // TODO-APP: investigate this test it.skip('should soft replace', async () => { const browser = await webdriver(next.url, '/link-soft-replace') try { // Get the render ID so we can compare it. const firstID = await browser.elementById('render-id').text() // Click the link on the page, and verify that the history entry was NOT // added. expect(await browser.eval('window.history.length')).toBe(2) await browser.elementById('self-link').click() await browser.waitForElementByCss('#render-id') expect(await browser.eval('window.history.length')).toBe(2) // Get the id on the rendered page. const secondID = await browser.elementById('render-id').text() expect(secondID).toBe(firstID) // Navigate to the subpage, verify that the history entry was NOT added. await browser.elementById('subpage-link').click() await browser.waitForElementByCss('#back-link') expect(await browser.eval('window.history.length')).toBe(2) // Navigate back again, verify that the history entry was NOT added. await browser.elementById('back-link').click() await browser.waitForElementByCss('#render-id') expect(await browser.eval('window.history.length')).toBe(2) // Get the date again, and compare, they should be the same. const thirdID = await browser.elementById('render-id').text() expect(thirdID).toBe(firstID) } finally { await browser.close() } }) it('should be soft for back navigation', async () => { const browser = await webdriver(next.url, '/with-id') try { // Get the id on the rendered page. const firstID = await browser.elementById('render-id').text() // Click the link, and go back. await browser.elementById('link').click() await browser.waitForElementByCss('#from-navigation') await browser.back() // Get the date again, and compare, they should be the same. const secondID = await browser.elementById('render-id').text() expect(firstID).toBe(secondID) } finally { await browser.close() } }) it('should be soft for forward navigation', async () => { const browser = await webdriver(next.url, '/with-id') try { // Click the link. await browser.elementById('link').click() await browser.waitForElementByCss('#from-navigation') // Get the id on the rendered page. const firstID = await browser.elementById('render-id').text() // Go back, then forward. await browser.back() await browser.forward() // Get the date again, and compare, they should be the same. const secondID = await browser.elementById('render-id').text() expect(firstID).toBe(secondID) } finally { await browser.close() } }) it('should allow linking from app page to pages page', async () => { const browser = await webdriver(next.url, '/pages-linking') try { // Click the link. await browser.elementById('app-link').click() expect(await browser.waitForElementByCss('#pages-link').text()).toBe('To App Page') // Click the other link. await browser.elementById('pages-link').click() expect(await browser.waitForElementByCss('#app-link').text()).toBe('To Pages Page') } finally { await browser.close() } }) it('should navigate to pages dynamic route from pages page if it overlaps with an app page', async () => { const browser = await webdriver(next.url, '/dynamic-pages-route-app-overlap') try { // Click the link. await browser.elementById('pages-link').click() expect(await browser.waitForElementByCss('#pages-text').text()).toBe( 'hello from pages/dynamic-pages-route-app-overlap/[slug]', ) // When refreshing the browser, the app page should be rendered await browser.refresh() expect(await browser.waitForElementByCss('#app-text').text()).toBe( 'hello from app/dynamic-pages-route-app-overlap/app-dir/page', ) } finally { await browser.close() } }) }) describe('server components', () => { // TODO-APP: why is this not servable but /dashboard+rootonly/hello.server.js // should be? Seems like they both either should be servable or not it('should not serve .server.js as a path', async () => { // Without .server.js should serve const html = await renderViaHTTP(next.url, '/should-not-serve-server') expect(html).toContain('hello from app/should-not-serve-server') // Should not serve `.server` const res = await fetchViaHTTP(next.url, '/should-not-serve-server.server') expect(res.status).toBe(404) expect(await res.text()).toContain('This page could not be found') // Should not serve `.server.js` const res2 = await fetchViaHTTP(next.url, '/should-not-serve-server.server.js') expect(res2.status).toBe(404) expect(await res2.text()).toContain('This page could not be found') }) it('should not serve .client.js as a path', async () => { // Without .client.js should serve const html = await renderViaHTTP(next.url, '/should-not-serve-client') expect(html).toContain('hello from app/should-not-serve-client') // Should not serve `.client` const res = await fetchViaHTTP(next.url, '/should-not-serve-client.client') expect(res.status).toBe(404) expect(await res.text()).toContain('This page could not be found') // Should not serve `.client.js` const res2 = await fetchViaHTTP(next.url, '/should-not-serve-client.client.js') expect(res2.status).toBe(404) expect(await res2.text()).toContain('This page could not be found') }) it('should serve shared component', async () => { // Without .client.js should serve const html = await renderViaHTTP(next.url, '/shared-component-route') expect(html).toContain('hello from app/shared-component-route') }) describe('dynamic routes', () => { it('should only pass params that apply to the layout', async () => { const html = await renderViaHTTP(next.url, '/dynamic/books/hello-world') const $ = cheerio.load(html) expect($('#dynamic-layout-params').text()).toBe('{}') expect($('#category-layout-params').text()).toBe('{"category":"books"}') expect($('#id-layout-params').text()).toBe('{"category":"books","id":"hello-world"}') expect($('#id-page-params').text()).toBe('{"category":"books","id":"hello-world"}') }) }) describe('catch-all routes', () => { it('should handle optional segments', async () => { const params = ['this', 'is', 'a', 'test'] const route = params.join('/') const html = await renderViaHTTP(next.url, `/catch-all-optional/${route}`) const $ = cheerio.load(html) expect($('#text').attr('data-params')).toBe(route) }) it('should handle optional segments root', async () => { const html = await renderViaHTTP(next.url, `/catch-all-optional`) const $ = cheerio.load(html) expect($('#text').attr('data-params')).toBe('') }) it('should handle optional catch-all segments link', async () => { const browser = await webdriver(next.url, '/catch-all-link') expect(await browser.elementByCss('#to-catch-all-optional').click().waitForElementByCss('#text').text()).toBe( `hello from /catch-all-optional/this/is/a/test`, ) }) it('should handle required segments', async () => { const params = ['this', 'is', 'a', 'test'] const route = params.join('/') const html = await renderViaHTTP(next.url, `/catch-all/${route}`) const $ = cheerio.load(html) expect($('#text').attr('data-params')).toBe(route) expect($('#not-a-page').text()).toBe('Not a page') // Components under catch-all should not be treated as route that errors during build. // They should be rendered properly when imported in page route. expect($('#widget').text()).toBe('widget') }) it('should handle required segments root as not found', async () => { const res = await fetchViaHTTP(next.url, `/catch-all`) expect(res.status).toBe(404) expect(await res.text()).toContain('This page could not be found') }) it('should handle catch-all segments link', async () => { const browser = await webdriver(next.url, '/catch-all-link') expect(await browser.elementByCss('#to-catch-all').click().waitForElementByCss('#text').text()).toBe( `hello from /catch-all/this/is/a/test`, ) }) }) describe('should serve client component', () => { it('should serve server-side', async () => { const html = await renderViaHTTP(next.url, '/client-component-route') const $ = cheerio.load(html) expect($('p').text()).toBe('hello from app/client-component-route. count: 0') }) // TODO-APP: investigate hydration not kicking in on some runs it('should serve client-side', async () => { const browser = await webdriver(next.url, '/client-component-route') // After hydration count should be 1 expect(await browser.elementByCss('p').text()).toBe('hello from app/client-component-route. count: 1') }) }) describe('should include client component layout with server component route', () => { it('should include it server-side', async () => { const html = await renderViaHTTP(next.url, '/client-nested') const $ = cheerio.load(html) // Should not be nested in dashboard expect($('h1').text()).toBe('Client Nested. Count: 0') // Should include the page text expect($('p').text()).toBe('hello from app/client-nested') }) it('should include it client-side', async () => { const browser = await webdriver(next.url, '/client-nested') // After hydration count should be 1 expect(await browser.elementByCss('h1').text()).toBe('Client Nested. Count: 1') // After hydration count should be 1 expect(await browser.elementByCss('p').text()).toBe('hello from app/client-nested') }) }) describe('Loading', () => { it('should render loading.js in initial html for slow page', async () => { const html = await renderViaHTTP(next.url, '/slow-page-with-loading') const $ = cheerio.load(html) expect($('#loading').text()).toBe('Loading...') }) it('should render loading.js in browser for slow page', async () => { const browser = await webdriver(next.url, '/slow-page-with-loading', { waitHydration: false, }) // TODO-APP: `await webdriver()` causes waiting for the full page to complete streaming. At that point "Loading..." is replaced by the actual content // expect(await browser.elementByCss('#loading').text()).toBe('Loading...') expect(await browser.elementByCss('#slow-page-message').text()).toBe('hello from slow page') }) it('should render loading.js in initial html for slow layout', async () => { const html = await renderViaHTTP(next.url, '/slow-layout-with-loading/slow') const $ = cheerio.load(html) expect($('#loading').text()).toBe('Loading...') }) it('should render loading.js in browser for slow layout', async () => { const browser = await webdriver(next.url, '/slow-layout-with-loading/slow', { waitHydration: false, }) // TODO-APP: `await webdriver()` causes waiting for the full page to complete streaming. At that point "Loading..." is replaced by the actual content // expect(await browser.elementByCss('#loading').text()).toBe('Loading...') expect(await browser.elementByCss('#slow-layout-message').text()).toBe('hello from slow layout') expect(await browser.elementByCss('#page-message').text()).toBe('Hello World') }) it('should render loading.js in initial html for slow layout and page', async () => { const html = await renderViaHTTP(next.url, '/slow-layout-and-page-with-loading/slow') const $ = cheerio.load(html) expect($('#loading-layout').text()).toBe('Loading layout...') expect($('#loading-page').text()).toBe('Loading page...') }) it('should render loading.js in browser for slow layout and page', async () => { const browser = await webdriver(next.url, '/slow-layout-and-page-with-loading/slow', { waitHydration: false, }) // TODO-APP: `await webdriver()` causes waiting for the full page to complete streaming. At that point "Loading..." is replaced by the actual content // expect(await browser.elementByCss('#loading-layout').text()).toBe('Loading...') // expect(await browser.elementByCss('#loading-page').text()).toBe('Loading...') expect(await browser.elementByCss('#slow-layout-message').text()).toBe('hello from slow layout') expect(await browser.elementByCss('#slow-page-message').text()).toBe('hello from slow page') }) }) describe('middleware', () => { it.each(['rewrite', 'redirect'])( `should strip internal query parameters from requests to middleware for %s`, async (method) => { const browser = await webdriver(next.url, '/internal') try { // Wait for and click the navigation element, this should trigger // the flight request that'll be caught by the middleware. If the // middleware sees any flight data on the request it'll redirect to // a page with an element of #failure, otherwise, we'll see the // element for #success. await browser.waitForElementByCss(`#navigate-${method}`).elementById(`navigate-${method}`).click() expect(await browser.waitForElementByCss('#success', 3000).text()).toBe('Success') } finally { await browser.close() } }, ) }) describe('next/router', () => { it('should support router.back and router.forward', async () => { const browser = await webdriver(next.url, '/back-forward/1') const firstMessage = 'Hello from 1' const secondMessage = 'Hello from 2' expect(await browser.elementByCss('#message-1').text()).toBe(firstMessage) try { const message2 = await browser .waitForElementByCss('#to-other-page') .click() .waitForElementByCss('#message-2') .text() expect(message2).toBe(secondMessage) const message1 = await browser .waitForElementByCss('#back-button') .click() .waitForElementByCss('#message-1') .text() expect(message1).toBe(firstMessage) const message2Again = await browser .waitForElementByCss('#forward-button') .click() .waitForElementByCss('#message-2') .text() expect(message2Again).toBe(secondMessage) } finally { await browser.close() } }) }) describe('hooks', () => { describe('cookies function', () => { it('should retrieve cookies in a server component', async () => { const browser = await webdriver(next.url, '/hooks/use-cookies') try { await browser.waitForElementByCss('#does-not-have-cookie') browser.addCookie({ name: 'use-cookies', value: 'value' }) browser.refresh() await browser.waitForElementByCss('#has-cookie') browser.deleteCookies() browser.refresh() await browser.waitForElementByCss('#does-not-have-cookie') } finally { await browser.close() } }) it('should retrieve cookies in a server component in the edge runtime', async () => { const res = await fetchViaHTTP(next.url, '/edge-apis/cookies') expect(await res.text()).toInclude('Hello') }) it('should access cookies on <Link /> navigation', async () => { const browser = await webdriver(next.url, '/navigation') try { // Click the cookies link to verify it can't see the cookie that's // not there. await browser.elementById('use-cookies').click() await browser.waitForElementByCss('#does-not-have-cookie') // Go back and add the cookies. await browser.back() await browser.waitForElementByCss('#from-navigation') browser.addCookie({ name: 'use-cookies', value: 'value' }) // Click the cookies link again to see that the cookie can be picked // up again. await browser.elementById('use-cookies').click() await browser.waitForElementByCss('#has-cookie') // Go back and remove the cookies. await browser.back() await browser.waitForElementByCss('#from-navigation') browser.deleteCookies() // Verify for the last time that after clicking the cookie link // again, there are no cookies. await browser.elementById('use-cookies').click() await browser.waitForElementByCss('#does-not-have-cookie') } finally { await browser.close() } }) }) describe('headers function', () => { it('should have access to incoming headers in a server component', async () => { // Check to see that we can't see the header when it's not present. let html = await renderViaHTTP(next.url, '/hooks/use-headers', {}, { headers: {} }) let $ = cheerio.load(html) expect($('#does-not-have-header').length).toBe(1) expect($('#has-header').length).toBe(0) // Check to see that we can see the header when it's present. html = await renderViaHTTP(next.url, '/hooks/use-headers', {}, { headers: { 'x-use-headers': 'value' } }) $ = cheerio.load(html) expect($('#has-header').length).toBe(1) expect($('#does-not-have-header').length).toBe(0) }) it('should access headers on <Link /> navigation', async () => { const browser = await webdriver(next.url, '/navigation') try { await browser.elementById('use-headers').click() await browser.waitForElementByCss('#has-referer') } finally { await browser.close() } }) }) describe('previewData function', () => { it('should return no preview data when there is none', async () => { const browser = await webdriver(next.url, '/hooks/use-preview-data') try { await browser.waitForElementByCss('#does-not-have-preview-data') } finally { await browser.close() } }) it('should return preview data when there is some', async () => { const browser = await webdriver(next.url, '/api/preview') try { await browser.loadPage(next.url + '/hooks/use-preview-data', { disableCache: false, beforePageLoad: null, }) await browser.waitForElementByCss('#has-preview-data') } finally { await browser.close() } }) }) describe('useRouter', () => { // TODO-APP: should enable when implemented it.skip('should throw an error when imported', async () => { const res = await fetchViaHTTP(next.url, '/hooks/use-router/server') expect(res.status).toBe(500) expect(await res.text()).toContain('Internal Server Error') }) }) describe('useParams', () => { // TODO-APP: should enable when implemented it.skip('should throw an error when imported', async () => { const res = await fetchViaHTTP(next.url, '/hooks/use-params/server') expect(res.status).toBe(500) expect(await res.text()).toContain('Internal Server Error') }) }) describe('useSearchParams', () => { // TODO-APP: should enable when implemented it.skip('should throw an error when imported', async () => { const res = await fetchViaHTTP(next.url, '/hooks/use-search-params/server') expect(res.status).toBe(500) expect(await res.text()).toContain('Internal Server Error') }) }) describe('usePathname', () => { // TODO-APP: should enable when implemented it.skip('should throw an error when imported', async () => { const res = await fetchViaHTTP(next.url, '/hooks/use-pathname/server') expect(res.status).toBe(500) expect(await res.text()).toContain('Internal Server Error') }) }) describe('useLayoutSegments', () => { // TODO-APP: should enable when implemented it.skip('should throw an error when imported', async () => { const res = await fetchViaHTTP(next.url, '/hooks/use-layout-segments/server') expect(res.status).toBe(500) expect(await res.text()).toContain('Internal Server Error') }) }) describe('useSelectedLayoutSegment', () => { // TODO-APP: should enable when implemented it.skip('should throw an error when imported', async () => { const res = await fetchViaHTTP(next.url, '/hooks/use-selected-layout-segment/server') expect(res.status).toBe(500) expect(await res.text()).toContain('Internal Server Error') }) }) }) }) describe('client components', () => { describe('hooks', () => { describe('from pages', () => { it.each([ { pathname: '/adapter-hooks/static' }, { pathname: '/adapter-hooks/1' }, { pathname: '/adapter-hooks/2' }, { pathname: '/adapter-hooks/1/account' }, { pathname: '/adapter-hooks/static', keyValue: 'value' }, { pathname: '/adapter-hooks/1', keyValue: 'value' }, { pathname: '/adapter-hooks/2', keyValue: 'value' }, { pathname: '/adapter-hooks/1/account', keyValue: 'value' }, ])('should have the correct hooks', async ({ pathname, keyValue = '' }) => { const browser = await webdriver(next.url, pathname + (keyValue ? `?key=${keyValue}` : '')) try { await browser.waitForElementByCss('#router-ready') expect(await browser.elementById('key-value').text()).toBe(keyValue) expect(await browser.elementById('pathname').text()).toBe(pathname) await browser.elementByCss('button').click() await browser.waitForElementByCss('#pushed') } finally { await browser.close() } }) }) describe('usePathname', () => { it('should have the correct pathname', async () => { const html = await renderViaHTTP(next.url, '/hooks/use-pathname') const $ = cheerio.load(html) expect($('#pathname').attr('data-pathname')).toBe('/hooks/use-pathname') }) it('should have the canonical url pathname on rewrite', async () => { const html = await renderViaHTTP(next.url, '/rewritten-use-pathname') const $ = cheerio.load(html) expect($('#pathname').attr('data-pathname')).toBe('/rewritten-use-pathname') }) }) describe('useSearchParams', () => { it('should have the correct search params', async () => { const html = await renderViaHTTP( next.url, '/hooks/use-search-params?first=value&second=other%20value&third', ) const $ = cheerio.load(html) expect($('#params-first').text()).toBe('value') expect($('#params-second').text()).toBe('other value') expect($('#params-third').text()).toBe('') expect($('#params-not-real').text()).toBe('N/A') }) // TODO-APP: correct this behavior when deployed if (!(global as any).isNextDeploy) { it('should have the canonical url search params on rewrite', async () => { const html = await renderViaHTTP(next.url, '/rewritten-use-search-params?first=a&second=b&third=c') const $ = cheerio.load(html) expect($('#params-first').text()).toBe('a') expect($('#params-second').text()).toBe('b') expect($('#params-third').text()).toBe('c') expect($('#params-not-real').text()).toBe('N/A') }) } }) describe('useRouter', () => { it('should allow access to the router', async () => { const browser = await webdriver(next.url, '/hooks/use-router') try { // Wait for the page to load, click the button (which uses a method // on the router) and then wait for the correct page to load. await browser.waitForElementByCss('#router') await browser.elementById('button-push').click() await browser.waitForElementByCss('#router-sub-page') // Go back (confirming we did do a hard push), and wait for the // correct previous page. await browser.back() await browser.waitForElementByCss('#router') } finally { await browser.close() } }) if (!(global as any).isNextDeploy) { it('should have consistent query and params handling', async () => { const html = await renderViaHTTP(next.url, '/param-and-query/params?slug=query') const $ = cheerio.load(html) const el = $('#params-and-query') expect(el.attr('data-params')).toBe('params') expect(el.attr('data-query')).toBe('query') }) } }) describe('useSelectedLayoutSegments', () => { it.each` path | outerLayout | innerLayout ${'/hooks/use-selected-layout-segment/first'} | ${['first']} | ${[]} ${'/hooks/use-selected-layout-segment/first/slug1'} | ${['first', 'slug1']} | ${['slug1']} ${'/hooks/use-selected-layout-segment/first/slug2/second'} | ${['first', 'slug2', '(group)', 'second']} | ${['slug2', '(group)', 'second']} ${'/hooks/use-selected-layout-segment/first/slug2/second/a/b'} | ${['first', 'slug2', '(group)', 'second', 'a/b']} | ${['slug2', '(group)', 'second', 'a/b']} ${'/hooks/use-selected-layout-segment/rewritten'} | ${['first', 'slug3', '(group)', 'second', 'catch/all']} | ${['slug3', '(group)', 'second', 'catch/all']} ${'/hooks/use-selected-layout-segment/rewritten-middleware'} | ${['first', 'slug3', '(group)', 'second', 'catch/all']} | ${['slug3', '(group)', 'second', 'catch/all']} `('should have the correct layout segments at $path', async ({ path, outerLayout, innerLayout }) => { const html = await renderViaHTTP(next.url, path) const $ = cheerio.load(html) expect(JSON.parse($('#outer-layout').text())).toEqual(outerLayout) expect(JSON.parse($('#inner-layout').text())).toEqual(innerLayout) }) it('should return an empty array in pages', async () => { const html = await renderViaHTTP(next.url, '/hooks/use-selected-layout-segment/first/slug2/second/a/b') const $ = cheerio.load(html) expect(JSON.parse($('#page-layout-segments').text())).toEqual([]) }) }) describe('useSelectedLayoutSegment', () => { it.each` path | outerLayout | innerLayout ${'/hooks/use-selected-layout-segment/first'} | ${'first'} | ${null} ${'/hooks/use-selected-layout-segment/first/slug1'} | ${'first'} | ${'slug1'} ${'/hooks/use-selected-layout-segment/first/slug2/second/a/b'} | ${'first'} | ${'slug2'} `('should have the correct layout segment at $path', async ({ path, outerLayout, innerLayout }) => { const html = await renderViaHTTP(next.url, path)