From 66af3cee556945f29cdf5178430d3827282cda66 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 27 Jan 2022 18:24:50 +0100 Subject: [PATCH 1/5] Improve tests for streaming and server components (#33740) Reorganizes the existing tests as they're getting longer. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- .../test/index.test.js | 210 ++---------------- .../test/rsc.js | 75 +++++++ .../test/streaming.js | 107 +++++++++ 3 files changed, 203 insertions(+), 189 deletions(-) create mode 100644 test/integration/react-streaming-and-server-components/test/rsc.js create mode 100644 test/integration/react-streaming-and-server-components/test/streaming.js diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index a298254caaff563..09c789141fc3357 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -1,6 +1,5 @@ /* eslint-env jest */ -import cheerio from 'cheerio' import { join } from 'path' import fs from 'fs-extra' import webdriver from 'next-webdriver' @@ -14,10 +13,11 @@ import { nextBuild as _nextBuild, nextStart as _nextStart, renderViaHTTP, - check, } from 'next-test-utils' import css from './css' +import rsc from './rsc' +import streaming from './streaming' const nodeArgs = ['-r', join(__dirname, '../../react-18/test/require-hook.js')] const appDir = join(__dirname, '../app') @@ -123,20 +123,6 @@ async function nextDev(dir, port) { }) } -async function resolveStreamResponse(response, onData) { - let result = '' - onData = onData || (() => {}) - await new Promise((resolve) => { - response.body.on('data', (chunk) => { - result += chunk.toString() - onData(chunk.toString(), result) - }) - - response.body.on('end', resolve) - }) - return result -} - describe('concurrentFeatures - basic', () => { it('should warn user for experimental risk with server components', async () => { const edgeRuntimeWarning = @@ -308,197 +294,43 @@ runSuite('document', 'dev', documentSuite) runSuite('document', 'prod', documentSuite) async function runBasicTests(context, env) { - const isDev = env === 'dev' - it('should render html correctly', async () => { - const homeHTML = await renderViaHTTP(context.appPort, '/', null, { - headers: { - 'x-next-test-client': 'test-util', - }, - }) - - // should have only 1 DOCTYPE - expect(homeHTML).toMatch(/^ { const path500HTML = await renderViaHTTP(context.appPort, '/err') - const pathNotFoundHTML = await renderViaHTTP( - context.appPort, - '/this-is-not-found' - ) - - const page404Content = 'custom-404-page' - - expect(homeHTML).toContain('component:index.server') - expect(homeHTML).toContain('env:env_var_test') - expect(homeHTML).toContain('header:test-util') - expect(homeHTML).toContain('path:/') - expect(homeHTML).toContain('foo.client') - - expect(dynamicRouteHTML1).toContain('query: dynamic1') - expect(dynamicRouteHTML2).toContain('query: dynamic2') - - const $404 = cheerio.load(path404HTML) - expect($404('#__next').text()).toBe(page404Content) - // In dev mode: it should show the error popup. + // In dev mode it should show the error popup. + const isDev = env === 'dev' expect(path500HTML).toContain(isDev ? 'Error: oops' : 'custom-500-page') - expect(pathNotFoundHTML).toContain(page404Content) }) - it('should disable cache for fizz pages', async () => { - const urls = ['/', '/next-api/image', '/next-api/link'] - await Promise.all( - urls.map(async (url) => { - const { headers } = await fetchViaHTTP(context.appPort, url) - expect(headers.get('cache-control')).toBe( - 'no-cache, no-store, max-age=0, must-revalidate' - ) - }) - ) - }) - - it('should support next/link', async () => { - const linkHTML = await renderViaHTTP(context.appPort, '/next-api/link') - const $ = cheerio.load(linkHTML) - const linkText = $('div[hidden] > a[href="/"]').text() - - expect(linkText).toContain('go home') - - const browser = await webdriver(context.appPort, '/next-api/link') - - // We need to make sure the app is fully hydrated before clicking, otherwise - // it will be a full redirection instead of being taken over by the next - // router. This timeout prevents it being flaky caused by fast refresh's - // rebuilding event. - await new Promise((res) => setTimeout(res, 1000)) - await browser.eval('window.beforeNav = 1') - - await browser.waitForElementByCss('#next_id').click() - await check(() => browser.elementByCss('#query').text(), 'query:1') - - await browser.waitForElementByCss('#next_id').click() - await check(() => browser.elementByCss('#query').text(), 'query:2') - - expect(await browser.eval('window.beforeNav')).toBe(1) - }) - - it('should suspense next/image on server side', async () => { - const imageHTML = await renderViaHTTP(context.appPort, '/next-api/image') - const $ = cheerio.load(imageHTML) - const imageTag = $('div[hidden] > span > span > img') + it('should render 404 error correctly', async () => { + const path404HTML = await renderViaHTTP(context.appPort, '/404') + const pathNotFoundHTML = await renderViaHTTP(context.appPort, '/not-found') - expect(imageTag.attr('src')).toContain('data:image') + expect(path404HTML).toContain('custom-404-page') + expect(pathNotFoundHTML).toContain('custom-404-page') }) - it('should handle multiple named exports correctly', async () => { - const clientExportsHTML = await renderViaHTTP( + it('should render dynamic routes correctly', async () => { + const dynamicRoute1HTML = await renderViaHTTP( context.appPort, - '/client-exports' - ) - const $clientExports = cheerio.load(clientExportsHTML) - expect($clientExports('div[hidden] > div').text()).toBe('abcde') - - const browser = await webdriver(context.appPort, '/client-exports') - const text = await browser.waitForElementByCss('#__next').text() - expect(text).toBe('abcde') - }) - - it('should support multi-level server component imports', async () => { - const html = await renderViaHTTP(context.appPort, '/multi') - expect(html).toContain('bar.server.js:') - expect(html).toContain('foo.client') - }) - - it('should support streaming', async () => { - await fetchViaHTTP(context.appPort, '/streaming', null, {}).then( - async (response) => { - let gotFallback = false - let gotData = false - - await resolveStreamResponse(response, (_, result) => { - gotData = result.includes('next_streaming_data') - if (!gotFallback) { - gotFallback = result.includes('next_streaming_fallback') - if (gotFallback) { - expect(gotData).toBe(false) - } - } - }) - - expect(gotFallback).toBe(true) - expect(gotData).toBe(true) - } - ) - - // Should end up with "next_streaming_data". - const browser = await webdriver(context.appPort, '/streaming') - const content = await browser.eval(`window.document.body.innerText`) - expect(content).toMatchInlineSnapshot('"next_streaming_data"') - }) - - it('should support streaming flight request', async () => { - await fetchViaHTTP(context.appPort, '/?__flight__=1').then( - async (response) => { - const result = await resolveStreamResponse(response) - expect(result).toContain('component:index.server') - } + '/routes/dynamic1' ) - }) - - it('should support partial hydration with inlined server data', async () => { - await fetchViaHTTP(context.appPort, '/partial-hydration', null, {}).then( - async (response) => { - let gotFallback = false - let gotData = false - let gotInlinedData = false - - await resolveStreamResponse(response, (_, result) => { - gotInlinedData = result.includes('self.__next_s=') - gotData = result.includes('next_streaming_data') - if (!gotFallback) { - gotFallback = result.includes('next_streaming_fallback') - if (gotFallback) { - expect(gotData).toBe(false) - expect(gotInlinedData).toBe(false) - } - } - }) - - expect(gotFallback).toBe(true) - expect(gotData).toBe(true) - expect(gotInlinedData).toBe(true) - } + const dynamicRoute2HTML = await renderViaHTTP( + context.appPort, + '/routes/dynamic2' ) - // Should end up with "next_streaming_data". - const browser = await webdriver(context.appPort, '/partial-hydration') - const content = await browser.eval(`window.document.body.innerText`) - expect(content).toContain('next_streaming_data') - - // Should support partial hydration: the boundary should still be pending - // while another part is hydrated already. - expect(await browser.eval(`window.partial_hydration_suspense_result`)).toBe( - 'next_streaming_fallback' - ) - expect(await browser.eval(`window.partial_hydration_counter_result`)).toBe( - 'count: 1' - ) + expect(dynamicRoute1HTML).toContain('query: dynamic1') + expect(dynamicRoute2HTML).toContain('query: dynamic2') }) it('should support api routes', async () => { const res = await renderViaHTTP(context.appPort, '/api/ping') expect(res).toContain('pong') }) + + rsc(context) + streaming(context) } function runSuite(suiteName, env, { runTests, before, after }) { diff --git a/test/integration/react-streaming-and-server-components/test/rsc.js b/test/integration/react-streaming-and-server-components/test/rsc.js new file mode 100644 index 000000000000000..619c37c89b9e03e --- /dev/null +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -0,0 +1,75 @@ +/* eslint-env jest */ +import webdriver from 'next-webdriver' +import cheerio from 'cheerio' +import { renderViaHTTP, check } from 'next-test-utils' + +export default function (context) { + it('should render server components correctly', async () => { + const homeHTML = await renderViaHTTP(context.appPort, '/', null, { + headers: { + 'x-next-test-client': 'test-util', + }, + }) + + // should have only 1 DOCTYPE + expect(homeHTML).toMatch(/^ { + const html = await renderViaHTTP(context.appPort, '/multi') + expect(html).toContain('bar.server.js:') + expect(html).toContain('foo.client') + }) + + it('should support next/link in server components', async () => { + const linkHTML = await renderViaHTTP(context.appPort, '/next-api/link') + const $ = cheerio.load(linkHTML) + const linkText = $('div[hidden] > a[href="/"]').text() + + expect(linkText).toContain('go home') + + const browser = await webdriver(context.appPort, '/next-api/link') + + // We need to make sure the app is fully hydrated before clicking, otherwise + // it will be a full redirection instead of being taken over by the next + // router. This timeout prevents it being flaky caused by fast refresh's + // rebuilding event. + await new Promise((res) => setTimeout(res, 1000)) + await browser.eval('window.beforeNav = 1') + + await browser.waitForElementByCss('#next_id').click() + await check(() => browser.elementByCss('#query').text(), 'query:1') + + await browser.waitForElementByCss('#next_id').click() + await check(() => browser.elementByCss('#query').text(), 'query:2') + + expect(await browser.eval('window.beforeNav')).toBe(1) + }) + + it('should suspense next/image in server components', async () => { + const imageHTML = await renderViaHTTP(context.appPort, '/next-api/image') + const $ = cheerio.load(imageHTML) + const imageTag = $('div[hidden] > span > span > img') + + expect(imageTag.attr('src')).toContain('data:image') + }) + + it('should handle multiple named exports correctly', async () => { + const clientExportsHTML = await renderViaHTTP( + context.appPort, + '/client-exports' + ) + const $clientExports = cheerio.load(clientExportsHTML) + expect($clientExports('div[hidden] > div').text()).toBe('abcde') + + const browser = await webdriver(context.appPort, '/client-exports') + const text = await browser.waitForElementByCss('#__next').text() + expect(text).toBe('abcde') + }) +} diff --git a/test/integration/react-streaming-and-server-components/test/streaming.js b/test/integration/react-streaming-and-server-components/test/streaming.js new file mode 100644 index 000000000000000..7748400b92b8289 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/test/streaming.js @@ -0,0 +1,107 @@ +/* eslint-env jest */ +import webdriver from 'next-webdriver' +import { fetchViaHTTP } from 'next-test-utils' + +async function resolveStreamResponse(response, onData) { + let result = '' + onData = onData || (() => {}) + await new Promise((resolve) => { + response.body.on('data', (chunk) => { + result += chunk.toString() + onData(chunk.toString(), result) + }) + + response.body.on('end', resolve) + }) + return result +} + +export default function (context) { + it('should disable cache for fizz pages', async () => { + const urls = ['/', '/next-api/image', '/next-api/link'] + await Promise.all( + urls.map(async (url) => { + const { headers } = await fetchViaHTTP(context.appPort, url) + expect(headers.get('cache-control')).toBe( + 'no-cache, no-store, max-age=0, must-revalidate' + ) + }) + ) + }) + + it('should support streaming for fizz response', async () => { + await fetchViaHTTP(context.appPort, '/streaming', null, {}).then( + async (response) => { + let gotFallback = false + let gotData = false + + await resolveStreamResponse(response, (_, result) => { + gotData = result.includes('next_streaming_data') + if (!gotFallback) { + gotFallback = result.includes('next_streaming_fallback') + if (gotFallback) { + expect(gotData).toBe(false) + } + } + }) + + expect(gotFallback).toBe(true) + expect(gotData).toBe(true) + } + ) + + // Should end up with "next_streaming_data". + const browser = await webdriver(context.appPort, '/streaming') + const content = await browser.eval(`window.document.body.innerText`) + expect(content).toMatchInlineSnapshot('"next_streaming_data"') + }) + + it('should support streaming for flight response', async () => { + await fetchViaHTTP(context.appPort, '/?__flight__=1').then( + async (response) => { + const result = await resolveStreamResponse(response) + expect(result).toContain('component:index.server') + } + ) + }) + + it('should support partial hydration with inlined server data', async () => { + await fetchViaHTTP(context.appPort, '/partial-hydration', null, {}).then( + async (response) => { + let gotFallback = false + let gotData = false + let gotInlinedData = false + + await resolveStreamResponse(response, (_, result) => { + gotInlinedData = result.includes('self.__next_s=') + gotData = result.includes('next_streaming_data') + if (!gotFallback) { + gotFallback = result.includes('next_streaming_fallback') + if (gotFallback) { + expect(gotData).toBe(false) + expect(gotInlinedData).toBe(false) + } + } + }) + + expect(gotFallback).toBe(true) + expect(gotData).toBe(true) + expect(gotInlinedData).toBe(true) + } + ) + + // Should end up with "next_streaming_data". + const browser = await webdriver(context.appPort, '/partial-hydration') + const content = await browser.eval(`window.document.body.innerText`) + expect(content).toContain('next_streaming_data') + + // Should support partial hydration: the boundary should still be pending + // while another part is hydrated already. + expect(await browser.eval(`window.partial_hydration_suspense_result`)).toBe( + 'next_streaming_fallback' + ) + expect(await browser.eval(`window.partial_hydration_counter_result`)).toBe( + 'count: 1' + ) + }) +} From 33784f1342487007619ddb02ccef3167d5cd8df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Kr=C3=A1tk=C3=BD?= Date: Thu, 27 Jan 2022 09:30:52 -0800 Subject: [PATCH 2/5] Fix `with-docker` example dockerfile (#33695) Fixed `dockerfile` in `with-docker` example ## Bug - error when executing `docker build -t nextjs-docker .` - `yarn.lock` file is missing ## Documentation / Examples - edited to not fail when `package-lock.json` or `yarn.lock` are not found - example: use `with-docker` example - [x] Make sure the linting passes by running yarn lint Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com> --- examples/with-docker/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/with-docker/Dockerfile b/examples/with-docker/Dockerfile index aa4b329d5e39965..57635340bb16f2a 100644 --- a/examples/with-docker/Dockerfile +++ b/examples/with-docker/Dockerfile @@ -6,6 +6,10 @@ WORKDIR /app COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile +# If using npm with a `package-lock.json` comment out above and use below instead +# COPY package.json package-lock.json / +# RUN npm install + # Rebuild the source code only when needed FROM node:16-alpine AS builder WORKDIR /app From 662fa6362a07caa9185e999983d42715281a61aa Mon Sep 17 00:00:00 2001 From: Pieter Bogaerts Date: Thu, 27 Jan 2022 18:59:42 +0100 Subject: [PATCH 3/5] fix: fixes #33314 move is-plain-object for es5 compilation (#33690) ## Bug - [ ] Related issues linked using `fixes #33314` #33314 - [ ] Moved the `is-plain-object` file to the shared directory since it's emitted to the client and thus needs to be transpiled. This is just my 2nd PR so if I'm missing something please let me know. --- packages/next/lib/is-error.ts | 2 +- packages/next/lib/is-serializable-props.ts | 5 ++++- packages/next/{ => shared}/lib/is-plain-object.ts | 0 3 files changed, 5 insertions(+), 2 deletions(-) rename packages/next/{ => shared}/lib/is-plain-object.ts (100%) diff --git a/packages/next/lib/is-error.ts b/packages/next/lib/is-error.ts index 4560fa7e928e938..31ba3e4a47a5de7 100644 --- a/packages/next/lib/is-error.ts +++ b/packages/next/lib/is-error.ts @@ -1,4 +1,4 @@ -import { isPlainObject } from './is-plain-object' +import { isPlainObject } from '../shared/lib/is-plain-object' // We allow some additional attached properties for Errors export interface NextError extends Error { diff --git a/packages/next/lib/is-serializable-props.ts b/packages/next/lib/is-serializable-props.ts index 1202d4d98882d2d..e42407efc85f4fa 100644 --- a/packages/next/lib/is-serializable-props.ts +++ b/packages/next/lib/is-serializable-props.ts @@ -1,4 +1,7 @@ -import { isPlainObject, getObjectClassLabel } from './is-plain-object' +import { + isPlainObject, + getObjectClassLabel, +} from '../shared/lib/is-plain-object' const regexpPlainIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/ diff --git a/packages/next/lib/is-plain-object.ts b/packages/next/shared/lib/is-plain-object.ts similarity index 100% rename from packages/next/lib/is-plain-object.ts rename to packages/next/shared/lib/is-plain-object.ts From 5f96a0b1d7fc89c7863a675a0e89d499830c7f14 Mon Sep 17 00:00:00 2001 From: Irene Oppo Date: Thu, 27 Jan 2022 19:06:38 +0100 Subject: [PATCH 4/5] Added next.config.js with datocms-assets domain in allow list (#33647) ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [v] Make sure the linting passes by running `yarn lint` Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com> --- examples/cms-datocms/next.config.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 examples/cms-datocms/next.config.js diff --git a/examples/cms-datocms/next.config.js b/examples/cms-datocms/next.config.js new file mode 100644 index 000000000000000..761cfcc4d39b227 --- /dev/null +++ b/examples/cms-datocms/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + images: { + domains: ['www.datocms-assets.com'], + }, +} From eea3adc53d217523ca2595b6403f832d060ed2d1 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 27 Jan 2022 20:51:36 +0100 Subject: [PATCH 5/5] fix rsc test suite runner (#33745) --- .../test/index.test.js | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index 09c789141fc3357..18a5258fe7b5f22 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -221,8 +221,8 @@ const customAppPageSuite = { expect(indexFlight).toContain('container-server') }) }, - before: () => appServerPage.write(rscAppPage), - after: () => appServerPage.delete(), + beforeAll: () => appServerPage.write(rscAppPage), + afterAll: () => appServerPage.delete(), } runSuite('Custom App', 'dev', customAppPageSuite) @@ -267,8 +267,8 @@ describe('concurrentFeatures - dev', () => { const cssSuite = { runTests: css, - before: () => appPage.write(appWithGlobalCss), - after: () => appPage.delete(), + beforeAll: () => appPage.write(appWithGlobalCss), + afterAll: () => appPage.delete(), } runSuite('CSS', 'dev', cssSuite) @@ -286,8 +286,8 @@ const documentSuite = { ) }) }, - before: () => documentPage.write(documentWithGip), - after: () => documentPage.delete(), + beforeAll: () => documentPage.write(documentWithGip), + afterAll: () => documentPage.delete(), } runSuite('document', 'dev', documentSuite) @@ -333,28 +333,29 @@ async function runBasicTests(context, env) { streaming(context) } -function runSuite(suiteName, env, { runTests, before, after }) { +function runSuite(suiteName, env, options) { const context = { appDir } describe(`${suiteName} ${env}`, () => { if (env === 'prod') { beforeAll(async () => { - before?.() + options.beforeAll?.() context.appPort = await findPort() - context.server = await nextDev(context.appDir, context.appPort) + await nextBuild(context.appDir) + context.server = await nextStart(context.appDir, context.appPort) }) } if (env === 'dev') { beforeAll(async () => { - before?.() + options.beforeAll?.() context.appPort = await findPort() context.server = await nextDev(context.appDir, context.appPort) }) } afterAll(async () => { - after?.() + options.afterAll?.() await killApp(context.server) }) - runTests(context, env) + options.runTests(context, env) }) }