test: refactor test suite and reduce networkidle dependency (#22596)

This commit is contained in:
Daniel Roe 2023-08-12 08:18:58 +01:00 committed by GitHub
parent 2a4867a23f
commit e93195a317
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 136 additions and 244 deletions

View File

@ -8,7 +8,7 @@ import { $fetch, createPage, fetch, isDev, setup, startServer, url, useTestConte
import { $fetchComponent } from '@nuxt/test-utils/experimental' import { $fetchComponent } from '@nuxt/test-utils/experimental'
import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer' import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
import { expectNoClientErrors, expectWithPolling, isRenderingJson, parseData, parsePayload, renderPage, withLogs } from './utils' import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage } from './utils'
const isWebpack = process.env.TEST_BUILDER === 'webpack' const isWebpack = process.env.TEST_BUILDER === 'webpack'
@ -17,7 +17,7 @@ await setup({
dev: process.env.TEST_ENV === 'dev', dev: process.env.TEST_ENV === 'dev',
server: true, server: true,
browser: true, browser: true,
setupTimeout: (isWindows ? 240 : 120) * 1000, setupTimeout: (isWindows ? 360 : 120) * 1000,
nuxtConfig: { nuxtConfig: {
builder: isWebpack ? 'webpack' : 'vite', builder: isWebpack ? 'webpack' : 'vite',
buildDir: process.env.NITRO_BUILD_DIR, buildDir: process.env.NITRO_BUILD_DIR,
@ -53,9 +53,8 @@ describe('route rules', () => {
}) })
it('test noScript routeRules', async () => { it('test noScript routeRules', async () => {
const page = await createPage('/no-scripts') const html = await $fetch('/no-scripts')
expect(await page.locator('script').all()).toHaveLength(0) expect(html).not.toContain('<script')
await page.close()
}) })
}) })
@ -134,13 +133,12 @@ describe('pages', () => {
expect(status).toEqual(404) expect(status).toEqual(404)
expect(headers.get('Set-Cookie')).toBe('set-in-plugin=true; Path=/') expect(headers.get('Set-Cookie')).toBe('set-in-plugin=true; Path=/')
const page = await createPage('/navigate-to-forbidden') const { page } = await renderPage('/navigate-to-forbidden')
await page.waitForLoadState('networkidle')
await page.getByText('should throw a 404 error').click() await page.getByText('should throw a 404 error').click()
expect(await page.getByRole('heading').textContent()).toMatchInlineSnapshot('"Page Not Found: /forbidden"') expect(await page.getByRole('heading').textContent()).toMatchInlineSnapshot('"Page Not Found: /forbidden"')
await page.goto(url('/navigate-to-forbidden')) await gotoPath(page, '/navigate-to-forbidden')
await page.waitForLoadState('networkidle')
await page.getByText('should be caught by catchall').click() await page.getByText('should be caught by catchall').click()
expect(await page.getByRole('heading').textContent()).toMatchInlineSnapshot('"[...slug].vue"') expect(await page.getByRole('heading').textContent()).toMatchInlineSnapshot('"[...slug].vue"')
@ -172,22 +170,21 @@ describe('pages', () => {
it('expect no loading indicator on middleware abortNavigation', async () => { it('expect no loading indicator on middleware abortNavigation', async () => {
const { page } = await renderPage('/') const { page } = await renderPage('/')
await page.waitForLoadState('networkidle')
await page.locator('#middleware-abort-non-fatal').click() await page.locator('#middleware-abort-non-fatal').click()
expect(await page.locator('#lodagin-indicator').all()).toHaveLength(0) expect(await page.locator('#lodagin-indicator').all()).toHaveLength(0)
await page.locator('#middleware-abort-non-fatal-error').click() await page.locator('#middleware-abort-non-fatal-error').click()
expect(await page.locator('#lodagin-indicator').all()).toHaveLength(0) expect(await page.locator('#lodagin-indicator').all()).toHaveLength(0)
await page.close()
}) })
it('should render correctly when loaded on a different path', async () => { it('should render correctly when loaded on a different path', async () => {
const page = await createPage('/proxy') const { page, pageErrors } = await renderPage('/proxy')
await page.waitForLoadState('networkidle')
expect(await page.innerText('body')).toContain('Composable | foo: auto imported from ~/composables/foo.ts') expect(await page.innerText('body')).toContain('Composable | foo: auto imported from ~/composables/foo.ts')
await page.close() await page.close()
await expectNoClientErrors('/proxy') expect(pageErrors).toEqual([])
}) })
it('preserves query', async () => { it('preserves query', async () => {
@ -265,11 +262,7 @@ describe('pages', () => {
// ensure components are not rendered server-side // ensure components are not rendered server-side
expect(html).not.toContain('Should not be server rendered') expect(html).not.toContain('Should not be server rendered')
await expectNoClientErrors('/client-only-components') const { page, pageErrors } = await renderPage('/client-only-components')
const page = await createPage('/client-only-components')
await page.waitForLoadState('networkidle')
const hiddenSelectors = [ const hiddenSelectors = [
'.string-stateful-should-be-hidden', '.string-stateful-should-be-hidden',
@ -331,25 +324,16 @@ describe('pages', () => {
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible())) await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy())) .then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
expect(pageErrors).toEqual([])
await page.close() await page.close()
}) })
it('/wrapper-expose/layout', async () => { it('/wrapper-expose/layout', async () => {
await expectNoClientErrors('/wrapper-expose/layout') const { page, consoleLogs, pageErrors } = await renderPage('/wrapper-expose/layout')
let lastLog: string|undefined
const page = await createPage('/wrapper-expose/layout')
page.on('console', (log) => {
lastLog = log.text()
})
page.on('pageerror', (log) => {
lastLog = log.message
})
await page.waitForLoadState('networkidle')
await page.locator('.log-foo').first().click() await page.locator('.log-foo').first().click()
expect(lastLog).toContain('.logFoo is not a function') expect(pageErrors.at(-1)?.toString() || consoleLogs.at(-1)!.text).toContain('.logFoo is not a function')
await page.locator('.log-hello').first().click() await page.locator('.log-hello').first().click()
expect(lastLog).toContain('world') expect(consoleLogs.at(-1)!.text).toContain('world')
await page.locator('.add-count').first().click() await page.locator('.add-count').first().click()
expect(await page.locator('.count').first().innerText()).toContain('1') expect(await page.locator('.count').first().innerText()).toContain('1')
@ -357,14 +341,15 @@ describe('pages', () => {
await page.locator('.swap-layout').click() await page.locator('.swap-layout').click()
await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('0')) await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('0'))
await page.locator('.log-foo').first().click() await page.locator('.log-foo').first().click()
expect(lastLog).toContain('bar') expect(consoleLogs.at(-1)!.text).toContain('bar')
await page.locator('.log-hello').first().click() await page.locator('.log-hello').first().click()
expect(lastLog).toContain('.logHello is not a function') expect(pageErrors.at(-1)?.toString() || consoleLogs.at(-1)!.text).toContain('.logHello is not a function')
await page.locator('.add-count').first().click() await page.locator('.add-count').first().click()
await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('1')) await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('1'))
// change layout // change layout
await page.locator('.swap-layout').click() await page.locator('.swap-layout').click()
await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('0')) await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('0'))
await page.close()
}) })
it('/client-only-explicit-import', async () => { it('/client-only-explicit-import', async () => {
@ -379,24 +364,16 @@ describe('pages', () => {
}) })
it('/wrapper-expose/page', async () => { it('/wrapper-expose/page', async () => {
await expectNoClientErrors('/wrapper-expose/page') const { page, pageErrors, consoleLogs } = await renderPage('/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() await page.locator('#log-foo').click()
expect(lastLog === 'bar').toBeTruthy() expect(consoleLogs.at(-1)?.text).toBe('bar')
// change page // change page
await page.locator('#to-hello').click() await page.locator('#to-hello').click()
await page.locator('#log-foo').click() await page.locator('#log-foo').click()
expect(lastLog?.includes('.foo is not a function')).toBeTruthy() expect(pageErrors.at(-1)?.toString() || consoleLogs.at(-1)!.text).toContain('.foo is not a function')
await page.locator('#log-hello').click() await page.locator('#log-hello').click()
expect(lastLog === 'world').toBeTruthy() expect(consoleLogs.at(-1)?.text).toBe('world')
await page.close()
}) })
it('client-fallback', async () => { it('client-fallback', async () => {
@ -421,10 +398,7 @@ describe('pages', () => {
expect(html).not.toContain('<p></p>') expect(html).not.toContain('<p></p>')
expect(html).toContain('hi') expect(html).toContain('hi')
await expectNoClientErrors('/client-fallback') const { page, pageErrors } = await renderPage('/client-fallback')
const page = await createPage('/client-fallback')
await page.waitForLoadState('networkidle')
// ensure components reactivity once mounted // ensure components reactivity once mounted
await page.locator('#increment-count').click() await page.locator('#increment-count').click()
expect(await page.locator('#sugar-counter').innerHTML()).toContain('Sugar Counter 12 x 1 = 12') expect(await page.locator('#sugar-counter').innerHTML()).toContain('Sugar Counter 12 x 1 = 12')
@ -432,6 +406,7 @@ describe('pages', () => {
expect(await page.locator('#keep-fallback').all()).toHaveLength(1) expect(await page.locator('#keep-fallback').all()).toHaveLength(1)
// #20833 // #20833
expect(await page.locator('body').innerHTML()).not.toContain('Hello world !') expect(await page.locator('body').innerHTML()).not.toContain('Hello world !')
expect(pageErrors).toEqual([])
await page.close() await page.close()
}) })
@ -539,16 +514,14 @@ describe('nuxt links', () => {
}) })
it('preserves route state', async () => { it('preserves route state', async () => {
const page = await createPage('/nuxt-link/trailing-slash') const { page } = await renderPage('/nuxt-link/trailing-slash')
await page.waitForLoadState('networkidle')
for (const selector of ['nuxt-link', 'router-link', 'link-with-trailing-slash', 'link-without-trailing-slash']) { 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.locator(`.${selector}[href*=with-state]`).click()
await page.waitForLoadState('networkidle') await page.getByTestId('window-state').getByText('bar').waitFor()
expect(await page.getByTestId('window-state').innerText()).toContain('bar')
await page.locator(`.${selector}[href*=without-state]`).click() await page.locator(`.${selector}[href*=without-state]`).click()
await page.waitForLoadState('networkidle') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath.includes('without-state'))
expect(await page.getByTestId('window-state').innerText()).not.toContain('bar') expect(await page.getByTestId('window-state').innerText()).not.toContain('bar')
} }
@ -760,8 +733,8 @@ describe('errors', () => {
await page.waitForURL(url('/chunk-error')) await page.waitForURL(url('/chunk-error'))
expect(consoleLogs.map(c => c.text).join('')).toContain('caught chunk load error') expect(consoleLogs.map(c => c.text).join('')).toContain('caught chunk load error')
expect(await page.innerText('div')).toContain('Chunk error page') expect(await page.innerText('div')).toContain('Chunk error page')
await page.waitForLoadState('networkidle') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/chunk-error')
expect(await page.innerText('div')).toContain('State: 3') await page.locator('div').getByText('State: 3').waitFor()
await page.close() await page.close()
}) })
@ -803,8 +776,7 @@ describe('middlewares', () => {
it('should allow aborting navigation fatally on client-side', async () => { it('should allow aborting navigation fatally on client-side', async () => {
const html = await $fetch('/middleware-abort') const html = await $fetch('/middleware-abort')
expect(html).not.toContain('This is the error page') expect(html).not.toContain('This is the error page')
const page = await createPage('/middleware-abort') const { page } = await renderPage('/middleware-abort')
await page.waitForLoadState('networkidle')
expect(await page.innerHTML('body')).toContain('This is the error page') expect(await page.innerHTML('body')).toContain('This is the error page')
await page.close() await page.close()
}) })
@ -901,13 +873,11 @@ describe('composable tree shaking', () => {
expect(html).toContain('Tree Shake Example') expect(html).toContain('Tree Shake Example')
const page = await createPage('/tree-shake') const { page, pageErrors } = await renderPage('/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 // ensure scoped classes are correctly assigned between client and server
expect(await page.$eval('h1', e => getComputedStyle(e).color)).toBe('rgb(255, 192, 203)') expect(await page.$eval('h1', e => getComputedStyle(e).color)).toBe('rgb(255, 192, 203)')
await expectNoClientErrors('/tree-shake') expect(pageErrors).toEqual([])
await page.close() await page.close()
}) })
@ -922,8 +892,8 @@ describe('server tree shaking', () => {
expect(html).not.toContain('rendered client-side') expect(html).not.toContain('rendered client-side')
expect(html).not.toContain('id="client-side"') expect(html).not.toContain('id="client-side"')
const page = await createPage('/client') const { page } = await renderPage('/client')
await page.waitForLoadState('networkidle') await page.waitForFunction(() => window.useNuxtApp?.())
// ensure scoped classes are correctly assigned between client and server // 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('.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.$eval('.blue', e => getComputedStyle(e).color)).toBe('rgb(0, 0, 255)')
@ -1017,44 +987,29 @@ describe('extends support', () => {
// Bug #7337 // Bug #7337
describe('deferred app suspense resolve', () => { describe('deferred app suspense resolve', () => {
async function behaviour (path: string) { it.each(['/async-parent/child', '/internal-layout/async-parent/child'])('should wait for all suspense instance on initial hydration', async (path) => {
await withLogs(async (page, logs) => { const { page, consoleLogs } = await renderPage(path)
await page.goto(url(path))
await page.waitForLoadState('networkidle')
// Wait for all pending micro ticks to be cleared in case hydration hasn't finished yet. // Wait for all pending micro ticks to be cleared in case hydration hasn't finished yet.
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
const hydrationLogs = logs.filter(log => log.includes('isHydrating')) const hydrationLogs = consoleLogs.filter(log => log.text.includes('isHydrating'))
expect(hydrationLogs.length).toBe(3) expect(hydrationLogs.length).toBe(3)
expect(hydrationLogs.every(log => log === 'isHydrating: true')) expect(hydrationLogs.every(log => log.text === 'isHydrating: true'))
})
} await page.close()
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')
}) })
it('should wait for suspense in parent layout', async () => { it('should wait for suspense in parent layout', async () => {
const page = await createPage('/hydration/layout') const { page } = await renderPage('/hydration/layout')
await page.waitForLoadState('networkidle') await page.getByText('Tests whether hydration is properly resolved within an async layout').waitFor()
await page.close()
// Wait for all pending micro ticks to be cleared in case hydration hasn't finished yet.
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
const html = await page.getByRole('document').innerHTML()
expect(html).toContain('Tests whether hydration is properly resolved within an async layout')
}) })
it('should fully hydrate even if there is a redirection on a page with `ssr: false`', async () => { it('should fully hydrate even if there is a redirection on a page with `ssr: false`', async () => {
const page = await createPage('/hydration/spa-redirection/start') const { page } = await renderPage('/hydration/spa-redirection/start')
await page.waitForLoadState('networkidle') await page.getByText('fully hydrated and ready to go').waitFor()
await page.close()
// Wait for all pending micro ticks to be cleared in case hydration hasn't finished yet.
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
const html = await page.getByRole('document').innerHTML()
expect(html).toContain('fully hydrated and ready to go')
}) })
}) })
@ -1071,14 +1026,7 @@ describe('nested suspense', () => {
]) ])
it.each(navigations)('should navigate from %s to %s with no white flash', async (start, nav) => { it.each(navigations)('should navigate from %s to %s with no white flash', async (start, nav) => {
const page = await createPage(start, {}) const { page, consoleLogs } = await renderPage(start)
const logs: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (text.includes('[vite]') || text.includes('<Suspense> is an experimental feature')) { return }
logs.push(msg.text())
})
await page.waitForLoadState('networkidle')
const slug = nav.replace(/\?.*$/, '').replace(/[/-]+/g, '-') const slug = nav.replace(/\?.*$/, '').replace(/[/-]+/g, '-')
await page.click(`[href^="${nav}"]`) await page.click(`[href^="${nav}"]`)
@ -1098,7 +1046,7 @@ describe('nested suspense', () => {
const first = start.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\//)!.groups! const first = start.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\//)!.groups!
const last = nav.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\//)!.groups! const last = nav.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\//)!.groups!
expect(logs.sort()).toEqual([ expect(consoleLogs.map(l => l.text).filter(i => !i.includes('[vite]') && !i.includes('<Suspense> is an experimental feature')).sort()).toEqual([
// [first load] from parent // [first load] from parent
`[${first.parentType}]`, `[${first.parentType}]`,
...first.parentType === 'async' ? ['[async] running async data'] : [], ...first.parentType === 'async' ? ['[async] running async data'] : [],
@ -1122,14 +1070,7 @@ describe('nested suspense', () => {
] ]
it.each(outwardNavigations)('should navigate from %s to a parent %s with no white flash', async (start, nav) => { it.each(outwardNavigations)('should navigate from %s to a parent %s with no white flash', async (start, nav) => {
const page = await createPage(start, {}) const { page, consoleLogs } = await renderPage(start)
const logs: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (text.includes('[vite]') || text.includes('<Suspense> is an experimental feature')) { return }
logs.push(msg.text())
})
await page.waitForLoadState('networkidle')
await page.waitForSelector(`main:has(#child${start.replace(/[/-]+/g, '-')})`) await page.waitForSelector(`main:has(#child${start.replace(/[/-]+/g, '-')})`)
@ -1146,7 +1087,7 @@ describe('nested suspense', () => {
const first = start.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\//)!.groups! const first = start.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\//)!.groups!
const last = nav.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\//)!.groups! const last = nav.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\//)!.groups!
expect(logs.sort()).toEqual([ expect(consoleLogs.map(l => l.text).filter(i => !i.includes('[vite]') && !i.includes('<Suspense> is an experimental feature')).sort()).toEqual([
// [first load] from parent // [first load] from parent
`[${first.parentType}]`, `[${first.parentType}]`,
...first.parentType === 'async' ? ['[async] running async data'] : [], ...first.parentType === 'async' ? ['[async] running async data'] : [],
@ -1167,14 +1108,7 @@ describe('nested suspense', () => {
] ]
it.each(inwardNavigations)('should navigate from %s to a child %s with no white flash', async (start, nav) => { it.each(inwardNavigations)('should navigate from %s to a child %s with no white flash', async (start, nav) => {
const page = await createPage(start, {}) const { page, consoleLogs } = await renderPage(start)
const logs: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (text.includes('[vite]') || text.includes('<Suspense> is an experimental feature')) { return }
logs.push(msg.text())
})
await page.waitForLoadState('networkidle')
const slug = nav.replace(/[/-]+/g, '-') const slug = nav.replace(/[/-]+/g, '-')
await page.click(`[href^="${nav}"]`) await page.click(`[href^="${nav}"]`)
@ -1190,7 +1124,7 @@ describe('nested suspense', () => {
const first = start.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\//)!.groups! const first = start.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\//)!.groups!
const last = nav.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\//)!.groups! const last = nav.match(/\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\//)!.groups!
expect(logs.sort()).toEqual([ expect(consoleLogs.map(l => l.text).filter(i => !i.includes('[vite]') && !i.includes('<Suspense> is an experimental feature')).sort()).toEqual([
// [first load] from parent // [first load] from parent
`[${first.parentType}]`, `[${first.parentType}]`,
...first.parentType === 'async' ? ['[async] running async data'] : [], ...first.parentType === 'async' ? ['[async] running async data'] : [],
@ -1208,80 +1142,64 @@ describe('nested suspense', () => {
// Bug #6592 // Bug #6592
describe('page key', () => { describe('page key', () => {
it('should not cause run of setup if navigation not change page key and layout', async () => { it.each(['/fixed-keyed-child-parent', '/internal-layout/fixed-keyed-child-parent'])('should not cause run of setup if navigation not change page key and layout', async (path) => {
async function behaviour (path: string) { const { page, consoleLogs } = await renderPage(`${path}/0`)
await withLogs(async (page, logs) => {
await page.goto(url(`${path}/0`))
await page.waitForLoadState('networkidle')
await page.click(`[href="${path}/1"]`) await page.click(`[href="${path}/1"]`)
await page.waitForSelector('#page-1') await page.waitForSelector('#page-1')
// Wait for all pending micro ticks to be cleared, // Wait for all pending micro ticks to be cleared,
// so we are not resolved too early when there are repeated page loading // so we are not resolved too early when there are repeated page loading
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
expect(logs.filter(l => l.includes('Child Setup')).length).toBe(1) expect(consoleLogs.filter(l => l.text.includes('Child Setup')).length).toBe(1)
}) await page.close()
}
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"]`) it.each(['/keyed-child-parent', '/internal-layout/keyed-child-parent'])('will cause run of setup if navigation changed page key', async (path) => {
await page.waitForSelector('#page-1') const { page, consoleLogs } = await renderPage(`${path}/0`)
// Wait for all pending micro ticks to be cleared, await page.click(`[href="${path}/1"]`)
// so we are not resolved too early when there are repeated page loading await page.waitForSelector('#page-1')
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
expect(logs.filter(l => l.includes('Child Setup')).length).toBe(2) // 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)))
await behaviour('/keyed-child-parent')
await behaviour('/internal-layout/keyed-child-parent') expect(consoleLogs.filter(l => l.text.includes('Child Setup')).length).toBe(2)
await page.close()
}) })
}) })
// Bug #6592 // Bug #6592
describe('layout change not load page twice', () => { describe('layout change not load page twice', () => {
async function behaviour (path1: string, path2: string) { const cases = {
await withLogs(async (page, logs) => { '/with-layout': '/with-layout2',
await page.goto(url(path1)) '/internal-layout/with-layout': '/internal-layout/with-layout2'
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') it.each(Object.entries(cases))('should not cause run of page setup to repeat if layout changed', async (path1, path2) => {
await behaviour('/internal-layout/with-layout', '/internal-layout/with-layout2') const { page, consoleLogs } = await renderPage(path1)
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(consoleLogs.filter(l => l.text.includes('Layout2 Page Setup')).length).toBe(1)
}) })
}) })
describe('layout switching', () => { describe('layout switching', () => {
// #13309 // #13309
it('does not cause TypeError: Cannot read properties of null', async () => { it('does not cause TypeError: Cannot read properties of null', async () => {
await withLogs(async (page, logs) => { const { page, consoleLogs, pageErrors } = await renderPage('/layout-switch/start')
await page.goto(url('/layout-switch/start')) await page.click('[href="/layout-switch/end"]')
await page.waitForLoadState('networkidle') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/layout-switch/end')
await page.click('[href="/layout-switch/end"]') expect(consoleLogs.map(i => i.text).filter(l => l.match(/error/i))).toMatchInlineSnapshot('[]')
// Wait for all pending micro ticks to be cleared, expect(pageErrors).toMatchInlineSnapshot('[]')
// so we are not resolved too early when there are repeated page loading await page.close()
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
expect(logs.filter(l => l.match(/error/i))).toMatchInlineSnapshot('[]')
})
}) })
}) })
@ -1372,8 +1290,7 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
}) })
it('still downloads client-only styles', async () => { it('still downloads client-only styles', async () => {
const page = await createPage('/styles') const { page } = await renderPage('/styles')
await page.waitForLoadState('networkidle')
expect(await page.$eval('.client-only-css', e => getComputedStyle(e).color)).toBe('rgb(50, 50, 50)') expect(await page.$eval('.client-only-css', e => getComputedStyle(e).color)).toBe('rgb(50, 50, 50)')
await page.close() await page.close()
@ -1387,13 +1304,12 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
describe('server components/islands', () => { describe('server components/islands', () => {
it('/islands', async () => { it('/islands', async () => {
const page = await createPage('/islands') const { page } = await renderPage('/islands')
await page.waitForLoadState('networkidle')
await page.locator('#increase-pure-component').click() await page.locator('#increase-pure-component').click()
await page.waitForResponse(response => response.url().includes('/__nuxt_island/') && response.status() === 200) await page.waitForResponse(response => response.url().includes('/__nuxt_island/') && response.status() === 200)
await page.waitForLoadState('networkidle')
expect(await page.locator('#slot-in-server').first().innerHTML()).toContain('Slot with in .server component') await page.locator('#slot-in-server').getByText('Slot with in .server component').waitFor()
expect(await page.locator('#test-slot').first().innerHTML()).toContain('Slot with name test') await page.locator('#test-slot').getByText('Slot with name test').waitFor()
// test fallback slot with v-for // test fallback slot with v-for
expect(await page.locator('.fallback-slot-content').all()).toHaveLength(2) expect(await page.locator('.fallback-slot-content').all()).toHaveLength(2)
@ -1404,9 +1320,9 @@ describe('server components/islands', () => {
page.waitForResponse(response => response.url().includes('/__nuxt_island/LongAsyncComponent') && response.status() === 200), page.waitForResponse(response => response.url().includes('/__nuxt_island/LongAsyncComponent') && response.status() === 200),
page.waitForResponse(response => response.url().includes('/__nuxt_island/AsyncServerComponent') && response.status() === 200) page.waitForResponse(response => response.url().includes('/__nuxt_island/AsyncServerComponent') && response.status() === 200)
]) ])
await page.waitForLoadState('networkidle')
expect(await page.locator('#async-server-component-count').innerHTML()).toContain(('1')) await page.locator('#async-server-component-count').getByText('1').waitFor()
expect(await page.locator('#long-async-component-count').innerHTML()).toContain('1') await page.locator('#long-async-component-count').getByText('1').waitFor()
// test islands slots interactivity // test islands slots interactivity
await page.locator('#first-sugar-counter button').click() await page.locator('#first-sugar-counter button').click()
@ -1420,7 +1336,7 @@ describe('server components/islands', () => {
}) })
it('lazy server components', async () => { it('lazy server components', async () => {
const page = await createPage('/server-components/lazy/start') const { page } = await renderPage('/server-components/lazy/start')
await page.waitForLoadState('networkidle') await page.waitForLoadState('networkidle')
await page.getByText('Go to page with lazy server component').click() await page.getByText('Go to page with lazy server component').click()
@ -1439,7 +1355,7 @@ describe('server components/islands', () => {
}) })
it('non-lazy server components', async () => { it('non-lazy server components', async () => {
const page = await createPage('/server-components/lazy/start') const { page } = await renderPage('/server-components/lazy/start')
await page.waitForLoadState('networkidle') await page.waitForLoadState('networkidle')
await page.getByText('Go to page without lazy server component').click() await page.getByText('Go to page without lazy server component').click()
@ -1477,14 +1393,9 @@ describe.skipIf(isDev() || isWindows || !isRenderingJson)('prefetching', () => {
}) })
it('should prefetch everything needed when NuxtLink is used', async () => { it('should prefetch everything needed when NuxtLink is used', async () => {
const page = await createPage() const { page, requests } = await renderPage()
const requests: string[] = []
page.on('request', (req) => { await gotoPath(page, '/prefetch')
requests.push(req.url().replace(url('/'), '/').replace(/\.[^.]+\./g, '.'))
})
await page.goto(url('/prefetch'))
await page.waitForLoadState('networkidle') await page.waitForLoadState('networkidle')
const snapshot = [...requests] const snapshot = [...requests]
@ -1782,15 +1693,15 @@ describe('component islands', () => {
}) })
it('test client-side navigation', async () => { it('test client-side navigation', async () => {
const page = await createPage('/') const { page } = await renderPage('/')
await page.waitForLoadState('networkidle')
await page.click('#islands') await page.click('#islands')
await page.waitForLoadState('networkidle') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/islands')
await page.locator('#increase-pure-component').click() await page.locator('#increase-pure-component').click()
await page.waitForResponse(response => response.url().includes('/__nuxt_island/') && response.status() === 200) await page.waitForResponse(response => response.url().includes('/__nuxt_island/') && response.status() === 200)
await page.waitForLoadState('networkidle')
expect(await page.locator('#slot-in-server').first().innerHTML()).toContain('Slot with in .server component') await page.locator('#slot-in-server').getByText('Slot with in .server component').waitFor()
expect(await page.locator('#test-slot').first().innerHTML()).toContain('Slot with name test') await page.locator('#test-slot').getByText('Slot with name test').waitFor()
// test islands update // test islands update
expect(await page.locator('.box').innerHTML()).toContain('"number": 101,') expect(await page.locator('.box').innerHTML()).toContain('"number": 101,')
@ -1799,9 +1710,8 @@ describe('component islands', () => {
page.waitForResponse(response => response.url().includes('/__nuxt_island/LongAsyncComponent') && response.status() === 200), page.waitForResponse(response => response.url().includes('/__nuxt_island/LongAsyncComponent') && response.status() === 200),
page.waitForResponse(response => response.url().includes('/__nuxt_island/AsyncServerComponent') && response.status() === 200) page.waitForResponse(response => response.url().includes('/__nuxt_island/AsyncServerComponent') && response.status() === 200)
]) ])
await page.waitForLoadState('networkidle')
expect(await page.locator('#async-server-component-count').innerHTML()).toContain(('1')) await page.locator('#long-async-component-count').getByText('1').waitFor()
expect(await page.locator('#long-async-component-count').innerHTML()).toContain('1')
// test islands slots interactivity // test islands slots interactivity
await page.locator('#first-sugar-counter button').click() await page.locator('#first-sugar-counter button').click()
@ -1848,18 +1758,12 @@ describe.skipIf(isDev() || isWindows || !isRenderingJson)('payload rendering', (
}) })
it('does not fetch a prefetched payload', async () => { it('does not fetch a prefetched payload', async () => {
const page = await createPage() const { page, requests } = await renderPage()
const requests = [] as string[]
page.on('request', (req) => { await gotoPath(page, '/random/a')
requests.push(req.url().replace(url('/'), '/'))
})
await page.goto(url('/random/a'))
await page.waitForLoadState('networkidle')
// We are manually prefetching other payloads // We are manually prefetching other payloads
expect(requests).toContain('/random/c/_payload.json') await page.waitForRequest(url('/random/c/_payload.json'))
// We are not triggering API requests in the payload // We are not triggering API requests in the payload
expect(requests).not.toContain(expect.stringContaining('/api/random')) expect(requests).not.toContain(expect.stringContaining('/api/random'))
@ -1935,10 +1839,7 @@ describe.skipIf(isWindows)('useAsyncData', () => {
expect(html).not.toContain('false') expect(html).not.toContain('false')
const page = await createPage('/useAsyncData/status') const page = await createPage('/useAsyncData/status')
await page.waitForLoadState('networkidle') await page.locator('#status5-values').getByText('idle,pending,success').waitFor()
expect(await page.locator('#status5-values').textContent()).toContain('idle,pending,success')
await page.close() await page.close()
}) })
}) })

View File

@ -17,7 +17,7 @@ if (process.env.TEST_ENV !== 'built' && !isWindows) {
dev: true, dev: true,
server: true, server: true,
browser: true, browser: true,
setupTimeout: (isWindows ? 240 : 120) * 1000, setupTimeout: (isWindows ? 360 : 120) * 1000,
nuxtConfig: { nuxtConfig: {
builder: isWebpack ? 'webpack' : 'vite', builder: isWebpack ? 'webpack' : 'vite',
buildDir: process.env.NITRO_BUILD_DIR, buildDir: process.env.NITRO_BUILD_DIR,

View File

@ -1,7 +1,7 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { isWindows } from 'std-env'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils' import { $fetch, setup } from '@nuxt/test-utils'
import { isWindows } from 'std-env'
import { expectNoClientErrors, renderPage } from './utils' import { expectNoClientErrors, renderPage } from './utils'
const isWebpack = process.env.TEST_BUILDER === 'webpack' const isWebpack = process.env.TEST_BUILDER === 'webpack'
@ -10,7 +10,7 @@ await setup({
dev: process.env.TEST_ENV === 'dev', dev: process.env.TEST_ENV === 'dev',
server: true, server: true,
browser: true, browser: true,
setupTimeout: (isWindows ? 240 : 120) * 1000, setupTimeout: (isWindows ? 360 : 120) * 1000,
nuxtConfig: { nuxtConfig: {
builder: isWebpack ? 'webpack' : 'vite' builder: isWebpack ? 'webpack' : 'vite'
} }

View File

@ -4,7 +4,7 @@ import type { Page } from 'playwright-core'
import { parse } from 'devalue' import { parse } from 'devalue'
import { reactive, ref, shallowReactive, shallowRef } from 'vue' import { reactive, ref, shallowReactive, shallowRef } from 'vue'
import { createError } from 'h3' import { createError } from 'h3'
import { createPage, getBrowser, url, useTestContext } from '@nuxt/test-utils' import { getBrowser, url, useTestContext } from '@nuxt/test-utils'
export const isRenderingJson = true export const isRenderingJson = true
@ -17,6 +17,7 @@ export async function renderPage (path = '/') {
const browser = await getBrowser() const browser = await getBrowser()
const page = await browser.newPage({}) const page = await browser.newPage({})
const pageErrors: Error[] = [] const pageErrors: Error[] = []
const requests: string[] = []
const consoleLogs: { type: string, text: string }[] = [] const consoleLogs: { type: string, text: string }[] = []
page.on('console', (message) => { page.on('console', (message) => {
@ -28,14 +29,19 @@ export async function renderPage (path = '/') {
page.on('pageerror', (err) => { page.on('pageerror', (err) => {
pageErrors.push(err) pageErrors.push(err)
}) })
page.on('request', (req) => {
requests.push(req.url().replace(url('/'), '/'))
})
if (path) { if (path) {
await page.goto(url(path), { waitUntil: 'networkidle' }) await page.goto(url(path), { waitUntil: 'networkidle' })
await page.waitForFunction(() => window.useNuxtApp?.())
} }
return { return {
page, page,
pageErrors, pageErrors,
requests,
consoleLogs consoleLogs
} }
} }
@ -58,6 +64,11 @@ export async function expectNoClientErrors (path: string) {
await page.close() await page.close()
} }
export async function gotoPath (page: Page, path: string) {
await page.goto(url(path))
await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, path)
}
type EqualityVal = string | number | boolean | null | undefined | RegExp type EqualityVal = string | number | boolean | null | undefined | RegExp
export async function expectWithPolling ( export async function expectWithPolling (
get: () => Promise<EqualityVal> | EqualityVal, get: () => Promise<EqualityVal> | EqualityVal,
@ -76,26 +87,6 @@ export async function expectWithPolling (
expect(result?.toString(), `"${result?.toString()}" did not equal "${expected?.toString()}" in ${retries * delay}ms`).toEqual(expected?.toString()) expect(result?.toString(), `"${result?.toString()}" did not equal "${expected?.toString()}" in ${retries * delay}ms`).toEqual(expected?.toString())
} }
export async function withLogs (callback: (page: Page, logs: string[]) => Promise<void>) {
let done = false
const page = await createPage()
const logs: string[] = []
page.on('console', (msg) => {
const text = msg.text()
if (done && !text.includes('[vite] server connection lost')) {
throw new Error(`Test finished prematurely before log: [${msg.type()}] ${text}`)
}
logs.push(text)
})
try {
await callback(page, logs)
} finally {
done = true
await page.close()
}
}
const revivers = { const revivers = {
NuxtError: (data: any) => createError(data), NuxtError: (data: any) => createError(data),
EmptyShallowRef: (data: any) => shallowRef(JSON.parse(data)), EmptyShallowRef: (data: any) => shallowRef(JSON.parse(data)),