mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
89d017eac4
## Summary - warn during `next dev` and `next build` when React 18 is installed, while keeping `next start` quiet - explain that React 18 remains supported in Next.js 16 but will be removed in Next.js 17 - update the React version guidance with React 19 and TypeScript type upgrade commands ## Verification - `NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo test/e2e/react-current-version/react-current-version.test.ts` - `NEXT_TEST_REACT_VERSION=18.3.1 NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo test/e2e/react-current-version/react-current-version.test.ts` - targeted production build/start coverage passed with React 18 and React 19 - Not passing: `pnpm --filter=next types` (existing unrelated errors in `httpget.ts`, `image-optimizer.ts`, `proxy-request.ts`, telemetry typings, and compiled declarations) - Not passing: `pnpm lint` (existing unused Turbo Tasks check) <!-- NEXT_JS_LLM -->
195 lines
5.8 KiB
TypeScript
195 lines
5.8 KiB
TypeScript
import { isReact18, nextTestSetup, isNextDev } from 'e2e-utils'
|
|
import { join } from 'path'
|
|
import { retry } from 'next-test-utils'
|
|
|
|
const react18DeprecationWarning =
|
|
'React 18 support is deprecated in Next.js 16 and will be removed in Next.js 17. Please upgrade to React 19. Learn more: https://nextjs.org/docs/messages/react-version'
|
|
|
|
function makeIndexPage(runtime: string) {
|
|
return `import ReactDOM from 'react-dom'
|
|
import Image from 'next/image'
|
|
|
|
export default function Index() {
|
|
if (typeof window !== 'undefined') {
|
|
window.didHydrate = true
|
|
}
|
|
console.log('__render__')
|
|
return (
|
|
<div>
|
|
<p id="react-dom-version">{ReactDOM.version}</p>
|
|
<Image
|
|
id="priority-image"
|
|
priority
|
|
src="/noop.png"
|
|
width={300}
|
|
height={400}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const config = {
|
|
runtime: ${JSON.stringify(runtime)}
|
|
}
|
|
`
|
|
}
|
|
|
|
describe('react-current-version', () => {
|
|
describe('React 18 deprecation warning', () => {
|
|
const { next, isNextDeploy, skipped } = nextTestSetup({
|
|
files: join(__dirname, 'app'),
|
|
skipStart: true,
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
if (isNextDeploy) {
|
|
it('should skip in deploy mode', () => {})
|
|
return
|
|
}
|
|
|
|
if (isNextDev) {
|
|
it('warns during next dev only when using React 18', async () => {
|
|
await next.start()
|
|
await retry(() => {
|
|
expect(next.cliOutput).toContain('Running next.config')
|
|
})
|
|
|
|
const warningCount =
|
|
next.cliOutput.split(react18DeprecationWarning).length - 1
|
|
expect(warningCount).toBe(isReact18 ? 1 : 0)
|
|
})
|
|
} else {
|
|
it('warns during next build only when using React 18 but not during next start', async () => {
|
|
const buildResult = await next.build()
|
|
const warningCount =
|
|
buildResult.cliOutput.split(react18DeprecationWarning).length - 1
|
|
expect(warningCount).toBe(isReact18 ? 1 : 0)
|
|
|
|
const outputIndex = next.cliOutput.length
|
|
await next.start()
|
|
expect(next.cliOutput.slice(outputIndex)).not.toContain(
|
|
react18DeprecationWarning
|
|
)
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('Basics', () => {
|
|
const { next, isTurbopack } = nextTestSetup({
|
|
files: join(__dirname, 'app'),
|
|
})
|
|
|
|
it('should only render once in SSR', async () => {
|
|
await next.render('/')
|
|
expect([...next.cliOutput.matchAll(/__render__/g)].length).toBe(1)
|
|
})
|
|
|
|
it('no warnings for image related link props', async () => {
|
|
await next.render('/')
|
|
expect(next.cliOutput).not.toContain('Warning: Invalid DOM property')
|
|
expect(next.cliOutput).not.toContain('Warning: React does not recognize')
|
|
})
|
|
|
|
it('hydrates correctly for normal page', async () => {
|
|
const browser = await next.browser('/')
|
|
expect(await browser.eval('window.didHydrate')).toBe(true)
|
|
expect(await browser.elementById('react-dom-version').text()).toMatch(
|
|
isReact18 ? /^18\./ : /^19\./
|
|
)
|
|
})
|
|
|
|
it('useId() values should match on hydration', async () => {
|
|
const $ = await next.render$('/use-id')
|
|
const ssrId = $('#id').text()
|
|
|
|
const browser = await next.browser('/use-id')
|
|
const csrId = await browser.eval(
|
|
'document.getElementById("id").innerText'
|
|
)
|
|
|
|
expect(ssrId).toEqual(csrId)
|
|
})
|
|
|
|
it('should contain dynamicIds in next data for dynamic imports', async () => {
|
|
async function expectToContainPreload(page: string) {
|
|
const $ = await next.render$(`/${page}`)
|
|
const { dynamicIds } = JSON.parse($('#__NEXT_DATA__').html())
|
|
|
|
if (isNextDev) {
|
|
expect(
|
|
dynamicIds.find((id: string) =>
|
|
isTurbopack
|
|
? id.endsWith(
|
|
'components/foo.js [client] (ecmascript, next/dynamic entry)'
|
|
)
|
|
: id === `pages/${page}.js -> ../components/foo`
|
|
)
|
|
).toBeTruthy()
|
|
} else {
|
|
expect(dynamicIds.length).toBe(1)
|
|
}
|
|
}
|
|
await expectToContainPreload('dynamic')
|
|
})
|
|
})
|
|
|
|
function describeConcurrentMode(runtime: string) {
|
|
describe(`Concurrent mode in the ${runtime} runtime`, () => {
|
|
const { next } = nextTestSetup({
|
|
files: join(__dirname, 'app'),
|
|
overrideFiles: {
|
|
'pages/index.js': makeIndexPage(runtime),
|
|
},
|
|
})
|
|
|
|
it('flushes styled-jsx styles as the page renders', async () => {
|
|
const html = await next.render('/use-flush-effect/styled-jsx')
|
|
const stylesOccurrence = html.match(/color:(\s)*(?:blue|#00f)/g) || []
|
|
expect(stylesOccurrence.length).toBe(1)
|
|
|
|
const browser = await next.browser('/use-flush-effect/styled-jsx')
|
|
await retry(async () => {
|
|
const text = await browser
|
|
.waitForElementByCss('style#__jsx-900f996af369fc74', {
|
|
state: 'attached',
|
|
})
|
|
.text()
|
|
expect(text).toMatch(/(?:blue|#00f)/)
|
|
})
|
|
await retry(async () => {
|
|
const text = await browser
|
|
.waitForElementByCss('style#__jsx-8b0811664c4e575e', {
|
|
state: 'attached',
|
|
})
|
|
.text()
|
|
expect(text).toMatch(/red/)
|
|
})
|
|
})
|
|
|
|
describe('<RouteAnnouncer />', () => {
|
|
it('should not have the initial route announced', async () => {
|
|
const browser = await next.browser('/')
|
|
const title = await browser
|
|
.waitForElementByCss('#__next-route-announcer__', {
|
|
state: 'attached',
|
|
})
|
|
.text()
|
|
|
|
expect(title).toBe('')
|
|
})
|
|
})
|
|
|
|
it('should not have invalid config warning', async () => {
|
|
await next.render('/')
|
|
expect(next.cliOutput).not.toContain('not exist in this version')
|
|
})
|
|
})
|
|
}
|
|
|
|
describeConcurrentMode('experimental-edge')
|
|
describeConcurrentMode('nodejs')
|
|
})
|