diff --git a/test/development/pages-dir/client-navigation/fixture/next.config.js b/test/development/pages-dir/client-navigation/fixture/next.config.js index 86393e8dbb8ad..ddfd8e59257c1 100644 --- a/test/development/pages-dir/client-navigation/fixture/next.config.js +++ b/test/development/pages-dir/client-navigation/fixture/next.config.js @@ -4,6 +4,6 @@ module.exports = { maxInactiveAge: 1000 * 60 * 60, }, experimental: { - strictNextHead: true, + strictNextHead: process.env.TEST_STRICT_NEXT_HEAD !== 'false', }, } diff --git a/test/development/pages-dir/client-navigation/fixture/pages/head.js b/test/development/pages-dir/client-navigation/fixture/pages/head.js index a143355259fa2..579b4312c9b13 100644 --- a/test/development/pages-dir/client-navigation/fixture/pages/head.js +++ b/test/development/pages-dir/client-navigation/fixture/pages/head.js @@ -1,145 +1,165 @@ import React from 'react' import Head from 'next/head' -export default () => ( -
- - {/* this will not render */} - - {/* this will get rendered */} - +export default function HeadPage() { + const [shouldReverseScriptOrder, reverseScriptOrder] = React.useReducer( + (b) => !b, + false + ) + const [shouldInsertScript, toggleScript] = React.useReducer((b) => !b, false) - {/* this will not render */} - - {/* this will override the default */} - + const scriptAsyncTrue = + const scriptAsyncFalse = ( + + ) - + return ( +
+ + {/* this will not render */} + + {/* this will get rendered */} + - {/* this will not render the content prop */} - + {/* this will not render */} + + {/* this will override the default */} + - {/* allow duplicates for specific tags */} - - - - - - - - - - - - - - - - - - + - - + {/* this will not render the content prop */} + - {/* both meta tags will be rendered since they use unique keys */} - - + {/* allow duplicates for specific tags */} + + + + + + + + + + + + + + + + + + - - Fragment title - - + + - {/* the following 2 link tags will both be rendered */} - - + {/* both meta tags will be rendered since they use unique keys */} + + - {/* only one tag will be rendered as they have the same key */} - - + + Fragment title + + - {/* this should not execute twice on the client */} - - {/* this should have async set to false on the client */} - - {/* this should not execute twice on the client (intentionally sets defer to `yas` to test boolean coercion) */} - + {/* the following 2 link tags will both be rendered */} + + - {/* such style can be used for alternate links on _app vs individual pages */} - {['pl', 'en'].map((language) => ( - - ))} - {['pl', 'en'].map((language) => ( - - ))} - -

I can have meta tags

-
-) + {/* only one tag will be rendered as they have the same key */} + + + + {shouldInsertScript ? scriptAsyncTrue : null} + {/* this should not execute twice on the client */} + {shouldReverseScriptOrder ? scriptAsyncTrue : scriptAsyncFalse} + {/* this should have async set to false on the client */} + {shouldReverseScriptOrder ? scriptAsyncFalse : scriptAsyncTrue} + {/* this should not execute twice on the client (intentionally sets defer to `yas` to test boolean coercion) */} + + + {/* such style can be used for alternate links on _app vs individual pages */} + {['pl', 'en'].map((language) => ( + + ))} + {['pl', 'en'].map((language) => ( + + ))} + +

I can have meta tags

+ + +
+ ) +} diff --git a/test/development/pages-dir/client-navigation/index.test.ts b/test/development/pages-dir/client-navigation/index.test.ts index 3e4bae1fbedd9..d684bffa3a615 100644 --- a/test/development/pages-dir/client-navigation/index.test.ts +++ b/test/development/pages-dir/client-navigation/index.test.ts @@ -5,18 +5,19 @@ import { getRedboxSource, hasRedbox, getRedboxHeader, - renderViaHTTP, waitFor, check, } from 'next-test-utils' import webdriver from 'next-webdriver' import path from 'path' -import renderingSuite from './rendering' import { nextTestSetup } from 'e2e-utils' describe('Client Navigation', () => { const { next } = nextTestSetup({ files: path.join(__dirname, 'fixture'), + env: { + TEST_STRICT_NEXT_HEAD: String(true), + }, }) it('should not reload when visiting /_error directly', async () => { @@ -1469,7 +1470,199 @@ describe('Client Navigation', () => { }) }) - describe('updating head while client routing', () => { + describe('foreign history manipulation', () => { + it('should ignore history state without options', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/nav') + // push history object without options + await browser.eval( + 'window.history.pushState({ url: "/whatever" }, "", "/whatever")' + ) + await browser.elementByCss('#about-link').click() + await browser.waitForElementByCss('.nav-about') + await browser.back() + await waitFor(1000) + expect(await hasRedbox(browser)).toBe(false) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should ignore history state with an invalid url', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/nav') + // push history object wit invalid url (not relative) + await browser.eval( + 'window.history.pushState({ url: "http://google.com" }, "", "/whatever")' + ) + await browser.elementByCss('#about-link').click() + await browser.waitForElementByCss('.nav-about') + await browser.back() + await waitFor(1000) + expect(await hasRedbox(browser)).toBe(false) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should ignore foreign history state with missing properties', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/nav') + // push empty history state + await browser.eval('window.history.pushState({}, "", "/whatever")') + await browser.elementByCss('#about-link').click() + await browser.waitForElementByCss('.nav-about') + await browser.back() + await waitFor(1000) + expect(await hasRedbox(browser)).toBe(false) + } finally { + if (browser) { + await browser.close() + } + } + }) + }) + + it('should not error on module.exports + polyfills', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/read-only-object-error') + expect(await browser.elementByCss('body').text()).toBe( + 'this is just a placeholder component' + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should work on nested /index/index.js', async () => { + const browser = await webdriver(next.appPort, '/nested-index/index') + expect(await browser.elementByCss('p').text()).toBe( + 'This is an index.js nested in an index/ folder.' + ) + await browser.close() + }) + + it('should handle undefined prop in head client-side', async () => { + const browser = await webdriver(next.appPort, '/head') + const value = await browser.eval( + `document.querySelector('meta[name="empty-content"]').hasAttribute('content')` + ) + + expect(value).toBe(false) + }) + + it.each([true, false])( + 'should handle boolean async prop in next/script client-side: %s', + async (bool) => { + const browser = await webdriver(next.appPort, '/script') + const value = await browser.eval( + `document.querySelector('script[src="/test-async-${JSON.stringify( + bool + )}.js"]').async` + ) + + expect(value).toBe(bool) + } + ) + + it('should only execute async and defer scripts with next/script once', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/script') + + await browser.waitForElementByCss('h1') + await waitFor(2000) + expect(Number(await browser.eval('window.__test_async_executions'))).toBe( + 1 + ) + expect(Number(await browser.eval('window.__test_defer_executions'))).toBe( + 1 + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should emit routeChangeError on hash change cancel', async () => { + const browser = await webdriver(next.appPort, '/') + + await browser.eval(`(function() { + window.routeErrors = [] + + window.next.router.events.on('routeChangeError', function (err) { + window.routeErrors.push(err) + }) + window.next.router.push('#first') + window.next.router.push('#second') + window.next.router.push('#third') + })()`) + + await check(async () => { + const errorCount = await browser.eval('window.routeErrors.length') + return errorCount > 0 ? 'success' : errorCount + }, 'success') + }) + + it('should navigate to paths relative to the current page', async () => { + const browser = await webdriver(next.appPort, '/nav/relative') + let page + + await browser.elementByCss('a').click() + + await browser.waitForElementByCss('#relative-1') + page = await browser.elementByCss('body').text() + expect(page).toMatch(/On relative 1/) + await browser.elementByCss('a').click() + + await browser.waitForElementByCss('#relative-2') + page = await browser.elementByCss('body').text() + expect(page).toMatch(/On relative 2/) + + await browser.elementByCss('button').click() + await browser.waitForElementByCss('#relative') + page = await browser.elementByCss('body').text() + expect(page).toMatch(/On relative index/) + + await browser.close() + }) +}) + +describe.each([[false], [true]])( + 'updating with strictNextHead=%s while client routing', + (strictNextHead) => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixture'), + env: { + TEST_STRICT_NEXT_HEAD: String(strictNextHead), + }, + }) + + it.each([true, false])( + 'should handle boolean async prop in next/head client-side: %s', + async (bool) => { + const browser = await webdriver(next.appPort, '/head') + const value = await browser.eval( + `document.querySelector('script[src="/test-async-${JSON.stringify( + bool + )}.js"]').async` + ) + + expect(value).toBe(bool) + } + ) + it('should only execute async and defer scripts once', async () => { let browser try { @@ -1483,6 +1676,20 @@ describe('Client Navigation', () => { expect( Number(await browser.eval('window.__test_defer_executions')) ).toBe(1) + + await browser.elementByCss('#reverseScriptOrder').click() + await waitFor(2000) + + expect( + Number(await browser.eval('window.__test_async_executions')) + ).toBe(1) + + await browser.elementByCss('#toggleScript').click() + await waitFor(2000) + + expect( + Number(await browser.eval('window.__test_async_executions')) + ).toBe(1) } finally { if (browser) { await browser.close() @@ -1662,194 +1869,5 @@ describe('Client Navigation', () => { } } }) - }) - - describe('foreign history manipulation', () => { - it('should ignore history state without options', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav') - // push history object without options - await browser.eval( - 'window.history.pushState({ url: "/whatever" }, "", "/whatever")' - ) - await browser.elementByCss('#about-link').click() - await browser.waitForElementByCss('.nav-about') - await browser.back() - await waitFor(1000) - expect(await hasRedbox(browser)).toBe(false) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should ignore history state with an invalid url', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav') - // push history object wit invalid url (not relative) - await browser.eval( - 'window.history.pushState({ url: "http://google.com" }, "", "/whatever")' - ) - await browser.elementByCss('#about-link').click() - await browser.waitForElementByCss('.nav-about') - await browser.back() - await waitFor(1000) - expect(await hasRedbox(browser)).toBe(false) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should ignore foreign history state with missing properties', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/nav') - // push empty history state - await browser.eval('window.history.pushState({}, "", "/whatever")') - await browser.elementByCss('#about-link').click() - await browser.waitForElementByCss('.nav-about') - await browser.back() - await waitFor(1000) - expect(await hasRedbox(browser)).toBe(false) - } finally { - if (browser) { - await browser.close() - } - } - }) - }) - - it('should not error on module.exports + polyfills', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/read-only-object-error') - expect(await browser.elementByCss('body').text()).toBe( - 'this is just a placeholder component' - ) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should work on nested /index/index.js', async () => { - const browser = await webdriver(next.appPort, '/nested-index/index') - expect(await browser.elementByCss('p').text()).toBe( - 'This is an index.js nested in an index/ folder.' - ) - await browser.close() - }) - - it('should handle undefined prop in head client-side', async () => { - const browser = await webdriver(next.appPort, '/head') - const value = await browser.eval( - `document.querySelector('meta[name="empty-content"]').hasAttribute('content')` - ) - - expect(value).toBe(false) - }) - - it.each([true, false])( - 'should handle boolean async prop in next/head client-side: %s', - async (bool) => { - const browser = await webdriver(next.appPort, '/head') - const value = await browser.eval( - `document.querySelector('script[src="/test-async-${JSON.stringify( - bool - )}.js"]').async` - ) - - expect(value).toBe(bool) - } - ) - - it.each([true, false])( - 'should handle boolean async prop in next/script client-side: %s', - async (bool) => { - const browser = await webdriver(next.appPort, '/script') - const value = await browser.eval( - `document.querySelector('script[src="/test-async-${JSON.stringify( - bool - )}.js"]').async` - ) - - expect(value).toBe(bool) - } - ) - - it('should only execute async and defer scripts with next/script once', async () => { - let browser - try { - browser = await webdriver(next.appPort, '/script') - - await browser.waitForElementByCss('h1') - await waitFor(2000) - expect(Number(await browser.eval('window.__test_async_executions'))).toBe( - 1 - ) - expect(Number(await browser.eval('window.__test_defer_executions'))).toBe( - 1 - ) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should emit routeChangeError on hash change cancel', async () => { - const browser = await webdriver(next.appPort, '/') - - await browser.eval(`(function() { - window.routeErrors = [] - - window.next.router.events.on('routeChangeError', function (err) { - window.routeErrors.push(err) - }) - window.next.router.push('#first') - window.next.router.push('#second') - window.next.router.push('#third') - })()`) - - await check(async () => { - const errorCount = await browser.eval('window.routeErrors.length') - return errorCount > 0 ? 'success' : errorCount - }, 'success') - }) - - it('should navigate to paths relative to the current page', async () => { - const browser = await webdriver(next.appPort, '/nav/relative') - let page - - await browser.elementByCss('a').click() - - await browser.waitForElementByCss('#relative-1') - page = await browser.elementByCss('body').text() - expect(page).toMatch(/On relative 1/) - await browser.elementByCss('a').click() - - await browser.waitForElementByCss('#relative-2') - page = await browser.elementByCss('body').text() - expect(page).toMatch(/On relative 2/) - - await browser.elementByCss('button').click() - await browser.waitForElementByCss('#relative') - page = await browser.elementByCss('body').text() - expect(page).toMatch(/On relative index/) - - await browser.close() - }) - - renderingSuite( - next, - (p, q) => renderViaHTTP(next.appPort, p, q), - (p, q) => fetchViaHTTP(next.appPort, p, q), - next - ) -}) + } +) diff --git a/test/development/pages-dir/client-navigation/rendering.ts b/test/development/pages-dir/client-navigation/rendering.test.ts similarity index 85% rename from test/development/pages-dir/client-navigation/rendering.ts rename to test/development/pages-dir/client-navigation/rendering.test.ts index 50b63dfc10eca..2fd1dcb096bca 100644 --- a/test/development/pages-dir/client-navigation/rendering.ts +++ b/test/development/pages-dir/client-navigation/rendering.test.ts @@ -1,13 +1,40 @@ /* eslint-env jest */ import cheerio from 'cheerio' -import { type NextInstance } from 'e2e-utils' -import { getRedboxHeader, hasRedbox } from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' +import { + fetchViaHTTP, + getRedboxHeader, + hasRedbox, + renderViaHTTP, +} from 'next-test-utils' import webdriver from 'next-webdriver' import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST } from 'next/constants' +import path from 'path' import url from 'url' -export default function (next: NextInstance, render, fetch, ctx) { +describe('Client Navigation rendering', () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixture'), + env: { + TEST_STRICT_NEXT_HEAD: String(true), + }, + }) + + function render( + pathname: Parameters[1], + query?: Parameters[2] + ) { + return renderViaHTTP(next.appPort, pathname, query) + } + + function fetch( + pathname: Parameters[1], + query?: Parameters[2] + ) { + return fetchViaHTTP(next.appPort, pathname, query) + } + async function get$(path: any, query?: any) { const html = await render(path, query) return cheerio.load(html) @@ -32,14 +59,6 @@ export default function (next: NextInstance, render, fetch, ctx) { }) }) - it('should handle undefined prop in head server-side', async () => { - const html = await render('/head') - const $ = cheerio.load(html) - const value = 'content' in $('meta[name="empty-content"]').attr() - - expect(value).toBe(false) - }) - test('renders with fragment syntax', async () => { const html = await render('/fragment-syntax') expect(html.includes('My component!')).toBeTruthy() @@ -57,150 +76,6 @@ export default function (next: NextInstance, render, fetch, ctx) { expect(html.includes('Memo component')).toBeTruthy() }) - // default-head contains an empty . - test('header renders default charset', async () => { - const html = await render('/default-head') - expect(html.includes('')).toBeTruthy() - expect(html.includes('next-head, but only once.')).toBeTruthy() - }) - - test('header renders default viewport', async () => { - const html = await render('/default-head') - expect(html).toContain( - '' - ) - }) - - test('header helper renders header information', async () => { - const html = await render('/head') - expect(html.includes('')).toBeTruthy() - expect(html.includes('')).toBeTruthy() - expect(html).toContain( - '' - ) - expect(html.includes('I can have meta tags')).toBeTruthy() - }) - - test('header helper dedupes tags', async () => { - const html = await render('/head') - expect(html).toContain('') - expect(html).not.toContain('') - expect(html).toContain( - '' - ) - // Should contain only one viewport - expect(html.match(/' - ) - expect(html).toContain('') - expect(html).toContain( - '' - ) - const dedupeLink = '' - expect(html).toContain(dedupeLink) - expect( - html.substring(html.indexOf(dedupeLink) + dedupeLink.length) - ).not.toContain('') - expect(html).toContain( - '' - ) - expect(html).not.toContain( - '' - ) - }) - - test('header helper dedupes tags with the same key as the default', async () => { - const html = await render('/head-duplicate-default-keys') - // Expect exactly one `charSet` - expect((html.match(/charSet=/g) || []).length).toBe(1) - // Expect exactly one `viewport` - expect((html.match(/name="viewport"/g) || []).length).toBe(1) - expect(html).toContain('') - expect(html).toContain('') - }) - - test('header helper avoids dedupe of specific tags', async () => { - const html = await render('/head') - expect(html).toContain('') - expect(html).toContain('') - expect(html).not.toContain('') - expect(html).toContain('') - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - expect(html).toContain('') - expect(html).toContain('') - }) - - test('header helper avoids dedupe of meta tags with the same name if they use unique keys', async () => { - const html = await render('/head') - expect(html).toContain( - '' - ) - expect(html).toContain( - '' - ) - }) - - test('header helper renders Fragment children', async () => { - const html = await render('/head') - expect(html).toContain('Fragment title') - expect(html).toContain('') - }) - - test('header helper renders boolean attributes correctly children', async () => { - const html = await render('/head') - expect(html).toContain('