')
// ensure components are not rendered server-side
expect(html).not.toContain('client only script')
await expectNoClientErrors('/client-only-explicit-import')
})
it('/wrapper-expose/page', async () => {
await expectNoClientErrors('/wrapper-expose/page')
let lastLog: string|undefined
const page = await createPage('/wrapper-expose/page')
page.on('console', (log) => {
lastLog = log.text()
})
page.on('pageerror', (log) => {
lastLog = log.message
})
await page.waitForLoadState('networkidle')
await page.locator('#log-foo').click()
expect(lastLog === 'bar').toBeTruthy()
// change page
await page.locator('#to-hello').click()
await page.locator('#log-foo').click()
expect(lastLog?.includes('.foo is not a function')).toBeTruthy()
await page.locator('#log-hello').click()
expect(lastLog === 'world').toBeTruthy()
})
it('client-fallback', async () => {
const classes = [
'clientfallback-non-stateful-setup',
'clientfallback-non-stateful',
'clientfallback-stateful-setup',
'clientfallback-stateful'
]
const html = await $fetch('/client-fallback')
// ensure failed components are not rendered server-side
expect(html).not.toContain('This breaks in server-side setup.')
classes.forEach(c => expect(html).not.toContain(c))
// ensure not failed component not be rendered
expect(html).not.toContain('Sugar Counter 12 x 0 = 0')
// ensure NuxtClientFallback is being rendered with its fallback tag and attributes
expect(html).toContain('
this failed to render')
// ensure Fallback slot is being rendered server side
expect(html).toContain('Hello world !')
// ensure not failed component are correctly rendered
expect(html).not.toContain('
')
expect(html).toContain('hi')
await expectNoClientErrors('/client-fallback')
const page = await createPage('/client-fallback')
await page.waitForLoadState('networkidle')
// ensure components reactivity once mounted
await page.locator('#increment-count').click()
expect(await page.locator('#sugar-counter').innerHTML()).toContain('Sugar Counter 12 x 1 = 12')
// keep-fallback strategy
expect(await page.locator('#keep-fallback').all()).toHaveLength(1)
// #20833
expect(await page.locator('body').innerHTML()).not.toContain('Hello world !')
await page.close()
})
it('/legacy-async-data-fail', async () => {
const response = await fetch('/legacy-async-data-fail').then(r => r.text())
expect(response).not.toContain('don\'t look at this')
expect(response).toContain('OH NNNNNNOOOOOOOOOOO')
})
})
describe('nuxt composables', () => {
it('has useRequestURL()', async () => {
const html = await $fetch('/url')
expect(html).toContain('path: /url')
})
it('sets cookies correctly', async () => {
const res = await fetch('/cookies', {
headers: {
cookie: Object.entries({
'browser-accessed-but-not-used': 'provided-by-browser',
'browser-accessed-with-default-value': 'provided-by-browser',
'browser-set': 'provided-by-browser',
'browser-set-to-null': 'provided-by-browser',
'browser-set-to-null-with-default': 'provided-by-browser'
}).map(([key, value]) => `${key}=${value}`).join('; ')
}
})
const cookies = res.headers.get('set-cookie')
expect(cookies).toMatchInlineSnapshot('"set-in-plugin=true; Path=/, set=set; Path=/, browser-set=set; Path=/, browser-set-to-null=; Max-Age=0; Path=/, browser-set-to-null-with-default=; Max-Age=0; Path=/"')
})
})
describe('rich payloads', () => {
it('correctly serializes and revivifies complex types', async () => {
const html = await $fetch('/json-payload')
for (const test of [
'Date: true',
'Recursive objects: true',
'Shallow reactive: true',
'Shallow ref: true',
'Undefined ref: true',
'Reactive: true',
'Ref: true',
'Error: true'
]) {
expect(html).toContain(test)
}
})
})
describe('nuxt links', () => {
it('handles trailing slashes', async () => {
const html = await $fetch('/nuxt-link/trailing-slash')
const data: Record
= {}
for (const selector of ['nuxt-link', 'router-link', 'link-with-trailing-slash', 'link-without-trailing-slash']) {
data[selector] = []
for (const match of html.matchAll(new RegExp(`href="([^"]*)"[^>]*class="[^"]*\\b${selector}\\b`, 'g'))) {
data[selector].push(match[1])
}
}
expect(data).toMatchInlineSnapshot(`
{
"link-with-trailing-slash": [
"/",
"/nuxt-link/trailing-slash/",
"/nuxt-link/trailing-slash/",
"/nuxt-link/trailing-slash/?test=true&thing=other/thing#thing-other",
"/nuxt-link/trailing-slash/?test=true&thing=other/thing#thing-other",
"/nuxt-link/trailing-slash/",
"/nuxt-link/trailing-slash/?with-state=true",
"/nuxt-link/trailing-slash/?without-state=true",
],
"link-without-trailing-slash": [
"/",
"/nuxt-link/trailing-slash",
"/nuxt-link/trailing-slash",
"/nuxt-link/trailing-slash?test=true&thing=other/thing#thing-other",
"/nuxt-link/trailing-slash?test=true&thing=other/thing#thing-other",
"/nuxt-link/trailing-slash",
"/nuxt-link/trailing-slash?with-state=true",
"/nuxt-link/trailing-slash?without-state=true",
],
"nuxt-link": [
"/",
"/nuxt-link/trailing-slash",
"/nuxt-link/trailing-slash/",
"/nuxt-link/trailing-slash?test=true&thing=other/thing#thing-other",
"/nuxt-link/trailing-slash/?test=true&thing=other/thing#thing-other",
"/nuxt-link/trailing-slash",
"/nuxt-link/trailing-slash?with-state=true",
"/nuxt-link/trailing-slash?without-state=true",
],
"router-link": [
"/",
"/nuxt-link/trailing-slash",
"/nuxt-link/trailing-slash/",
"/nuxt-link/trailing-slash?test=true&thing=other/thing#thing-other",
"/nuxt-link/trailing-slash/?test=true&thing=other/thing#thing-other",
"/nuxt-link/trailing-slash",
"/nuxt-link/trailing-slash?with-state=true",
"/nuxt-link/trailing-slash?without-state=true",
],
}
`)
})
it('preserves route state', async () => {
const page = await createPage('/nuxt-link/trailing-slash')
await page.waitForLoadState('networkidle')
for (const selector of ['nuxt-link', 'router-link', 'link-with-trailing-slash', 'link-without-trailing-slash']) {
await page.locator(`.${selector}[href*=with-state]`).click()
await page.waitForLoadState('networkidle')
expect(await page.getByTestId('window-state').innerText()).toContain('bar')
await page.locator(`.${selector}[href*=without-state]`).click()
await page.waitForLoadState('networkidle')
expect(await page.getByTestId('window-state').innerText()).not.toContain('bar')
}
await page.close()
})
})
describe('head tags', () => {
it('SSR should render tags', async () => {
const headHtml = await $fetch('/head')
expect(headHtml).toContain('Using a dynamic component - Title Template Fn Change')
expect(headHtml).not.toContain('')
expect(headHtml).toContain('')
expect(headHtml.match('meta charset').length).toEqual(1)
expect(headHtml).toContain('')
expect(headHtml.match('meta name="viewport"').length).toEqual(1)
expect(headHtml).not.toContain('')
expect(headHtml).toContain('')
expect(headHtml).toMatch(/]*class="html-attrs-test"/)
expect(headHtml).toMatch(/]*class="body-attrs-test"/)
expect(headHtml).toContain('')
const indexHtml = await $fetch('/')
// should render charset by default
expect(indexHtml).toContain('')
// should render components
expect(indexHtml).toContain('Basic fixture')
})
it('SSR script setup should render tags', async () => {
const headHtml = await $fetch('/head-script-setup')
// useHead - title & titleTemplate are working
expect(headHtml).toContain('head script setup - Nuxt Playground')
// useSeoMeta - template params
expect(headHtml).toContain('')
// useSeoMeta - refs
expect(headHtml).toContain('')
// useServerHead - shorthands
expect(headHtml).toContain('>/* Custom styles */')
// useHeadSafe - removes dangerous content
expect(headHtml).toContain('')
expect(headHtml).toContain('')
})
it('SPA should render appHead tags', async () => {
const headHtml = await $fetch('/head', { headers: { 'x-nuxt-no-ssr': '1' } })
expect(headHtml).toContain('')
expect(headHtml).toContain('')
expect(headHtml).toContain('')
})
it('legacy vueuse/head works', async () => {
const headHtml = await $fetch('/vueuse-head')
expect(headHtml).toContain('using provides usehead and updateDOM - VueUse head polyfill test')
})
it('should render http-equiv correctly', async () => {
const html = await $fetch('/head')
// http-equiv should be rendered kebab case
expect(html).toContain('')
})
// TODO: Doesn't adds header in test environment
// it.todo('should render stylesheet link tag (SPA mode)', async () => {
// const html = await $fetch('/head', { headers: { 'x-nuxt-no-ssr': '1' } })
// expect(html).toMatch(/ {
it('should work with defineNuxtComponent', async () => {
const html = await $fetch('/legacy/async-data')
expect(html).toContain('Hello API
')
expect(html).toContain('fooChild
')
expect(html).toContain('fooParent
')
const { script } = parseData(html)
expect(script.data['options:asyncdata:hello'].hello).toBe('Hello API')
expect(Object.values(script.data)).toMatchInlineSnapshot(`
[
{
"baz": "qux",
"foo": "bar",
},
{
"hello": "Hello API",
},
{
"fooParent": "fooParent",
},
{
"fooChild": "fooChild",
},
]
`)
})
})
describe('navigate', () => {
it('should redirect to index with navigateTo', async () => {
const { headers, status } = await fetch('/navigate-to/', { redirect: 'manual' })
expect(headers.get('location')).toEqual('/')
expect(status).toEqual(301)
})
it('respects redirects + headers in middleware', async () => {
const res = await fetch('/navigate-some-path/', { redirect: 'manual', headers: { 'trailing-slash': 'true' } })
expect(res.headers.get('location')).toEqual('/navigate-some-path')
expect(res.status).toEqual(307)
expect(await res.text()).toMatchInlineSnapshot('""')
})
it('should not overwrite headers', async () => {
const { headers, status } = await fetch('/navigate-to-external', { redirect: 'manual' })
expect(headers.get('location')).toEqual('/')
expect(status).toEqual(302)
})
it('should not run setup function in path redirected to', async () => {
const { headers, status } = await fetch('/navigate-to-error', { redirect: 'manual' })
expect(headers.get('location')).toEqual('/setup-should-not-run')
expect(status).toEqual(302)
})
it('supports directly aborting navigation on SSR', async () => {
const { status } = await fetch('/navigate-to-false', { redirect: 'manual' })
expect(status).toEqual(404)
})
})
describe('preserves current instance', () => {
// TODO: it's unclear why there's an error here but it must be an upstream issue
it.todo('should not return getCurrentInstance when there\'s an error in data', async () => {
await fetch('/instance/error')
const html = await $fetch('/instance/next-request')
expect(html).toContain('This should be false: false')
})
// TODO: re-enable when https://github.com/nuxt/nuxt/issues/15164 is resolved
it.skipIf(isWindows)('should not lose current nuxt app after await in vue component', async () => {
const requests = await Promise.all(Array.from({ length: 100 }).map(() => $fetch('/instance/next-request')))
for (const html of requests) {
expect(html).toContain('This should be true: true')
}
})
})
describe('errors', () => {
it('should render a JSON error page', async () => {
const res = await fetch('/error', {
headers: {
accept: 'application/json'
}
})
expect(res.status).toBe(422)
expect(res.statusText).toBe('This is a custom error')
const error = await res.json()
delete error.stack
expect(error).toMatchObject({
message: 'This is a custom error',
statusCode: 422,
statusMessage: 'This is a custom error',
url: '/error'
})
})
it('should render a HTML error page', async () => {
const res = await fetch('/error')
// TODO: remove when we update CI to node v18
if (process.version.startsWith('v16')) {
expect(res.headers.get('Set-Cookie')).toBe('set-in-plugin=true; Path=/')
} else {
expect(res.headers.get('Set-Cookie')).toBe('set-in-plugin=true; Path=/, some-error=was%20set; Path=/')
}
expect(await res.text()).toContain('This is a custom error')
})
it('should not allow accessing error route directly', async () => {
const res = await fetch('/__nuxt_error', {
headers: {
accept: 'application/json'
}
})
expect(res.status).toBe(404)
const error = await res.json()
delete error.stack
expect(error).toMatchInlineSnapshot(`
{
"message": "Page Not Found: /__nuxt_error",
"statusCode": 404,
"statusMessage": "Page Not Found: /__nuxt_error",
"url": "/__nuxt_error",
}
`)
})
// TODO: need to create test for webpack
it.runIf(!isDev() && !isWebpack)('should handle chunk loading errors', async () => {
const { page, consoleLogs } = await renderPage('/')
await page.getByText('Increment state').click()
await page.getByText('Increment state').click()
expect(await page.innerText('div')).toContain('Some value: 3')
await page.getByText('Chunk error').click()
await page.waitForURL(url('/chunk-error'))
expect(consoleLogs.map(c => c.text).join('')).toContain('caught chunk load error')
expect(await page.innerText('div')).toContain('Chunk error page')
await page.waitForLoadState('networkidle')
expect(await page.innerText('div')).toContain('State: 3')
await page.close()
})
})
describe('navigate external', () => {
it('should redirect to example.com', async () => {
const { headers } = await fetch('/navigate-to-external/', { redirect: 'manual' })
expect(headers.get('location')).toEqual('https://example.com/?redirect=false#test')
})
it('should redirect to api endpoint', async () => {
const { headers } = await fetch('/navigate-to-api', { redirect: 'manual' })
expect(headers.get('location')).toEqual('/api/test')
})
})
describe('middlewares', () => {
it('should redirect to index with global middleware', async () => {
const html = await $fetch('/redirect/')
// Snapshot
// expect(html).toMatchInlineSnapshot()
expect(html).toContain('Hello Nuxt 3!')
})
it('should allow aborting navigation on server-side', async () => {
const res = await fetch('/?abort', {
headers: {
accept: 'application/json'
}
})
expect(res.status).toEqual(401)
})
it('should allow aborting navigation fatally on client-side', async () => {
const html = await $fetch('/middleware-abort')
expect(html).not.toContain('This is the error page')
const page = await createPage('/middleware-abort')
await page.waitForLoadState('networkidle')
expect(await page.innerHTML('body')).toContain('This is the error page')
await page.close()
})
it('should inject auth', async () => {
const html = await $fetch('/auth')
// Snapshot
// expect(html).toMatchInlineSnapshot()
expect(html).toContain('auth.vue')
expect(html).toContain('auth: Injected by injectAuth middleware')
})
it('should not inject auth', async () => {
const html = await $fetch('/no-auth')
// Snapshot
// expect(html).toMatchInlineSnapshot()
expect(html).toContain('no-auth.vue')
expect(html).toContain('auth: ')
expect(html).not.toContain('Injected by injectAuth middleware')
})
it('should redirect to index with http 307 with navigateTo on server side', async () => {
const html = await fetch('/navigate-to-redirect', { redirect: 'manual' })
expect(html.headers.get('location')).toEqual('/')
expect(html.status).toEqual(307)
})
})
describe('plugins', () => {
it('basic plugin', async () => {
const html = await $fetch('/plugins')
expect(html).toContain('myPlugin: Injected by my-plugin')
})
it('async plugin', async () => {
const html = await $fetch('/plugins')
expect(html).toContain('asyncPlugin: Async plugin works! 123')
expect(html).toContain('useFetch works!')
})
})
describe('layouts', () => {
it('should apply custom layout', async () => {
const html = await $fetch('/with-layout')
// Snapshot
// expect(html).toMatchInlineSnapshot()
expect(html).toContain('with-layout.vue')
expect(html).toContain('Custom Layout:')
})
it('should work with a dynamically set layout', async () => {
const html = await $fetch('/with-dynamic-layout')
// Snapshot
// expect(html).toMatchInlineSnapshot()
expect(html).toContain('with-dynamic-layout')
expect(html).toContain('Custom Layout:')
await expectNoClientErrors('/with-dynamic-layout')
})
it('should work with a computed layout', async () => {
const html = await $fetch('/with-computed-layout')
// Snapshot
// expect(html).toMatchInlineSnapshot()
expect(html).toContain('with-computed-layout')
expect(html).toContain('Custom Layout')
await expectNoClientErrors('/with-computed-layout')
})
it('should allow passing custom props to a layout', async () => {
const html = await $fetch('/layouts/with-props')
expect(html).toContain('some prop was passed')
await expectNoClientErrors('/layouts/with-props')
})
})
describe('reactivity transform', () => {
it('should works', async () => {
const html = await $fetch('/')
expect(html).toContain('Sugar Counter 12 x 2 = 24')
})
})
describe('composable tree shaking', () => {
it('should work', async () => {
const html = await $fetch('/tree-shake')
expect(html).toContain('Tree Shake Example')
const page = await createPage('/tree-shake')
// check page doesn't have any errors or warnings in the console
await page.waitForLoadState('networkidle')
// ensure scoped classes are correctly assigned between client and server
expect(await page.$eval('h1', e => getComputedStyle(e).color)).toBe('rgb(255, 192, 203)')
await expectNoClientErrors('/tree-shake')
await page.close()
})
})
describe('server tree shaking', () => {
it('should work', async () => {
const html = await $fetch('/client')
expect(html).toContain('This page should not crash when rendered')
expect(html).toContain('fallback for ClientOnly')
expect(html).not.toContain('rendered client-side')
expect(html).not.toContain('id="client-side"')
const page = await createPage('/client')
await page.waitForLoadState('networkidle')
// ensure scoped classes are correctly assigned between client and server
expect(await page.$eval('.red', e => getComputedStyle(e).color)).toBe('rgb(255, 0, 0)')
expect(await page.$eval('.blue', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)')
expect(await page.locator('#client-side').textContent()).toContain('This should be rendered client-side')
await page.close()
})
})
describe('extends support', () => {
describe('layouts & pages', () => {
it('extends foo/layouts/default & foo/pages/index', async () => {
const html = await $fetch('/foo')
expect(html).toContain('Extended layout from foo')
expect(html).toContain('Extended page from foo')
})
it('extends [bar/layouts/override & bar/pages/override] over [foo/layouts/override & foo/pages/override]', async () => {
const html = await $fetch('/override')
expect(html).toContain('Extended layout from bar')
expect(html).toContain('Extended page from bar')
})
})
describe('components', () => {
it('extends foo/components/ExtendsFoo', async () => {
const html = await $fetch('/foo')
expect(html).toContain('Extended component from foo')
})
it('extends bar/components/ExtendsOverride over foo/components/ExtendsOverride', async () => {
const html = await $fetch('/override')
expect(html).toContain('Extended component from bar')
})
})
describe('middlewares', () => {
it('works with layer aliases', async () => {
const html = await $fetch('/foo')
expect(html).toContain('from layer alias')
})
it('extends foo/middleware/foo', async () => {
const html = await $fetch('/foo')
expect(html).toContain('Middleware | foo: Injected by extended middleware from foo')
})
it('extends bar/middleware/override over foo/middleware/override', async () => {
const html = await $fetch('/override')
expect(html).toContain('Middleware | override: Injected by extended middleware from bar')
})
})
describe('composables', () => {
it('extends foo/composables/foo', async () => {
const html = await $fetch('/foo')
expect(html).toContain('Composable | useExtendsFoo: foo')
})
it('allows overriding composables', async () => {
const html = await $fetch('/extends')
expect(html).toContain('test from project')
})
})
describe('plugins', () => {
it('extends foo/plugins/foo', async () => {
const html = await $fetch('/foo')
expect(html).toContain('Plugin | foo: String generated from foo plugin!')
})
})
describe('server', () => {
it('extends foo/server/api/foo', async () => {
expect(await $fetch('/api/foo')).toBe('foo')
})
it('extends foo/server/middleware/foo', async () => {
const { headers } = await fetch('/')
expect(headers.get('injected-header')).toEqual('foo')
})
})
describe('app', () => {
it('extends foo/app/router.options & bar/app/router.options', async () => {
const html: string = await $fetch('/')
const routerLinkClasses = html.match(/href="\/" class="([^"]*)"/)?.[1].split(' ')
expect(routerLinkClasses).toContain('foo-active-class')
expect(routerLinkClasses).toContain('bar-exact-active-class')
})
})
})
// Bug #7337
describe('deferred app suspense resolve', () => {
async function behaviour (path: string) {
await withLogs(async (page, logs) => {
await page.goto(url(path))
await page.waitForLoadState('networkidle')
// Wait for all pending micro ticks to be cleared in case hydration haven't finished yet.
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
const hydrationLogs = logs.filter(log => log.includes('isHydrating'))
expect(hydrationLogs.length).toBe(3)
expect(hydrationLogs.every(log => log === 'isHydrating: true'))
})
}
it('should wait for all suspense instance on initial hydration', async () => {
await behaviour('/async-parent/child')
})
it('should wait for all suspense instance on initial hydration', async () => {
await behaviour('/internal-layout/async-parent/child')
})
})
describe('nested suspense', () => {
const navigations = ([
['/suspense/sync-1/async-1/', '/suspense/sync-2/async-1/'],
['/suspense/sync-1/sync-1/', '/suspense/sync-2/async-1/'],
['/suspense/async-1/async-1/', '/suspense/async-2/async-1/'],
['/suspense/async-1/sync-1/', '/suspense/async-2/async-1/']
]).flatMap(([start, end]) => [
[start, end],
[start, end + '?layout=custom'],
[start + '?layout=custom', end]
])
it.each(navigations)('should navigate from %s to %s with no white flash', async (start, nav) => {
const page = await createPage(start, {})
const logs: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (text.includes('[vite]') || text.includes(' is an experimental feature')) { return }
logs.push(msg.text())
})
await page.waitForLoadState('networkidle')
const slug = nav.replace(/\?.*$/, '').replace(/[/-]+/g, '-')
await page.click(`[href^="${nav}"]`)
const text = await page.waitForFunction(slug => document.querySelector(`main:has(#child${slug})`)?.innerHTML, slug)
// @ts-expect-error TODO: fix upstream in playwright - types for evaluate are broken
.then(r => r.evaluate(r => r))
// expect(text).toMatchInlineSnapshot()
// const parent = await page.waitForSelector(`#${slug}`, { state: 'attached' })
// const text = await parent.innerText()
expect(text).toContain('Async child: 2 - 1')
expect(text).toContain('parent: 2')
const first = start.match(/\/suspense\/(?a?sync)-(?\d)\/(?a?sync)-(?\d)\//)!.groups!
const last = nav.match(/\/suspense\/(?a?sync)-(?\d)\/(?a?sync)-(?\d)\//)!.groups!
expect(logs.sort()).toEqual([
// [first load] from parent
`[${first.parentType}]`,
...first.parentType === 'async' ? ['[async] running async data'] : [],
// [first load] from child
`[${first.parentType}] [${first.childType}]`,
...first.childType === 'async' ? [`[${first.parentType}] [${first.parentNum}] [async] [${first.childNum}] running async data`] : [],
// [navigation] from parent
`[${last.parentType}]`,
...last.parentType === 'async' ? ['[async] running async data'] : [],
// [navigation] from child
`[${last.parentType}] [${last.childType}]`,
...last.childType === 'async' ? [`[${last.parentType}] [${last.parentNum}] [async] [${last.childNum}] running async data`] : []
].sort())
await page.close()
})
const outwardNavigations = [
['/suspense/async-2/async-1/', '/suspense/async-1/'],
['/suspense/async-2/sync-1/', '/suspense/async-1/']
]
it.each(outwardNavigations)('should navigate from %s to a parent %s with no white flash', async (start, nav) => {
const page = await createPage(start, {})
const logs: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (text.includes('[vite]') || text.includes(' is an experimental feature')) { return }
logs.push(msg.text())
})
await page.waitForLoadState('networkidle')
await page.waitForSelector(`main:has(#child${start.replace(/[/-]+/g, '-')})`)
const slug = start.replace(/[/-]+/g, '-')
await page.click(`[href^="${nav}"]`)
// wait until child selector disappears and grab HTML of parent
const text = await page.waitForFunction(slug => document.querySelector(`main:not(:has(#child${slug}))`)?.innerHTML, slug)
// @ts-expect-error TODO: fix upstream in playwright - types for evaluate are broken
.then(r => r.evaluate(r => r))
expect(text).toContain('Async parent: 1')
const first = start.match(/\/suspense\/(?a?sync)-(?\d)\/(?a?sync)-(?\d)\//)!.groups!
const last = nav.match(/\/suspense\/(?a?sync)-(?\d)\//)!.groups!
expect(logs.sort()).toEqual([
// [first load] from parent
`[${first.parentType}]`,
...first.parentType === 'async' ? ['[async] running async data'] : [],
// [first load] from child
`[${first.parentType}] [${first.childType}]`,
...first.childType === 'async' ? [`[${first.parentType}] [${first.parentNum}] [async] [${first.childNum}] running async data`] : [],
// [navigation] from parent
`[${last.parentType}]`,
...last.parentType === 'async' ? ['[async] running async data'] : []
].sort())
await page.close()
})
const inwardNavigations = [
['/suspense/async-2/', '/suspense/async-1/async-1/'],
['/suspense/async-2/', '/suspense/async-1/sync-1/']
]
it.each(inwardNavigations)('should navigate from %s to a child %s with no white flash', async (start, nav) => {
const page = await createPage(start, {})
const logs: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (text.includes('[vite]') || text.includes(' is an experimental feature')) { return }
logs.push(msg.text())
})
await page.waitForLoadState('networkidle')
const slug = nav.replace(/[/-]+/g, '-')
await page.click(`[href^="${nav}"]`)
// wait until child selector appears and grab HTML of parent
const text = await page.waitForFunction(slug => document.querySelector(`main:has(#child${slug})`)?.innerHTML, slug)
// @ts-expect-error TODO: fix upstream in playwright - types for evaluate are broken
.then(r => r.evaluate(r => r))
// const text = await parent.innerText()
expect(text).toContain('Async parent: 1')
const first = start.match(/\/suspense\/(?a?sync)-(?\d)\//)!.groups!
const last = nav.match(/\/suspense\/(?a?sync)-(?\d)\/(?a?sync)-(?\d)\//)!.groups!
expect(logs.sort()).toEqual([
// [first load] from parent
`[${first.parentType}]`,
...first.parentType === 'async' ? ['[async] running async data'] : [],
// [navigation] from parent
`[${last.parentType}]`,
...last.parentType === 'async' ? ['[async] running async data'] : [],
// [navigation] from child
`[${last.parentType}] [${last.childType}]`,
...last.childType === 'async' ? [`[${last.parentType}] [${last.parentNum}] [async] [${last.childNum}] running async data`] : []
].sort())
await page.close()
})
})
// Bug #6592
describe('page key', () => {
it('should not cause run of setup if navigation not change page key and layout', async () => {
async function behaviour (path: string) {
await withLogs(async (page, logs) => {
await page.goto(url(`${path}/0`))
await page.waitForLoadState('networkidle')
await page.click(`[href="${path}/1"]`)
await page.waitForSelector('#page-1')
// Wait for all pending micro ticks to be cleared,
// so we are not resolved too early when there are repeated page loading
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
expect(logs.filter(l => l.includes('Child Setup')).length).toBe(1)
})
}
await behaviour('/fixed-keyed-child-parent')
await behaviour('/internal-layout/fixed-keyed-child-parent')
})
it('will cause run of setup if navigation changed page key', async () => {
async function behaviour (path: string) {
await withLogs(async (page, logs) => {
await page.goto(url(`${path}/0`))
await page.waitForLoadState('networkidle')
await page.click(`[href="${path}/1"]`)
await page.waitForSelector('#page-1')
// Wait for all pending micro ticks to be cleared,
// so we are not resolved too early when there are repeated page loading
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
expect(logs.filter(l => l.includes('Child Setup')).length).toBe(2)
})
}
await behaviour('/keyed-child-parent')
await behaviour('/internal-layout/keyed-child-parent')
})
})
// Bug #6592
describe('layout change not load page twice', () => {
async function behaviour (path1: string, path2: string) {
await withLogs(async (page, logs) => {
await page.goto(url(path1))
await page.waitForLoadState('networkidle')
await page.click(`[href="${path2}"]`)
await page.waitForSelector('#with-layout2')
// Wait for all pending micro ticks to be cleared,
// so we are not resolved too early when there are repeated page loading
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
expect(logs.filter(l => l.includes('Layout2 Page Setup')).length).toBe(1)
})
}
it('should not cause run of page setup to repeat if layout changed', async () => {
await behaviour('/with-layout', '/with-layout2')
await behaviour('/internal-layout/with-layout', '/internal-layout/with-layout2')
})
})
describe('layout switching', () => {
// #13309
it('does not cause TypeError: Cannot read properties of null', async () => {
await withLogs(async (page, logs) => {
await page.goto(url('/layout-switch/start'))
await page.waitForLoadState('networkidle')
await page.click('[href="/layout-switch/end"]')
// Wait for all pending micro ticks to be cleared,
// so we are not resolved too early when there are repeated page loading
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
expect(logs.filter(l => l.match(/error/i))).toMatchInlineSnapshot('[]')
})
})
})
describe('automatically keyed composables', () => {
it('should automatically generate keys', async () => {
const html = await $fetch('/keyed-composables')
expect(html).toContain('true')
expect(html).not.toContain('false')
})
it('should match server-generated keys', async () => {
await expectNoClientErrors('/keyed-composables')
})
it('should not automatically generate keys', async () => {
await expectNoClientErrors('/keyed-composables/local')
const html = await $fetch('/keyed-composables/local')
expect(html).toContain('true')
expect(html).not.toContain('false')
})
})
describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
const inlinedCSS = [
'{--plugin:"plugin"}', // CSS imported ambiently in JS/TS
'{--global:"global";', // global css from nuxt.config
'{--assets:"assets"}', //