import { readdir } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { joinURL, withQuery } from 'ufo' import { isCI, isWindows } from 'std-env' import { join, normalize } from 'pathe' import { $fetch, createPage, fetch, isDev, setup, startServer, url, useTestContext } from '@nuxt/test-utils/e2e' import { $fetchComponent } from '@nuxt/test-utils/experimental' import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage } from './utils' import type { NuxtIslandResponse } from '#app' const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack' const isTestingAppManifest = process.env.TEST_MANIFEST !== 'manifest-off' const isV4 = process.env.TEST_V4 === 'true' await setup({ rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), dev: process.env.TEST_ENV === 'dev', server: true, browser: true, setupTimeout: 360 * 1000, nuxtConfig: { hooks: { 'modules:done' () { // TODO: investigate whether to upstream a fix to vite-plugin-vue or nuxt/test-utils // Vite reads its `isProduction` value from NODE_ENV and passes this to some plugins // like vite-plugin-vue if (process.env.TEST_ENV !== 'dev') { process.env.NODE_ENV = 'production' } }, }, builder: isWebpack ? 'webpack' : 'vite', }, }) describe('server api', () => { it('should serialize', async () => { expect(await $fetch('/api/hello')).toBe('Hello API') expect(await $fetch('/api/hey')).toEqual({ foo: 'bar', baz: 'qux', }) }) it('should preserve states', async () => { expect(await $fetch('/api/counter')).toEqual({ count: 0 }) expect(await $fetch('/api/counter')).toEqual({ count: 1 }) expect(await $fetch('/api/counter')).toEqual({ count: 2 }) expect(await $fetch('/api/counter')).toEqual({ count: 3 }) }) it('should auto-import', async () => { const res = await $fetch('/api/auto-imports') expect(res).toMatchInlineSnapshot(` { "autoImported": "utils", "fromServerDir": "test-utils", "thisIs": "serverAutoImported", } `) }) }) describe('route rules', () => { it('should enable spa mode', async () => { const headHtml = await $fetch('/route-rules/spa') // SPA should render appHead tags expect(headHtml).toContain('') expect(headHtml).toContain('') expect(headHtml).toContain('') expect(headHtml.match(//g)).toHaveLength(1) const { script, attrs } = parseData(headHtml) expect(script.serverRendered).toEqual(false) if (isRenderingJson) { expect(attrs['data-ssr']).toEqual('false') } await expectNoClientErrors('/route-rules/spa') }) it('should not render loading template in spa mode if it is not enabled', async () => { const html = await $fetch('/route-rules/spa') expect(html).toContain('
') }) it('should allow defining route rules inline', async () => { const res = await fetch('/route-rules/inline') expect(res.status).toEqual(200) expect(res.headers.get('x-extend')).toEqual('added in routeRules') }) it('test noScript routeRules', async () => { const html = await $fetch('/no-scripts') expect(html).not.toContain(' { const html = await $fetch('/route-rules/middleware') expect(html).toContain('Hello from routeRules!') }) }) describe('modules', () => { it('should auto-register modules in ~/modules', async () => { const result = await $fetch('/auto-registered-module') expect(result).toEqual('handler added by auto-registered module') }) }) describe('pages', () => { it('render index', async () => { const html = await $fetch('/') // Snapshot // expect(html).toMatchInlineSnapshot() // should render text expect(html).toContain('Hello Nuxt 3!') // should inject runtime config expect(html).toContain('RuntimeConfig | testConfig: 123') expect(html).toContain('needsFallback:') // composables auto import expect(html).toContain('Composable | foo: auto imported from ~/composables/foo.ts') expect(html).toContain('Composable | bar: auto imported from ~/utils/useBar.ts') expect(html).toContain('Composable | template: auto imported from ~/composables/template.ts') expect(html).toContain('Composable | star: auto imported from ~/composables/nested/bar.ts via star export') // should import components expect(html).toContain('This is a custom component with a named export.') // should remove dev-only and replace with any fallback content expect(html).toContain(isDev() ? 'Some dev-only info' : 'Some prod-only info') // should apply attributes to client-only components expect(html).toContain('
') // should render server-only components expect(html.replaceAll(/ data-island-uid="[^"]*"/g, '')).toContain('
server-only component
server-only component child (non-server-only)
') // should register global components automatically expect(html).toContain('global component registered automatically') expect(html).toContain('global component via suffix') expect(html).toContain('This is a synchronously registered global component') await expectNoClientErrors('/') }) // TODO: support jsx with webpack it.runIf(!isWebpack)('supports jsx', async () => { const html = await $fetch('/jsx') // should import JSX/TSX components with custom elements expect(html).toContain('TSX component') expect(html).toContain('custom') expect(html).toContain('Sugar Counter 12 x 2 = 24') }) it('respects aliases in page metadata', async () => { const html = await $fetch('/some-alias') expect(html).toContain('Hello Nuxt 3!') }) it('respects redirects in page metadata', async () => { const { headers } = await fetch('/redirect', { redirect: 'manual' }) expect(headers.get('location')).toEqual('/') }) it('allows routes to be added dynamically', async () => { const html = await $fetch('/add-route-test') expect(html).toContain('Hello Nuxt 3!') }) it('includes page metadata from pages added in pages:extend hook', async () => { const res = await fetch('/page-extend') expect(res.headers.get('x-extend')).toEqual('added in pages:extend') }) it('preserves page metadata added in pages:extend hook', async () => { const html = await $fetch('/some-custom-path') expect (html.match(/
([^<]*)<\/pre>/)?.[1]?.trim().replace(/"/g, '"').replace(/>/g, '>')).toMatchInlineSnapshot(`
      "{
        "name": "some-custom-name",
        "path": "/some-custom-path",
        "validate": "() => true",
        "middleware": [
          "() => true"
        ],
        "otherValue": "{\\"foo\\":\\"bar\\"}"
      }"
    `)
  })

  it('validates routes', async () => {
    const { status, headers } = await fetch('/catchall/forbidden')
    expect(status).toEqual(404)
    expect(headers.get('Set-Cookie')).toBe('set-in-plugin=true; Path=/')

    const { page } = await renderPage('/navigate-to-forbidden')

    await page.getByText('should throw a 404 error').click()
    expect(await page.getByRole('heading').textContent()).toMatchInlineSnapshot('"Page Not Found: /catchall/forbidden"')
    expect(await page.getByTestId('path').textContent()).toMatchInlineSnapshot('" Path: /catchall/forbidden"')

    await gotoPath(page, '/navigate-to-forbidden')
    await page.getByText('should be caught by catchall').click()
    expect(await page.getByRole('heading').textContent()).toMatchInlineSnapshot('"[...slug].vue"')

    await page.close()
  })

  it('validates routes with custom statusCode and statusMessage', async () => {
    const CUSTOM_ERROR_CODE = 401
    const CUSTOM_ERROR_MESSAGE = 'Custom error message'
    const ERROR_PAGE_TEXT = 'This is the error page'
    const PAGE_TEXT = 'You should not see this'

    // Check status code and message on fetch
    const res = await fetch('/validate-custom-error')
    const serverText = await res.text()

    expect(res.status).toEqual(CUSTOM_ERROR_CODE)
    expect(serverText).toContain(CUSTOM_ERROR_MESSAGE)
    expect(serverText).not.toContain(PAGE_TEXT)

    // Client-side navigation
    const { page } = await renderPage('/navigate-to-validate-custom-error')
    await page.getByText('should throw a 401 error with custom message').click()
    // error.vue has an h1 tag
    await page.waitForSelector('h1')

    const clientText = await page.innerText('body')

    expect(clientText).toContain(CUSTOM_ERROR_MESSAGE)
    expect(clientText).toContain(ERROR_PAGE_TEXT)
    expect(clientText).not.toContain(PAGE_TEXT)

    await page.close()

    // Server-side navigation
    const { page: serverPage } = await renderPage('/validate-custom-error')
    const serverPageText = await serverPage.innerText('body')

    expect(serverPageText).toContain(CUSTOM_ERROR_MESSAGE)
    expect(serverPageText).toContain(ERROR_PAGE_TEXT)
    expect(serverPageText).not.toContain(PAGE_TEXT)

    await serverPage.close()
  })

  it.runIf(isDev())('returns 500 when there is an infinite redirect', async () => {
    const { status } = await fetch('/catchall/redirect-infinite', { redirect: 'manual' })
    expect(status).toEqual(500)
  })

  it('render catchall page', async () => {
    const res = await fetch('/catchall/not-found')
    expect(res.status).toEqual(200)

    const html = await res.text()

    // Snapshot
    // expect(html).toMatchInlineSnapshot()

    expect(html).toContain('[...slug].vue')
    expect(html).toContain('catchall at not-found')

    // Middleware still runs after validation: https://github.com/nuxt/nuxt/issues/15650
    expect(html).toContain('Middleware ran: true')

    await expectNoClientErrors('/catchall/not-found')
  })

  it('should render correctly when loaded on a different path', async () => {
    const { page, pageErrors } = await renderPage()
    await page.goto(url('/proxy'))
    await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)

    expect(await page.innerText('body')).toContain('Composable | foo: auto imported from ~/composables/foo.ts')
    expect(pageErrors).toEqual([])

    await page.close()
  })

  it('preserves query', async () => {
    const html = await $fetch('/?test=true')

    // Snapshot
    // expect(html).toMatchInlineSnapshot()

    // should render text
    expect(html).toContain('Path: /?test=true')

    await expectNoClientErrors('/?test=true')
  })

  it('/nested/[foo]/[bar].vue', async () => {
    const html = await $fetch('/nested/one/two')

    // Snapshot
    // expect(html).toMatchInlineSnapshot()

    expect(html).toContain('nested/[foo]/[bar].vue')
    expect(html).toContain('foo: one')
    expect(html).toContain('bar: two')
  })

  it('/nested/[foo]/index.vue', async () => {
    const html = await $fetch('/nested/foobar')

    // TODO: should resolved to same entry
    // const html2 = await $fetch('/nested/foobar/index')
    // expect(html).toEqual(html2)

    // Snapshot
    // expect(html).toMatchInlineSnapshot()

    expect(html).toContain('nested/[foo]/index.vue')
    expect(html).toContain('foo: foobar')

    await expectNoClientErrors('/nested/foobar')
  })

  it('/nested/[foo]/user-[group].vue', async () => {
    const html = await $fetch('/nested/foobar/user-admin')

    // Snapshot
    // expect(html).toMatchInlineSnapshot()

    expect(html).toContain('nested/[foo]/user-[group].vue')
    expect(html).toContain('foo: foobar')
    expect(html).toContain('group: admin')

    await expectNoClientErrors('/nested/foobar/user-admin')
  })

  it('/parent', async () => {
    const html = await $fetch('/parent')
    expect(html).toContain('parent/index')

    await expectNoClientErrors('/parent')
  })

  it('/another-parent', async () => {
    const html = await $fetch('/another-parent')
    expect(html).toContain('another-parent/index')

    await expectNoClientErrors('/another-parent')
  })

  it('/client-server', async () => {
    // expect no hydration issues
    await expectNoClientErrors('/client-server')
    const { page } = await renderPage('/client-server')
    const bodyHTML = await page.innerHTML('body')
    expect(await page.locator('.placeholder-to-ensure-no-override').all()).toHaveLength(5)
    expect(await page.locator('.server').all()).toHaveLength(0)
    expect(await page.locator('.client-fragment-server.client').all()).toHaveLength(2)
    expect(await page.locator('.client-fragment-server-fragment.client').all()).toHaveLength(2)
    expect(await page.locator('.client-server.client').all()).toHaveLength(1)
    expect(await page.locator('.client-server-fragment.client').all()).toHaveLength(1)
    expect(await page.locator('.client-server-fragment.client').all()).toHaveLength(1)

    expect(bodyHTML).not.toContain('hello')
    expect(bodyHTML).toContain('world')
    await page.close()
  })

  it('/client-only-components', async () => {
    const html = await $fetch('/client-only-components')
    // ensure fallbacks with classes and arbitrary attributes are rendered
    expect(html).toContain('
') expect(html).toContain('
') expect(html).toContain('
Fallback
') // ensure components are not rendered server-side expect(html).not.toContain('Should not be server rendered') const { page, pageErrors } = await renderPage('/client-only-components') const hiddenSelectors = [ '.string-stateful-should-be-hidden', '.client-script-should-be-hidden', '.string-stateful-script-should-be-hidden', '.no-state-hidden', ] const visibleSelectors = [ '.string-stateful', '.string-stateful-script', '.client-only-script', '.client-only-script-setup', '.no-state', ] // ensure directives are correctly applied await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isHidden())) .then(results => results.forEach(isHidden => expect(isHidden).toBeTruthy())) // ensure hidden components are still rendered await Promise.all(hiddenSelectors.map(selector => page.locator(selector).innerHTML())) .then(results => results.forEach(innerHTML => expect(innerHTML).not.toBe(''))) // ensure single root node components are rendered once on client (should not be empty) await Promise.all(visibleSelectors.map(selector => page.locator(selector).innerHTML())) .then(results => results.forEach(innerHTML => expect(innerHTML).not.toBe(''))) // issue #20061 expect(await page.$eval('.client-only-script-setup', e => getComputedStyle(e).backgroundColor)).toBe('rgb(255, 0, 0)') // ensure multi-root-node is correctly rendered expect(await page.locator('.multi-root-node-count').innerHTML()).toContain('0') expect(await page.locator('.multi-root-node-button').innerHTML()).toContain('add 1 to count') expect(await page.locator('.multi-root-node-script-count').innerHTML()).toContain('0') expect(await page.locator('.multi-root-node-script-button').innerHTML()).toContain('add 1 to count') // ensure components reactivity await page.locator('.multi-root-node-button').click() await page.locator('.multi-root-node-script-button').click() await page.locator('.client-only-script button').click() await page.locator('.client-only-script-setup button').click() expect(await page.locator('.multi-root-node-count').innerHTML()).toContain('1') expect(await page.locator('.multi-root-node-script-count').innerHTML()).toContain('1') expect(await page.locator('.client-only-script-setup button').innerHTML()).toContain('1') expect(await page.locator('.client-only-script button').innerHTML()).toContain('1') // ensure components ref is working and reactive await page.locator('button.test-ref-1').click() await page.locator('button.test-ref-2').click() await page.locator('button.test-ref-3').click() await page.locator('button.test-ref-4').click() expect(await page.locator('.client-only-script-setup button').innerHTML()).toContain('2') expect(await page.locator('.client-only-script button').innerHTML()).toContain('2') expect(await page.locator('.string-stateful-script').innerHTML()).toContain('1') expect(await page.locator('.string-stateful').innerHTML()).toContain('1') const waitForConsoleLog = page.waitForEvent('console', consoleLog => consoleLog.text() === 'has $el') // ensure directives are reactive await page.locator('button#show-all').click() await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible())) .then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy())) await waitForConsoleLog expect(pageErrors).toEqual([]) await page.close() // don't expect any errors or warning on client-side navigation const { page: page2, consoleLogs: consoleLogs2 } = await renderPage('/') await page2.locator('#to-client-only-components').click() await page2.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/client-only-components') expect(consoleLogs2.some(log => log.type === 'error' || log.type === 'warning')).toBeFalsy() await page2.close() }) it('/wrapper-expose/layout', async () => { const { page, consoleLogs, pageErrors } = await renderPage('/wrapper-expose/layout') await page.locator('.log-foo').first().click() expect(pageErrors.at(-1)?.toString() || consoleLogs.at(-1)!.text).toContain('.logFoo is not a function') await page.locator('.log-hello').first().click() expect(consoleLogs.at(-1)!.text).toContain('world') await page.locator('.add-count').first().click() expect(await page.locator('.count').first().innerText()).toContain('1') // change layout await page.locator('.swap-layout').click() await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('0')) await page.locator('.log-foo').first().click() expect(consoleLogs.at(-1)!.text).toContain('bar') await page.locator('.log-hello').first().click() expect(pageErrors.at(-1)?.toString() || consoleLogs.at(-1)!.text).toContain('.logHello is not a function') await page.locator('.add-count').first().click() await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('1')) // change layout await page.locator('.swap-layout').click() await page.waitForFunction(() => document.querySelector('.count')?.innerHTML.includes('0')) await page.close() }) it('/client-only-explicit-import', async () => { const html = await $fetch('/client-only-explicit-import') // ensure fallbacks with classes and arbitrary attributes are rendered expect(html).toContain('
') expect(html).toContain('
') // 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 () => { const { page, pageErrors, consoleLogs } = await renderPage('/wrapper-expose/page') await page.waitForLoadState('networkidle') await page.locator('#log-foo').click() expect(consoleLogs.at(-1)?.text).toBe('bar') // change page await page.locator('#to-hello').click() await page.locator('#log-foo').click() expect(pageErrors.at(-1)?.toString() || consoleLogs.at(-1)!.text).toContain('.foo is not a function') await page.locator('#log-hello').click() expect(consoleLogs.at(-1)?.text).toBe('world') await page.close() }) it('client-fallback', async () => { const classes = [ 'clientfallback-non-stateful-setup', 'clientfallback-non-stateful', 'clientfallback-stateful-setup', 'clientfallback-stateful', 'clientfallback-async-setup', ] 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') // async setup expect(html).toContain('Work with async setup') const { page, pageErrors } = await renderPage('/client-fallback') // 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 !') expect(pageErrors).toEqual([]) 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') }) it('client only page', async () => { const response = await fetch('/client-only-page').then(r => r.text()) // Should not contain rendered page on initial request expect(response).not.toContain('"hasAccessToWindow": true') expect(response).not.toContain('"isServer": false') const errors: string[] = [] const { page: clientInitialPage } = await renderPage('/client-only-page') clientInitialPage.on('console', (message) => { const type = message.type() if (type === 'error' || type === 'warning') { errors.push(message.text()) } }) // But after hydration element should appear and contain this object expect(await clientInitialPage.locator('#state').textContent()).toMatchInlineSnapshot(` "{ "hasAccessToWindow": true, "isServer": false }" `) expect(await clientInitialPage.locator('#server-rendered').textContent()).toMatchInlineSnapshot('"false"') // Then go to non client only page await clientInitialPage.click('a') await clientInitialPage.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/client-only-page/normal') // that page should be client rendered expect(await clientInitialPage.locator('#server-rendered').textContent()).toMatchInlineSnapshot('"false"') // and not contain any errors or warnings expect(errors.length).toBe(0) await clientInitialPage.close() errors.length = 0 const { page: normalInitialPage } = await renderPage('/client-only-page/normal') normalInitialPage.on('console', (message) => { const type = message.type() if (type === 'error' || type === 'warning') { errors.push(message.text()) } }) // Now non client only page should be sever rendered expect(await normalInitialPage.locator('#server-rendered').textContent()).toMatchInlineSnapshot('"true"') // Go to client only page await normalInitialPage.click('a') await normalInitialPage.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/client-only-page') // and expect same object to be present expect(await normalInitialPage.locator('#state').textContent()).toMatchInlineSnapshot(` "{ "hasAccessToWindow": true, "isServer": false }" `) // also there should not be any errors expect(errors.length).toBe(0) await normalInitialPage.close() }) it('groups routes', async () => { for (const targetRoute of ['/group-page', '/nested-group/group-page', '/nested-group']) { const { status } = await fetch(targetRoute) expect(status).toBe(200) } }) it.skipIf(isDev() || isWebpack /* TODO: fix bug with import.meta.prerender being undefined in webpack build */)('prerenders pages hinted with a route rule', async () => { const html = await $fetch('/prerender/test') expect(html).toContain('should be prerendered: true') }) it('should trigger page:loading:end only once', async () => { const { page, consoleLogs } = await renderPage('/') await page.getByText('to page load hook').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/page-load-hook') const loadingEndLogs = consoleLogs.filter(c => c.text.includes('page:loading:end')) expect(loadingEndLogs.length).toBe(1) await page.close() }) it('should hide nuxt page load indicator after navigate back from nested page', async () => { const LOAD_INDICATOR_SELECTOR = '.nuxt-loading-indicator' const { page } = await renderPage('/page-load-hook') await page.getByText('To sub page').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/page-load-hook/subpage') await page.waitForSelector(LOAD_INDICATOR_SELECTOR) let isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR) expect(isVisible).toBe(true) await page.waitForSelector(LOAD_INDICATOR_SELECTOR, { state: 'hidden' }) isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR) expect(isVisible).toBe(false) await page.goBack() await page.waitForSelector(LOAD_INDICATOR_SELECTOR) isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR) expect(isVisible).toBe(true) await page.waitForSelector(LOAD_INDICATOR_SELECTOR, { state: 'hidden' }) isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR) expect(isVisible).toBe(false) await page.close() }) }) 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=/, accessed-with-default-value=default; 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=/, browser-object-default=%7B%22foo%22%3A%22bar%22%7D; Path=/, theCookie=show; Path=/"') }) it('updates cookies when they are changed', async () => { const { page } = await renderPage('/cookies') async function extractCookie () { const cookie = await page.evaluate(() => document.cookie) const raw = cookie.match(/browser-object-default=([^;]*)/)![1] ?? 'null' return JSON.parse(decodeURIComponent(raw)) } expect(await extractCookie()).toEqual({ foo: 'bar' }) await page.getByText('Change cookie').click() expect(await extractCookie()).toEqual({ foo: 'baz' }) let text = await page.innerText('pre') expect(text).toContain('baz') await page.getByText('Change cookie').click() expect(await extractCookie()).toEqual({ foo: 'bar' }) await page.evaluate(() => { document.cookie = `browser-object-default=${encodeURIComponent('{"foo":"foobar"}')}` }) await page.getByText('Refresh cookie').click() text = await page.innerText('pre') expect(text).toContain('foobar') await page.close() }) it('sets cookies in composable to null in all components', async () => { const { page } = await renderPage('/cookies') const parentBannerText = await page.locator('#parent-banner').textContent() expect(parentBannerText).toContain('parent banner') const childBannerText = await page.locator('#child-banner').innerText() expect(childBannerText).toContain('child banner') // Clear the composable cookie await page.getByText('Toggle cookie banner').click() await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) const parentBannerAfterToggle = await page.locator('#parent-banner').isVisible() expect(parentBannerAfterToggle).toBe(false) const childBannerAfterToggle = await page.locator('#child-banner').isVisible() expect(childBannerAfterToggle).toBe(false) await page.close() }) it('supports onPrehydrate', async () => { const html = await $fetch('/composables/on-prehydrate') as string /** * Should look something like this: * * ```html *
onPrehydrate testing
* * * * ``` */ const { id1, id2 } = html.match(/]* data-prehydrate-id=":(?[^:]+)::(?[^:]+):"> onPrehydrate testing <\/div>/)?.groups || {} expect(id1).toBeTruthy() const matches = [ html.match(/]*>\(\(\)=>\{console.log\(window\)\}\)\(\)<\/script>/), html.match(new RegExp(`]*>document.querySelectorAll\\('\\[data-prehydrate-id\\*=":${id1}:"]'\\).forEach\\(o=>{console.log\\(o.outerHTML\\)}\\)`, 'i')), html.match(new RegExp(`]*>document.querySelectorAll\\('\\[data-prehydrate-id\\*=":${id2}:"]'\\).forEach\\(o=>{console.log\\("other",o.outerHTML\\)}\\)`, 'i')), ] // This tests we inject all scripts correctly, and only have one occurrence of multiple calls of a composable expect(matches.every(s => s?.length === 1)).toBeTruthy() // Check for hydration/syntax errors on client side await expectNoClientErrors('/composables/on-prehydrate') }) it('respects preview mode with a token', async () => { const token = 'hehe' const page = await createPage(`/preview?preview=true&token=${token}`) const hasRerunFetchOnClient = await new Promise((resolve) => { page.on('console', (message) => { setTimeout(() => resolve(false), 4000) if (message.text() === 'true') { resolve(true) } }) }) expect(hasRerunFetchOnClient).toBe(true) expect(await page.locator('#fetched-on-client').textContent()).toContain('fetched on client') expect(await page.locator('#preview-mode').textContent()).toContain('preview mode enabled') await page.click('#use-fetch-check') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath.includes('/preview/with-use-fetch')) expect(await page.locator('#token-check').textContent()).toContain(token) expect(await page.locator('#correct-api-key-check').textContent()).toContain('true') await page.close() }) it('respects preview mode with custom state', async () => { const { page } = await renderPage('/preview/with-custom-state?preview=true') expect(await page.locator('#data1').textContent()).toContain('data1 updated') expect(await page.locator('#data2').textContent()).toContain('data2') await page.click('#toggle-preview') // manually turns off preview mode await page.click('#with-use-fetch') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath.includes('/preview/with-use-fetch')) expect(await page.locator('#enabled').textContent()).toContain('false') expect(await page.locator('#token-check').textContent()).toEqual('') expect(await page.locator('#correct-api-key-check').textContent()).toContain('false') await page.close() }) it('respects preview mode with custom enable', async () => { const { page } = await renderPage('/preview/with-custom-enable?preview=true') expect(await page.locator('#enabled').textContent()).toContain('false') await page.close() }) it('respects preview mode with custom enable and customPreview', async () => { const { page } = await renderPage('/preview/with-custom-enable?customPreview=true') expect(await page.locator('#enabled').textContent()).toContain('true') await page.close() }) }) describe('rich payloads', () => { it('correctly serializes and revivifies complex types', async () => { const html = await $fetch('/json-payload') for (const test of [ 'Date: true', 'BigInt: true', 'Error: true', 'Shallow reactive: true', 'Shallow ref: true', 'Undefined ref: true', 'BigInt ref:', 'Reactive: true', 'Ref: true', 'Recursive objects: 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('respects external links in edge cases', async () => { const html = await $fetch('/nuxt-link/custom-external') const hrefs = html.match(/]*href="([^"]+)"/g) expect(hrefs).toMatchInlineSnapshot(` [ " c.text.includes('No match found for location')) expect(warnings).toMatchInlineSnapshot(`[]`) await page.close() }) it('preserves route state', async () => { const { page } = await renderPage('/nuxt-link/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.getByTestId('window-state').getByText('bar').waitFor() await page.locator(`.${selector}[href*=without-state]`).click() await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath.includes('without-state')) expect(await page.getByTestId('window-state').innerText()).not.toContain('bar') } await page.close() }) it('expect scroll to top on routes with same component', async () => { // #22402 const page = await createPage('/big-page-1', { viewport: { width: 1000, height: 1000, }, }) await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/big-page-1') await page.locator('#big-page-2').scrollIntoViewIfNeeded() await page.waitForFunction(() => window.scrollY > 0) await page.locator('#big-page-2').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/big-page-2') await page.waitForFunction(() => window.scrollY === 0) await page.locator('#big-page-1').scrollIntoViewIfNeeded() await page.waitForFunction(() => window.scrollY > 0) await page.locator('#big-page-1').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/big-page-1') await page.waitForFunction(() => window.scrollY === 0) await page.close() }) it('expect scroll to top on nested pages', async () => { // #20523 const page = await createPage('/nested/foo/test', { viewport: { width: 1000, height: 1000, }, }) await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/nested/foo/test') await page.locator('#user-test').scrollIntoViewIfNeeded() await page.waitForFunction(() => window.scrollY > 0) await page.locator('#user-test').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/nested/foo/user-test') await page.waitForFunction(() => window.scrollY === 0) await page.locator('#test').scrollIntoViewIfNeeded() await page.waitForFunction(() => window.scrollY > 0) await page.locator('#test').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/nested/foo/test') await page.waitForFunction(() => window.scrollY === 0) await page.close() }) it('useLink works', async () => { const html = await $fetch('/nuxt-link/use-link') expect(html).toContain('
useLink in NuxtLink: true
') expect(html).toContain('
route using useLink: /nuxt-link/trailing-slash
') expect(html).toContain('
href using useLink: /nuxt-link/trailing-slash
') expect(html).toContain('
useLink2 in NuxtLink: true
') expect(html).toContain('
route2 using useLink: /nuxt-link/trailing-slash
') expect(html).toContain('
href2 using useLink: /nuxt-link/trailing-slash
') expect(html).toContain('
useLink3 in NuxtLink: true
') expect(html).toContain('
route3 using useLink: /nuxt-link/trailing-slash
') expect(html).toContain('
href3 using useLink: /nuxt-link/trailing-slash
') }) it('useLink navigate importing NuxtLink works', async () => { const page = await createPage('/nuxt-link/use-link') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/nuxt-link/use-link') await page.locator('#button1').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/nuxt-link/trailing-slash') await page.close() }) it('useLink navigate using resolveComponent works', async () => { const page = await createPage('/nuxt-link/use-link') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/nuxt-link/use-link') await page.locator('#button2').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/nuxt-link/trailing-slash') await page.close() }) it('useLink navigate using resolveDynamicComponent works', async () => { const page = await createPage('/nuxt-link/use-link') await page.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/nuxt-link/use-link') await page.locator('#button3').click() await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/nuxt-link/trailing-slash') 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"/) const bodyHtml = headHtml.match(/]*>(.*)<\/body>/s)![1] expect(bodyHtml).toContain('') const indexHtml = await $fetch('/') // should render charset by default expect(indexHtml).toContain('') // should render components expect(indexHtml).toContain('Basic fixture - 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).not.toContain('