Fix client component hydration (#37134)
This PR makes sure that chunks of client components can be loaded via `__webpack_chunk_load__`, and hydrated correctly inside `viewsDir`. Side note: we have to get rid of `[contenthash]` from the chunk filename because of a conflict currently which can be resolved later. ## Bug - [ ] Related issues linked using `fixes #number` - [x] 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`
This commit is contained in:
parent
6736041d51
commit
2a89c1926d
8 changed files with 241 additions and 195 deletions
|
@ -108,7 +108,7 @@ import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
|
|||
import { recursiveCopy } from '../lib/recursive-copy'
|
||||
import { recursiveReadDir } from '../lib/recursive-readdir'
|
||||
import { lockfilePatchPromise, teardownTraceSubscriber } from './swc'
|
||||
import { injectedClientEntries } from './webpack/plugins/flight-manifest-plugin'
|
||||
import { injectedClientEntries } from './webpack/plugins/client-entry-plugin'
|
||||
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
|
||||
import { flatReaddir } from '../lib/flat-readdir'
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin
|
|||
import { regexLikeCss } from './webpack/config/blocks/css'
|
||||
import { CopyFilePlugin } from './webpack/plugins/copy-file-plugin'
|
||||
import { FlightManifestPlugin } from './webpack/plugins/flight-manifest-plugin'
|
||||
import { ClientEntryPlugin } from './webpack/plugins/client-entry-plugin'
|
||||
import {
|
||||
Feature,
|
||||
SWC_TARGET_TRIPLE,
|
||||
|
@ -1016,6 +1017,7 @@ export default async function getBaseWebpackConfig(
|
|||
? {
|
||||
// We have to use the names here instead of hashes to ensure the consistency between compilers.
|
||||
moduleIds: 'named',
|
||||
chunkIds: 'named',
|
||||
}
|
||||
: {}),
|
||||
splitChunks: ((): webpack.Options.SplitChunksOptions | false => {
|
||||
|
@ -1182,7 +1184,7 @@ export default async function getBaseWebpackConfig(
|
|||
? `[name].js`
|
||||
: `../[name].js`
|
||||
: `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${
|
||||
dev ? '' : '-[contenthash]'
|
||||
dev ? '' : viewsDir ? '' : '-[contenthash]'
|
||||
}.js`,
|
||||
library: isClient || isEdgeServer ? '_N_E' : undefined,
|
||||
libraryTarget: isClient || isEdgeServer ? 'assign' : 'commonjs2',
|
||||
|
@ -1641,12 +1643,16 @@ export default async function getBaseWebpackConfig(
|
|||
},
|
||||
}),
|
||||
hasServerComponents &&
|
||||
!isClient &&
|
||||
new FlightManifestPlugin({
|
||||
dev,
|
||||
pageExtensions: rawPageExtensions,
|
||||
isEdgeServer,
|
||||
}),
|
||||
(isClient
|
||||
? new FlightManifestPlugin({
|
||||
dev,
|
||||
viewsDir: !!config.experimental.viewsDir,
|
||||
pageExtensions: rawPageExtensions,
|
||||
})
|
||||
: new ClientEntryPlugin({
|
||||
dev,
|
||||
isEdgeServer,
|
||||
})),
|
||||
!dev &&
|
||||
isClient &&
|
||||
new TelemetryPlugin(
|
||||
|
|
192
packages/next/build/webpack/plugins/client-entry-plugin.ts
Normal file
192
packages/next/build/webpack/plugins/client-entry-plugin.ts
Normal file
|
@ -0,0 +1,192 @@
|
|||
import { stringify } from 'querystring'
|
||||
import { webpack } from 'next/dist/compiled/webpack/webpack'
|
||||
import {
|
||||
EDGE_RUNTIME_WEBPACK,
|
||||
NEXT_CLIENT_SSR_ENTRY_SUFFIX,
|
||||
} from '../../../shared/lib/constants'
|
||||
import { clientComponentRegex } from '../loaders/utils'
|
||||
import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path'
|
||||
import { denormalizePagePath } from '../../../shared/lib/page-path/denormalize-page-path'
|
||||
import {
|
||||
getInvalidator,
|
||||
entries,
|
||||
} from '../../../server/dev/on-demand-entry-handler'
|
||||
import { getPageStaticInfo } from '../../analysis/get-page-static-info'
|
||||
|
||||
type Options = {
|
||||
dev: boolean
|
||||
isEdgeServer: boolean
|
||||
}
|
||||
|
||||
const PLUGIN_NAME = 'ClientEntryPlugin'
|
||||
|
||||
export const injectedClientEntries = new Map()
|
||||
|
||||
export class ClientEntryPlugin {
|
||||
dev: boolean = false
|
||||
isEdgeServer: boolean
|
||||
|
||||
constructor(options: Options) {
|
||||
if (typeof options.dev === 'boolean') {
|
||||
this.dev = options.dev
|
||||
}
|
||||
this.isEdgeServer = options.isEdgeServer
|
||||
}
|
||||
|
||||
apply(compiler: any) {
|
||||
compiler.hooks.compilation.tap(
|
||||
PLUGIN_NAME,
|
||||
(compilation: any, { normalModuleFactory }: any) => {
|
||||
compilation.dependencyFactories.set(
|
||||
(webpack as any).dependencies.ModuleDependency,
|
||||
normalModuleFactory
|
||||
)
|
||||
compilation.dependencyTemplates.set(
|
||||
(webpack as any).dependencies.ModuleDependency,
|
||||
new (webpack as any).dependencies.NullDependency.Template()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// Only for webpack 5
|
||||
compiler.hooks.finishMake.tapAsync(
|
||||
PLUGIN_NAME,
|
||||
async (compilation: any, callback: any) => {
|
||||
this.createClientEndpoints(compilation, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async createClientEndpoints(compilation: any, callback: () => void) {
|
||||
const context = (this as any).context
|
||||
const promises: any = []
|
||||
|
||||
// For each SC server compilation entry, we need to create its corresponding
|
||||
// client component entry.
|
||||
for (const [name, entry] of compilation.entries.entries()) {
|
||||
// Check if the page entry is a server component or not.
|
||||
const entryDependency = entry.dependencies?.[0]
|
||||
const request = entryDependency?.request
|
||||
|
||||
if (request && entry.options?.layer === 'sc_server') {
|
||||
const visited = new Set()
|
||||
const clientComponentImports: string[] = []
|
||||
|
||||
function filterClientComponents(dependency: any) {
|
||||
const module = compilation.moduleGraph.getResolvedModule(dependency)
|
||||
if (!module) return
|
||||
|
||||
if (visited.has(module.userRequest)) return
|
||||
visited.add(module.userRequest)
|
||||
|
||||
if (clientComponentRegex.test(module.userRequest)) {
|
||||
clientComponentImports.push(module.userRequest)
|
||||
}
|
||||
|
||||
compilation.moduleGraph
|
||||
.getOutgoingConnections(module)
|
||||
.forEach((connection: any) => {
|
||||
filterClientComponents(connection.dependency)
|
||||
})
|
||||
}
|
||||
|
||||
// Traverse the module graph to find all client components.
|
||||
filterClientComponents(entryDependency)
|
||||
|
||||
const entryModule =
|
||||
compilation.moduleGraph.getResolvedModule(entryDependency)
|
||||
const routeInfo = entryModule.buildInfo.route || {
|
||||
page: denormalizePagePath(name.replace(/^pages/, '')),
|
||||
absolutePagePath: entryModule.resource,
|
||||
}
|
||||
|
||||
// Parse gSSP and gSP exports from the page source.
|
||||
const pageStaticInfo = this.isEdgeServer
|
||||
? {}
|
||||
: await getPageStaticInfo({
|
||||
pageFilePath: routeInfo.absolutePagePath,
|
||||
nextConfig: {},
|
||||
isDev: this.dev,
|
||||
})
|
||||
|
||||
const clientLoader = `next-flight-client-entry-loader?${stringify({
|
||||
modules: clientComponentImports,
|
||||
runtime: this.isEdgeServer ? 'edge' : 'nodejs',
|
||||
ssr: pageStaticInfo.ssr,
|
||||
// Adding name here to make the entry key unique.
|
||||
name,
|
||||
})}!`
|
||||
|
||||
const bundlePath = 'pages' + normalizePagePath(routeInfo.page)
|
||||
|
||||
// Inject the entry to the client compiler.
|
||||
if (this.dev) {
|
||||
const pageKey = 'client' + routeInfo.page
|
||||
if (!entries[pageKey]) {
|
||||
entries[pageKey] = {
|
||||
bundlePath,
|
||||
absolutePagePath: routeInfo.absolutePagePath,
|
||||
clientLoader,
|
||||
dispose: false,
|
||||
lastActiveTime: Date.now(),
|
||||
} as any
|
||||
const invalidator = getInvalidator()
|
||||
if (invalidator) {
|
||||
invalidator.invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
injectedClientEntries.set(
|
||||
bundlePath,
|
||||
`next-client-pages-loader?${stringify({
|
||||
isServerComponent: true,
|
||||
page: denormalizePagePath(bundlePath.replace(/^pages/, '')),
|
||||
absolutePagePath: clientLoader,
|
||||
})}!` + clientLoader
|
||||
)
|
||||
}
|
||||
|
||||
// Inject the entry to the server compiler.
|
||||
const clientComponentEntryDep = (
|
||||
webpack as any
|
||||
).EntryPlugin.createDependency(
|
||||
clientLoader,
|
||||
name + NEXT_CLIENT_SSR_ENTRY_SUFFIX
|
||||
)
|
||||
promises.push(
|
||||
new Promise<void>((res, rej) => {
|
||||
compilation.addEntry(
|
||||
context,
|
||||
clientComponentEntryDep,
|
||||
this.isEdgeServer
|
||||
? {
|
||||
name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
|
||||
library: {
|
||||
name: ['self._CLIENT_ENTRY'],
|
||||
type: 'assign',
|
||||
},
|
||||
runtime: EDGE_RUNTIME_WEBPACK,
|
||||
asyncChunks: false,
|
||||
}
|
||||
: {
|
||||
name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
|
||||
runtime: 'webpack-runtime',
|
||||
},
|
||||
(err: any) => {
|
||||
if (err) {
|
||||
rej(err)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => callback())
|
||||
.catch(callback)
|
||||
}
|
||||
}
|
|
@ -5,21 +5,9 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { stringify } from 'querystring'
|
||||
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
|
||||
import {
|
||||
MIDDLEWARE_FLIGHT_MANIFEST,
|
||||
EDGE_RUNTIME_WEBPACK,
|
||||
NEXT_CLIENT_SSR_ENTRY_SUFFIX,
|
||||
} from '../../../shared/lib/constants'
|
||||
import { MIDDLEWARE_FLIGHT_MANIFEST } from '../../../shared/lib/constants'
|
||||
import { clientComponentRegex } from '../loaders/utils'
|
||||
import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path'
|
||||
import { denormalizePagePath } from '../../../shared/lib/page-path/denormalize-page-path'
|
||||
import {
|
||||
getInvalidator,
|
||||
entries,
|
||||
} from '../../../server/dev/on-demand-entry-handler'
|
||||
import { getPageStaticInfo } from '../../analysis/get-page-static-info'
|
||||
|
||||
// This is the module that will be used to anchor all client references to.
|
||||
// I.e. it will have all the client files as async deps from this point on.
|
||||
|
@ -30,28 +18,23 @@ import { getPageStaticInfo } from '../../analysis/get-page-static-info'
|
|||
|
||||
type Options = {
|
||||
dev: boolean
|
||||
viewsDir: boolean
|
||||
pageExtensions: string[]
|
||||
isEdgeServer: boolean
|
||||
}
|
||||
|
||||
const PLUGIN_NAME = 'FlightManifestPlugin'
|
||||
|
||||
let edgeFlightManifest = {}
|
||||
let nodeFlightManifest = {}
|
||||
|
||||
export const injectedClientEntries = new Map()
|
||||
|
||||
export class FlightManifestPlugin {
|
||||
dev: boolean = false
|
||||
pageExtensions: string[]
|
||||
isEdgeServer: boolean
|
||||
viewsDir: boolean = false
|
||||
|
||||
constructor(options: Options) {
|
||||
if (typeof options.dev === 'boolean') {
|
||||
this.dev = options.dev
|
||||
}
|
||||
this.viewsDir = options.viewsDir
|
||||
this.pageExtensions = options.pageExtensions
|
||||
this.isEdgeServer = options.isEdgeServer
|
||||
}
|
||||
|
||||
apply(compiler: any) {
|
||||
|
@ -69,14 +52,6 @@ export class FlightManifestPlugin {
|
|||
}
|
||||
)
|
||||
|
||||
// Only for webpack 5
|
||||
compiler.hooks.finishMake.tapAsync(
|
||||
PLUGIN_NAME,
|
||||
async (compilation: any, callback: any) => {
|
||||
this.createClientEndpoints(compilation, callback)
|
||||
}
|
||||
)
|
||||
|
||||
compiler.hooks.make.tap(PLUGIN_NAME, (compilation: any) => {
|
||||
compilation.hooks.processAssets.tap(
|
||||
{
|
||||
|
@ -89,143 +64,12 @@ export class FlightManifestPlugin {
|
|||
})
|
||||
}
|
||||
|
||||
async createClientEndpoints(compilation: any, callback: () => void) {
|
||||
const context = (this as any).context
|
||||
const promises: any = []
|
||||
|
||||
// For each SC server compilation entry, we need to create its corresponding
|
||||
// client component entry.
|
||||
for (const [name, entry] of compilation.entries.entries()) {
|
||||
// Check if the page entry is a server component or not.
|
||||
const entryDependency = entry.dependencies?.[0]
|
||||
const request = entryDependency?.request
|
||||
|
||||
if (request && entry.options?.layer === 'sc_server') {
|
||||
const visited = new Set()
|
||||
const clientComponentImports: string[] = []
|
||||
|
||||
function filterClientComponents(dependency: any) {
|
||||
const module = compilation.moduleGraph.getResolvedModule(dependency)
|
||||
if (!module) return
|
||||
|
||||
if (visited.has(module.userRequest)) return
|
||||
visited.add(module.userRequest)
|
||||
|
||||
if (clientComponentRegex.test(module.userRequest)) {
|
||||
clientComponentImports.push(module.userRequest)
|
||||
}
|
||||
|
||||
compilation.moduleGraph
|
||||
.getOutgoingConnections(module)
|
||||
.forEach((connection: any) => {
|
||||
filterClientComponents(connection.dependency)
|
||||
})
|
||||
}
|
||||
|
||||
// Traverse the module graph to find all client components.
|
||||
filterClientComponents(entryDependency)
|
||||
|
||||
const entryModule =
|
||||
compilation.moduleGraph.getResolvedModule(entryDependency)
|
||||
const routeInfo = entryModule.buildInfo.route || {
|
||||
page: denormalizePagePath(name.replace(/^pages/, '')),
|
||||
absolutePagePath: entryModule.resource,
|
||||
}
|
||||
|
||||
// Parse gSSP and gSP exports from the page source.
|
||||
const pageStaticInfo = this.isEdgeServer
|
||||
? {}
|
||||
: await getPageStaticInfo({
|
||||
pageFilePath: routeInfo.absolutePagePath,
|
||||
nextConfig: {},
|
||||
isDev: this.dev,
|
||||
})
|
||||
|
||||
const clientLoader = `next-flight-client-entry-loader?${stringify({
|
||||
modules: clientComponentImports,
|
||||
runtime: this.isEdgeServer ? 'edge' : 'nodejs',
|
||||
ssr: pageStaticInfo.ssr,
|
||||
// Adding name here to make the entry key unique.
|
||||
name,
|
||||
})}!`
|
||||
|
||||
const bundlePath = 'pages' + normalizePagePath(routeInfo.page)
|
||||
|
||||
// Inject the entry to the client compiler.
|
||||
if (this.dev) {
|
||||
const pageKey = 'client' + routeInfo.page
|
||||
if (!entries[pageKey]) {
|
||||
entries[pageKey] = {
|
||||
bundlePath,
|
||||
absolutePagePath: routeInfo.absolutePagePath,
|
||||
clientLoader,
|
||||
dispose: false,
|
||||
lastActiveTime: Date.now(),
|
||||
} as any
|
||||
const invalidator = getInvalidator()
|
||||
if (invalidator) {
|
||||
invalidator.invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
injectedClientEntries.set(
|
||||
bundlePath,
|
||||
`next-client-pages-loader?${stringify({
|
||||
isServerComponent: true,
|
||||
page: denormalizePagePath(bundlePath.replace(/^pages/, '')),
|
||||
absolutePagePath: clientLoader,
|
||||
})}!` + clientLoader
|
||||
)
|
||||
}
|
||||
|
||||
// Inject the entry to the server compiler.
|
||||
const clientComponentEntryDep = (
|
||||
webpack as any
|
||||
).EntryPlugin.createDependency(
|
||||
clientLoader,
|
||||
name + NEXT_CLIENT_SSR_ENTRY_SUFFIX
|
||||
)
|
||||
promises.push(
|
||||
new Promise<void>((res, rej) => {
|
||||
compilation.addEntry(
|
||||
context,
|
||||
clientComponentEntryDep,
|
||||
this.isEdgeServer
|
||||
? {
|
||||
name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
|
||||
library: {
|
||||
name: ['self._CLIENT_ENTRY'],
|
||||
type: 'assign',
|
||||
},
|
||||
runtime: EDGE_RUNTIME_WEBPACK,
|
||||
asyncChunks: false,
|
||||
}
|
||||
: {
|
||||
name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX,
|
||||
runtime: 'webpack-runtime',
|
||||
},
|
||||
(err: any) => {
|
||||
if (err) {
|
||||
rej(err)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => callback())
|
||||
.catch(callback)
|
||||
}
|
||||
|
||||
createAsset(assets: any, compilation: any) {
|
||||
const manifest: any = {}
|
||||
const viewsDir = this.viewsDir
|
||||
|
||||
compilation.chunkGroups.forEach((chunkGroup: any) => {
|
||||
function recordModule(id: string, _chunk: any, mod: any) {
|
||||
function recordModule(chunk: any, id: string, mod: any) {
|
||||
const resource = mod.resource
|
||||
|
||||
// TODO: Hook into deps instead of the target module.
|
||||
|
@ -258,7 +102,7 @@ export class FlightManifestPlugin {
|
|||
moduleExports[name] = {
|
||||
id: id.replace(/^\(sc_server\)\//, ''),
|
||||
name,
|
||||
chunks: [],
|
||||
chunks: viewsDir ? chunk.ids : [],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -277,33 +121,22 @@ export class FlightManifestPlugin {
|
|||
modId = modId.split('?')[0]
|
||||
// Remove the loader prefix.
|
||||
modId = modId.split('next-flight-client-loader.js!')[1] || modId
|
||||
modId = modId.replace(/^\(sc_server\)\//, '')
|
||||
|
||||
recordModule(chunk, modId, mod)
|
||||
|
||||
recordModule(modId, chunk, mod)
|
||||
// If this is a concatenation, register each child to the parent ID.
|
||||
if (mod.modules) {
|
||||
mod.modules.forEach((concatenatedMod: any) => {
|
||||
recordModule(modId, chunk, concatenatedMod)
|
||||
recordModule(chunk, modId, concatenatedMod)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// With switchable runtime, we need to emit the manifest files for both
|
||||
// runtimes.
|
||||
if (this.isEdgeServer) {
|
||||
edgeFlightManifest = manifest
|
||||
} else {
|
||||
nodeFlightManifest = manifest
|
||||
}
|
||||
const mergedManifest = {
|
||||
...nodeFlightManifest,
|
||||
...edgeFlightManifest,
|
||||
}
|
||||
const file =
|
||||
(!this.dev && !this.isEdgeServer ? '../' : '') +
|
||||
MIDDLEWARE_FLIGHT_MANIFEST
|
||||
const json = JSON.stringify(mergedManifest)
|
||||
const file = 'server/' + MIDDLEWARE_FLIGHT_MANIFEST
|
||||
const json = JSON.stringify(manifest)
|
||||
|
||||
assets[file + '.js'] = new sources.RawSource('self.__RSC_MANIFEST=' + json)
|
||||
assets[file + '.json'] = new sources.RawSource(json)
|
||||
|
|
|
@ -5,4 +5,15 @@ window.next = {
|
|||
root: true,
|
||||
}
|
||||
|
||||
// Override chunk URL mapping in the webpack runtime
|
||||
// https://github.com/webpack/webpack/blob/2738eebc7880835d88c727d364ad37f3ec557593/lib/RuntimeGlobals.js#L204
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const getChunkScriptFilename = __webpack_require__.u
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
__webpack_require__.u = (chunkId) => {
|
||||
return getChunkScriptFilename(chunkId) || `static/chunks/${chunkId}.js`
|
||||
}
|
||||
|
||||
hydrate()
|
||||
|
|
|
@ -486,6 +486,8 @@ export async function renderToHTML(
|
|||
// @ts-ignore
|
||||
globalThis.__webpack_require__ =
|
||||
ComponentMod.__next_rsc__.__webpack_require__
|
||||
// @ts-ignore
|
||||
globalThis.__webpack_chunk_load__ = () => Promise.resolve()
|
||||
|
||||
Component = createServerComponentRenderer(Component, {
|
||||
cachePrefix: pathname + (search ? `?${search}` : ''),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function ClientComponentRoute() {
|
||||
const [count, setCount] = useState(0)
|
||||
useEffect(() => {
|
||||
|
|
|
@ -240,9 +240,9 @@ describe('views dir', () => {
|
|||
)
|
||||
})
|
||||
|
||||
// TODO: Implement hydration
|
||||
it.skip('should serve client-side', async () => {
|
||||
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 root/client-component-route. count: 1'
|
||||
|
@ -260,15 +260,16 @@ describe('views dir', () => {
|
|||
expect($('p').text()).toBe('hello from root/client-nested')
|
||||
})
|
||||
|
||||
// TODO: Implement hydration
|
||||
it.skip('should include it client-side', async () => {
|
||||
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: 0'
|
||||
'Client Nested. Count: 1'
|
||||
)
|
||||
|
||||
// After hydration count should be 1
|
||||
expect(await browser.elementByCss('h1').text()).toBe(
|
||||
expect(await browser.elementByCss('p').text()).toBe(
|
||||
'hello from root/client-nested'
|
||||
)
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue