2023-06-25 16:38:15 +00:00
import { readdir } from 'node:fs/promises'
2022-04-15 15:19:05 +00:00
import { fileURLToPath } from 'node:url'
2022-02-18 18:14:57 +00:00
import { describe , expect , it } from 'vitest'
2022-11-24 12:24:14 +00:00
import { joinURL , withQuery } from 'ufo'
2023-02-14 00:02:41 +00:00
import { isCI , isWindows } from 'std-env'
2023-06-25 16:38:15 +00:00
import { join , normalize } from 'pathe'
2023-12-11 18:20:11 +00:00
import { $fetch , createPage , fetch , isDev , setup , startServer , url , useTestContext } from '@nuxt/test-utils/e2e'
2023-04-06 12:12:20 +00:00
import { $fetchComponent } from '@nuxt/test-utils/experimental'
2023-01-22 16:46:45 +00:00
2023-12-19 12:21:29 +00:00
import type { ConsoleMessage } from 'playwright-core'
2022-11-24 12:24:14 +00:00
import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
2023-08-12 07:18:58 +00:00
import { expectNoClientErrors , expectWithPolling , gotoPath , isRenderingJson , parseData , parsePayload , renderPage } from './utils'
2023-02-13 22:09:32 +00:00
const isWebpack = process . env . TEST_BUILDER === 'webpack'
2023-09-30 09:58:55 +00:00
const isTestingAppManifest = process . env . TEST_MANIFEST !== 'manifest-off'
2022-02-18 18:14:57 +00:00
2022-03-22 18:12:54 +00:00
await setup ( {
2023-02-13 22:09:32 +00:00
rootDir : fileURLToPath ( new URL ( './fixtures/basic' , import . meta . url ) ) ,
dev : process.env.TEST_ENV === 'dev' ,
2022-04-05 18:38:23 +00:00
server : true ,
2022-11-15 14:33:43 +00:00
browser : true ,
2023-08-12 07:18:58 +00:00
setupTimeout : ( isWindows ? 360 : 120 ) * 1000 ,
2023-02-13 22:09:32 +00:00
nuxtConfig : {
builder : isWebpack ? 'webpack' : 'vite' ,
buildDir : process.env.NITRO_BUILD_DIR ,
nitro : { output : { dir : process.env.NITRO_OUTPUT_DIR } }
}
2022-03-22 18:12:54 +00:00
} )
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'
2022-03-08 18:03:21 +00:00
} )
2022-03-22 18:12:54 +00:00
} )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
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 } )
} )
2023-09-28 10:08:02 +00:00
it ( 'should auto-import' , async ( ) = > {
const res = await $fetch ( '/api/auto-imports' )
expect ( res ) . toMatchInlineSnapshot ( `
{
"autoImported" : "utils" ,
"fromServerDir" : "test-utils" ,
"thisIs" : "serverAutoImported" ,
}
` )
} )
2022-03-22 18:12:54 +00:00
} )
2022-10-11 16:03:52 +00:00
describe ( 'route rules' , ( ) = > {
it ( 'should enable spa mode' , async ( ) = > {
2023-04-07 10:34:35 +00:00
const { script , attrs } = parseData ( await $fetch ( '/route-rules/spa' ) )
expect ( script . serverRendered ) . toEqual ( false )
2023-04-11 22:57:12 +00:00
if ( isRenderingJson ) {
expect ( attrs [ 'data-ssr' ] ) . toEqual ( 'false' )
}
2023-04-11 22:33:21 +00:00
await expectNoClientErrors ( '/route-rules/spa' )
2022-10-11 16:03:52 +00:00
} )
2023-04-11 14:17:44 +00:00
2023-12-13 11:54:56 +00:00
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 ( '<div id="__nuxt"></div>' )
} )
2023-08-23 20:38:17 +00:00
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' )
} )
2023-04-11 14:17:44 +00:00
it ( 'test noScript routeRules' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const html = await $fetch ( '/no-scripts' )
expect ( html ) . not . toContain ( '<script' )
2023-04-11 14:17:44 +00:00
} )
2022-10-11 16:03:52 +00:00
} )
2023-03-03 17:52:55 +00:00
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' )
} )
} )
2022-03-22 18:12:54 +00:00
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' )
2023-01-28 15:18:04 +00:00
expect ( html ) . toContain ( 'needsFallback:' )
2022-03-22 18:12:54 +00:00
// composables auto import
2023-03-01 12:24:46 +00:00
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' )
2022-03-22 18:12:54 +00:00
// should import components
expect ( html ) . toContain ( 'This is a custom component with a named export.' )
2023-05-13 22:32:31 +00:00
// should remove dev-only and replace with any fallback content
expect ( html ) . toContain ( isDev ( ) ? 'Some dev-only info' : 'Some prod-only info' )
2022-08-02 15:05:02 +00:00
// should apply attributes to client-only components
expect ( html ) . toContain ( '<div style="color:red;" class="client-only"></div>' )
2023-01-09 11:20:33 +00:00
// should render server-only components
2024-01-16 13:22:50 +00:00
expect ( html . replace ( / data-island-uid="[^"]*"/ , '' ) ) . toContain ( '<div class="server-only" style="background-color:gray;"> server-only component </div>' )
2022-08-02 15:05:02 +00:00
// should register global components automatically
2022-07-27 13:05:34 +00:00
expect ( html ) . toContain ( 'global component registered automatically' )
expect ( html ) . toContain ( 'global component via suffix' )
2023-08-09 11:19:00 +00:00
expect ( html ) . toContain ( 'This is a synchronously registered global component' )
2022-04-05 18:38:23 +00:00
2022-04-07 00:39:44 +00:00
await expectNoClientErrors ( '/' )
2022-03-08 18:03:21 +00:00
} )
2023-02-16 12:45:08 +00:00
// 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-component>custom</custom-component>' )
2023-07-26 04:30:44 +00:00
expect ( html ) . toContain ( 'Sugar Counter 12 x 2 = 24' )
2023-02-16 12:45:08 +00:00
} )
2022-09-22 13:54:34 +00:00
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 ( '/' )
} )
2023-06-27 10:15:35 +00:00
it ( 'allows routes to be added dynamically' , async ( ) = > {
const html = await $fetch ( '/add-route-test' )
expect ( html ) . toContain ( 'Hello Nuxt 3!' )
} )
2023-05-03 14:14:12 +00:00
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' )
} )
2022-10-10 10:18:20 +00:00
it ( 'validates routes' , async ( ) = > {
2023-05-01 22:55:24 +00:00
const { status , headers } = await fetch ( '/forbidden' )
2022-10-10 10:18:20 +00:00
expect ( status ) . toEqual ( 404 )
2023-05-01 22:55:24 +00:00
expect ( headers . get ( 'Set-Cookie' ) ) . toBe ( 'set-in-plugin=true; Path=/' )
2023-02-16 12:56:14 +00:00
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/navigate-to-forbidden' )
2023-02-16 12:56:14 +00:00
await page . getByText ( 'should throw a 404 error' ) . click ( )
expect ( await page . getByRole ( 'heading' ) . textContent ( ) ) . toMatchInlineSnapshot ( '"Page Not Found: /forbidden"' )
2023-12-11 20:30:59 +00:00
expect ( await page . getByTestId ( 'path' ) . textContent ( ) ) . toMatchInlineSnapshot ( '" Path: /forbidden"' )
2023-02-16 12:56:14 +00:00
2023-08-12 07:18:58 +00:00
await gotoPath ( page , '/navigate-to-forbidden' )
2023-02-16 12:56:14 +00:00
await page . getByText ( 'should be caught by catchall' ) . click ( )
expect ( await page . getByRole ( 'heading' ) . textContent ( ) ) . toMatchInlineSnapshot ( '"[...slug].vue"' )
2023-03-09 13:54:46 +00:00
await page . close ( )
2022-10-10 10:18:20 +00:00
} )
2023-04-13 10:14:44 +00:00
it ( 'returns 500 when there is an infinite redirect' , async ( ) = > {
const { status } = await fetch ( '/redirect-infinite' , { redirect : 'manual' } )
expect ( status ) . toEqual ( 500 )
} )
2023-05-01 22:55:24 +00:00
it ( 'render catchall page' , async ( ) = > {
const res = await fetch ( '/not-found' )
expect ( res . status ) . toEqual ( 200 )
const html = await res . text ( )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// Snapshot
// expect(html).toMatchInlineSnapshot()
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( '[...slug].vue' )
2023-02-16 12:56:14 +00:00
expect ( html ) . toContain ( 'catchall at not-found' )
2022-04-05 18:38:23 +00:00
2023-01-19 19:37:07 +00:00
// Middleware still runs after validation: https://github.com/nuxt/nuxt/issues/15650
2023-01-14 00:23:20 +00:00
expect ( html ) . toContain ( 'Middleware ran: true' )
2022-04-07 00:39:44 +00:00
await expectNoClientErrors ( '/not-found' )
2022-03-22 18:12:54 +00:00
} )
2022-03-08 18:03:21 +00:00
2023-07-03 11:14:17 +00:00
it ( 'expect no loading indicator on middleware abortNavigation' , async ( ) = > {
const { page } = await renderPage ( '/' )
await page . locator ( '#middleware-abort-non-fatal' ) . click ( )
expect ( await page . locator ( '#lodagin-indicator' ) . all ( ) ) . toHaveLength ( 0 )
await page . locator ( '#middleware-abort-non-fatal-error' ) . click ( )
expect ( await page . locator ( '#lodagin-indicator' ) . all ( ) ) . toHaveLength ( 0 )
2023-08-12 07:18:58 +00:00
await page . close ( )
2023-07-03 11:14:17 +00:00
} )
2023-06-06 21:47:32 +00:00
it ( 'should render correctly when loaded on a different path' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page , pageErrors } = await renderPage ( '/proxy' )
2023-06-06 21:47:32 +00:00
expect ( await page . innerText ( 'body' ) ) . toContain ( 'Composable | foo: auto imported from ~/composables/foo.ts' )
2023-06-14 09:09:27 +00:00
await page . close ( )
2023-08-12 07:18:58 +00:00
expect ( pageErrors ) . toEqual ( [ ] )
2023-06-06 21:47:32 +00:00
} )
2022-05-03 09:31:58 +00:00
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' )
} )
2022-03-22 18:12:54 +00:00
it ( '/nested/[foo]/[bar].vue' , async ( ) = > {
const html = await $fetch ( '/nested/one/two' )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// Snapshot
// expect(html).toMatchInlineSnapshot()
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( 'nested/[foo]/[bar].vue' )
expect ( html ) . toContain ( 'foo: one' )
expect ( html ) . toContain ( 'bar: two' )
} )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
it ( '/nested/[foo]/index.vue' , async ( ) = > {
const html = await $fetch ( '/nested/foobar' )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// TODO: should resolved to same entry
// const html2 = await $fetch('/nested/foobar/index')
// expect(html).toEqual(html2)
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// Snapshot
// expect(html).toMatchInlineSnapshot()
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( 'nested/[foo]/index.vue' )
expect ( html ) . toContain ( 'foo: foobar' )
2022-04-05 18:38:23 +00:00
2022-04-07 00:39:44 +00:00
await expectNoClientErrors ( '/nested/foobar' )
2022-03-22 18:12:54 +00:00
} )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
it ( '/nested/[foo]/user-[group].vue' , async ( ) = > {
const html = await $fetch ( '/nested/foobar/user-admin' )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// Snapshot
// expect(html).toMatchInlineSnapshot()
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( 'nested/[foo]/user-[group].vue' )
expect ( html ) . toContain ( 'foo: foobar' )
expect ( html ) . toContain ( 'group: admin' )
2022-04-05 18:38:23 +00:00
2022-04-07 00:39:44 +00:00
await expectNoClientErrors ( '/nested/foobar/user-admin' )
2022-03-22 18:12:54 +00:00
} )
2022-03-25 11:55:05 +00:00
it ( '/parent' , async ( ) = > {
const html = await $fetch ( '/parent' )
expect ( html ) . toContain ( 'parent/index' )
2022-04-05 18:38:23 +00:00
2022-04-07 00:39:44 +00:00
await expectNoClientErrors ( '/parent' )
2022-03-25 11:55:05 +00:00
} )
it ( '/another-parent' , async ( ) = > {
const html = await $fetch ( '/another-parent' )
expect ( html ) . toContain ( 'another-parent/index' )
2022-04-05 18:38:23 +00:00
2022-04-07 00:39:44 +00:00
await expectNoClientErrors ( '/another-parent' )
2022-03-25 11:55:05 +00:00
} )
2022-08-17 15:26:51 +00:00
2023-10-16 13:09:54 +00:00
it ( '/client-server' , async ( ) = > {
// expect no hydration issues
await expectNoClientErrors ( '/client-server' )
const page = await createPage ( '/client-server' )
await page . waitForLoadState ( 'networkidle' )
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' )
} )
2022-08-17 15:26:51 +00:00
it ( '/client-only-components' , async ( ) = > {
const html = await $fetch ( '/client-only-components' )
2022-10-03 14:14:55 +00:00
// ensure fallbacks with classes and arbitrary attributes are rendered
2022-08-17 15:26:51 +00:00
expect ( html ) . toContain ( '<div class="client-only-script" foo="bar">' )
expect ( html ) . toContain ( '<div class="client-only-script-setup" foo="hello">' )
2022-09-20 06:24:45 +00:00
expect ( html ) . toContain ( '<div>Fallback</div>' )
2022-10-03 14:14:55 +00:00
// ensure components are not rendered server-side
2022-09-20 06:24:45 +00:00
expect ( html ) . not . toContain ( 'Should not be server rendered' )
2022-08-17 15:26:51 +00:00
2023-08-12 07:18:58 +00:00
const { page , pageErrors } = await renderPage ( '/client-only-components' )
2022-10-03 14:14:55 +00:00
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'
]
2023-04-07 08:31:04 +00:00
2022-10-03 14:14:55 +00:00
// 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 ( '' ) ) )
2023-04-07 08:31:04 +00:00
// issue #20061
expect ( await page . $eval ( '.client-only-script-setup' , e = > getComputedStyle ( e ) . backgroundColor ) ) . toBe ( 'rgb(255, 0, 0)' )
2022-10-03 14:14:55 +00:00
// 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' )
// 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 ( ) ) )
2023-03-09 13:54:46 +00:00
2023-08-12 07:18:58 +00:00
expect ( pageErrors ) . toEqual ( [ ] )
2023-03-09 13:54:46 +00:00
await page . close ( )
2023-10-25 00:34:22 +00:00
// 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 ( )
// force wait for a few ticks
await page2 . waitForTimeout ( 50 )
expect ( consoleLogs2 . some ( log = > log . type === 'error' || log . type === 'warning' ) ) . toBeFalsy ( )
await page2 . close ( )
2022-08-17 15:26:51 +00:00
} )
2022-10-11 15:26:03 +00:00
2023-06-10 22:17:14 +00:00
it ( '/wrapper-expose/layout' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page , consoleLogs , pageErrors } = await renderPage ( '/wrapper-expose/layout' )
2023-06-10 22:17:14 +00:00
await page . locator ( '.log-foo' ) . first ( ) . click ( )
2023-08-12 07:18:58 +00:00
expect ( pageErrors . at ( - 1 ) ? . toString ( ) || consoleLogs . at ( - 1 ) ! . text ) . toContain ( '.logFoo is not a function' )
2023-06-10 22:17:14 +00:00
await page . locator ( '.log-hello' ) . first ( ) . click ( )
2023-08-12 07:18:58 +00:00
expect ( consoleLogs . at ( - 1 ) ! . text ) . toContain ( 'world' )
2023-06-10 22:17:14 +00:00
await page . locator ( '.add-count' ) . first ( ) . click ( )
expect ( await page . locator ( '.count' ) . first ( ) . innerText ( ) ) . toContain ( '1' )
// change layout
await page . locator ( '.swap-layout' ) . click ( )
2023-06-23 10:02:01 +00:00
await page . waitForFunction ( ( ) = > document . querySelector ( '.count' ) ? . innerHTML . includes ( '0' ) )
2023-06-10 22:17:14 +00:00
await page . locator ( '.log-foo' ) . first ( ) . click ( )
2023-08-12 07:18:58 +00:00
expect ( consoleLogs . at ( - 1 ) ! . text ) . toContain ( 'bar' )
2023-06-10 22:17:14 +00:00
await page . locator ( '.log-hello' ) . first ( ) . click ( )
2023-08-12 07:18:58 +00:00
expect ( pageErrors . at ( - 1 ) ? . toString ( ) || consoleLogs . at ( - 1 ) ! . text ) . toContain ( '.logHello is not a function' )
2023-06-10 22:17:14 +00:00
await page . locator ( '.add-count' ) . first ( ) . click ( )
2023-06-23 10:02:01 +00:00
await page . waitForFunction ( ( ) = > document . querySelector ( '.count' ) ? . innerHTML . includes ( '1' ) )
2023-06-10 22:17:14 +00:00
// change layout
await page . locator ( '.swap-layout' ) . click ( )
2023-06-23 10:02:01 +00:00
await page . waitForFunction ( ( ) = > document . querySelector ( '.count' ) ? . innerHTML . includes ( '0' ) )
2023-08-12 07:18:58 +00:00
await page . close ( )
2023-06-10 22:17:14 +00:00
} )
2022-10-11 15:26:03 +00:00
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 ( '<div class="client-only-script" foo="bar">' )
expect ( html ) . toContain ( '<div class="lazy-client-only-script-setup" foo="hello">' )
// ensure components are not rendered server-side
expect ( html ) . not . toContain ( 'client only script' )
2022-10-16 09:03:52 +00:00
await expectNoClientErrors ( '/client-only-explicit-import' )
2022-10-11 15:26:03 +00:00
} )
2023-03-08 21:13:06 +00:00
2023-06-10 22:13:33 +00:00
it ( '/wrapper-expose/page' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page , pageErrors , consoleLogs } = await renderPage ( '/wrapper-expose/page' )
2023-06-10 22:13:33 +00:00
await page . locator ( '#log-foo' ) . click ( )
2023-08-12 07:18:58 +00:00
expect ( consoleLogs . at ( - 1 ) ? . text ) . toBe ( 'bar' )
2023-06-10 22:13:33 +00:00
// change page
await page . locator ( '#to-hello' ) . click ( )
await page . locator ( '#log-foo' ) . click ( )
2023-08-12 07:18:58 +00:00
expect ( pageErrors . at ( - 1 ) ? . toString ( ) || consoleLogs . at ( - 1 ) ! . text ) . toContain ( '.foo is not a function' )
2023-06-10 22:13:33 +00:00
await page . locator ( '#log-hello' ) . click ( )
2023-08-12 07:18:58 +00:00
expect ( consoleLogs . at ( - 1 ) ? . text ) . toBe ( 'world' )
await page . close ( )
2023-06-10 22:13:33 +00:00
} )
2023-03-08 21:13:06 +00:00
it ( 'client-fallback' , async ( ) = > {
const classes = [
'clientfallback-non-stateful-setup' ,
'clientfallback-non-stateful' ,
'clientfallback-stateful-setup' ,
2023-11-03 21:04:26 +00:00
'clientfallback-stateful' ,
'clientfallback-async-setup'
2023-03-08 21:13:06 +00:00
]
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 ( '<span class="break-in-ssr">this failed to render</span>' )
// ensure Fallback slot is being rendered server side
expect ( html ) . toContain ( 'Hello world !' )
// ensure not failed component are correctly rendered
expect ( html ) . not . toContain ( '<p></p>' )
expect ( html ) . toContain ( 'hi' )
2023-11-03 21:04:26 +00:00
// aysnc setup
expect ( html ) . toContain ( 'Work with async setup' )
2023-08-12 07:18:58 +00:00
const { page , pageErrors } = await renderPage ( '/client-fallback' )
2023-03-08 21:13:06 +00:00
// 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' )
2023-05-14 21:22:54 +00:00
// keep-fallback strategy
expect ( await page . locator ( '#keep-fallback' ) . all ( ) ) . toHaveLength ( 1 )
2023-05-13 21:39:50 +00:00
// #20833
expect ( await page . locator ( 'body' ) . innerHTML ( ) ) . not . toContain ( 'Hello world !' )
2023-08-12 07:18:58 +00:00
expect ( pageErrors ) . toEqual ( [ ] )
2023-03-09 13:54:46 +00:00
await page . close ( )
2023-03-08 21:13:06 +00:00
} )
2023-04-20 21:41:20 +00:00
2023-04-27 10:51:33 +00:00
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' )
2023-04-20 21:41:20 +00:00
} )
2022-03-22 18:12:54 +00:00
} )
2022-03-08 18:03:21 +00:00
2023-05-13 21:09:37 +00:00
describe ( 'nuxt composables' , ( ) = > {
it ( 'has useRequestURL()' , async ( ) = > {
const html = await $fetch ( '/url' )
expect ( html ) . toContain ( 'path: /url' )
} )
2023-05-26 19:42:12 +00:00
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' )
2024-01-02 15:37:19 +00:00
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=/, browser-object-default=%7B%22foo%22%3A%22bar%22%7D; 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 . getByRole ( 'button' ) . click ( )
expect ( await extractCookie ( ) ) . toEqual ( { foo : 'baz' } )
2024-01-17 11:53:14 +00:00
await page . getByRole ( 'button' ) . click ( )
expect ( await extractCookie ( ) ) . toEqual ( { foo : 'bar' } )
2024-01-02 15:37:19 +00:00
await page . close ( )
2023-05-26 19:42:12 +00:00
} )
2023-05-13 21:09:37 +00:00
} )
2023-04-07 10:34:35 +00:00
describe ( 'rich payloads' , ( ) = > {
it ( 'correctly serializes and revivifies complex types' , async ( ) = > {
const html = await $fetch ( '/json-payload' )
for ( const test of [
'Date: true' ,
2023-12-26 20:04:14 +00:00
'BigInt: true' ,
'Error: true' ,
2023-04-07 10:34:35 +00:00
'Shallow reactive: true' ,
'Shallow ref: true' ,
2023-05-13 19:49:05 +00:00
'Undefined ref: true' ,
2023-12-26 20:04:14 +00:00
'BigInt ref:' ,
2023-04-07 10:34:35 +00:00
'Reactive: true' ,
'Ref: true' ,
2023-12-26 20:04:14 +00:00
'Recursive objects: true' ,
2023-04-07 10:34:35 +00:00
] ) {
expect ( html ) . toContain ( test )
}
} )
} )
2023-03-07 07:17:42 +00:00
describe ( 'nuxt links' , ( ) = > {
it ( 'handles trailing slashes' , async ( ) = > {
const html = await $fetch ( '/nuxt-link/trailing-slash' )
const data : Record < string , string [ ] > = { }
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 ( ) = > {
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/nuxt-link/trailing-slash' )
2023-03-07 07:17:42 +00:00
for ( const selector of [ 'nuxt-link' , 'router-link' , 'link-with-trailing-slash' , 'link-without-trailing-slash' ] ) {
await page . locator ( ` . ${ selector } [href*=with-state] ` ) . click ( )
2023-08-12 07:18:58 +00:00
await page . getByTestId ( 'window-state' ) . getByText ( 'bar' ) . waitFor ( )
2023-03-07 07:17:42 +00:00
await page . locator ( ` . ${ selector } [href*=without-state] ` ) . click ( )
2023-08-12 07:18:58 +00:00
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath . includes ( 'without-state' ) )
2023-03-07 07:17:42 +00:00
expect ( await page . getByTestId ( 'window-state' ) . innerText ( ) ) . not . toContain ( 'bar' )
}
2023-03-09 13:54:46 +00:00
await page . close ( )
2023-03-07 07:17:42 +00:00
} )
2023-09-06 19:44:59 +00:00
it ( 'expect scroll to top on routes with same component' , async ( ) = > {
// #22402
const page = await createPage ( '/big-page-1' )
await page . setViewportSize ( {
width : 1000 ,
height : 1000
} )
await page . waitForLoadState ( 'networkidle' )
await page . locator ( '#big-page-2' ) . scrollIntoViewIfNeeded ( )
expect ( await page . evaluate ( ( ) = > window . scrollY ) ) . toBeGreaterThan ( 0 )
await page . locator ( '#big-page-2' ) . click ( )
await page . waitForURL ( url = > url . href . includes ( '/big-page-2' ) )
await page . waitForTimeout ( 25 )
expect ( await page . evaluate ( ( ) = > window . scrollY ) ) . toBe ( 0 )
await page . locator ( '#big-page-1' ) . scrollIntoViewIfNeeded ( )
expect ( await page . evaluate ( ( ) = > window . scrollY ) ) . toBeGreaterThan ( 0 )
await page . locator ( '#big-page-1' ) . click ( )
await page . waitForURL ( url = > url . href . includes ( '/big-page-1' ) )
await page . waitForTimeout ( 25 )
expect ( await page . evaluate ( ( ) = > window . scrollY ) ) . toBe ( 0 )
await page . close ( )
} )
it ( 'expect scroll to top on nested pages' , async ( ) = > {
// #20523
const page = await createPage ( '/nested/foo/test' )
await page . setViewportSize ( {
width : 1000 ,
height : 1000
} )
await page . waitForLoadState ( 'networkidle' )
await page . locator ( '#user-test' ) . scrollIntoViewIfNeeded ( )
expect ( await page . evaluate ( ( ) = > window . scrollY ) ) . toBeGreaterThan ( 0 )
await page . locator ( '#user-test' ) . click ( )
await page . waitForURL ( url = > url . href . includes ( '/nested/foo/user-test' ) )
await page . waitForTimeout ( 25 )
expect ( await page . evaluate ( ( ) = > window . scrollY ) ) . toBe ( 0 )
await page . locator ( '#test' ) . scrollIntoViewIfNeeded ( )
expect ( await page . evaluate ( ( ) = > window . scrollY ) ) . toBeGreaterThan ( 0 )
await page . locator ( '#test' ) . click ( )
await page . waitForURL ( url = > url . href . includes ( '/nested/foo/test' ) )
await page . waitForTimeout ( 25 )
expect ( await page . evaluate ( ( ) = > window . scrollY ) ) . toBe ( 0 )
await page . close ( )
} )
2023-03-07 07:17:42 +00:00
} )
2022-04-05 14:02:29 +00:00
describe ( 'head tags' , ( ) = > {
2023-03-08 15:32:24 +00:00
it ( 'SSR should render tags' , async ( ) = > {
2022-08-02 11:20:44 +00:00
const headHtml = await $fetch ( '/head' )
2022-10-12 17:00:17 +00:00
2022-08-02 11:20:44 +00:00
expect ( headHtml ) . toContain ( '<title>Using a dynamic component - Title Template Fn Change</title>' )
expect ( headHtml ) . not . toContain ( '<meta name="description" content="first">' )
expect ( headHtml ) . toContain ( '<meta charset="utf-16">' )
2022-08-07 09:53:53 +00:00
expect ( headHtml . match ( 'meta charset' ) . length ) . toEqual ( 1 )
expect ( headHtml ) . toContain ( '<meta name="viewport" content="width=1024, initial-scale=1">' )
expect ( headHtml . match ( 'meta name="viewport"' ) . length ) . toEqual ( 1 )
2022-08-02 11:20:44 +00:00
expect ( headHtml ) . not . toContain ( '<meta charset="utf-8">' )
expect ( headHtml ) . toContain ( '<meta name="description" content="overriding with an inline useHead call">' )
expect ( headHtml ) . toMatch ( /<html[^>]*class="html-attrs-test"/ )
expect ( headHtml ) . toMatch ( /<body[^>]*class="body-attrs-test"/ )
2022-11-15 16:26:38 +00:00
expect ( headHtml ) . toContain ( '<script src="https://a-body-appended-script.com"></script></body>' )
2022-08-02 11:20:44 +00:00
const indexHtml = await $fetch ( '/' )
2022-04-11 09:03:31 +00:00
// should render charset by default
2022-08-02 11:20:44 +00:00
expect ( indexHtml ) . toContain ( '<meta charset="utf-8">' )
2022-04-11 09:03:31 +00:00
// should render <Head> components
2022-08-02 11:20:44 +00:00
expect ( indexHtml ) . toContain ( '<title>Basic fixture</title>' )
2022-04-05 14:02:29 +00:00
} )
2022-07-25 12:05:58 +00:00
2023-03-10 08:01:21 +00:00
it ( 'SSR script setup should render tags' , async ( ) = > {
const headHtml = await $fetch ( '/head-script-setup' )
// useHead - title & titleTemplate are working
expect ( headHtml ) . toContain ( '<title>head script setup - Nuxt Playground</title>' )
// useSeoMeta - template params
expect ( headHtml ) . toContain ( '<meta property="og:title" content="head script setup - Nuxt Playground">' )
// useSeoMeta - refs
expect ( headHtml ) . toContain ( '<meta name="description" content="head script setup description for Nuxt Playground">' )
// useServerHead - shorthands
expect ( headHtml ) . toContain ( '>/* Custom styles */</style>' )
// useHeadSafe - removes dangerous content
expect ( headHtml ) . toContain ( '<script id="xss-script"></script>' )
expect ( headHtml ) . toContain ( '<meta content="0;javascript:alert(1)">' )
} )
2023-03-08 15:32:24 +00:00
it ( 'SPA should render appHead tags' , async ( ) = > {
const headHtml = await $fetch ( '/head' , { headers : { 'x-nuxt-no-ssr' : '1' } } )
expect ( headHtml ) . toContain ( '<meta name="description" content="Nuxt Fixture">' )
expect ( headHtml ) . toContain ( '<meta charset="utf-8">' )
expect ( headHtml ) . toContain ( '<meta name="viewport" content="width=1024, initial-scale=1">' )
} )
it ( 'legacy vueuse/head works' , async ( ) = > {
const headHtml = await $fetch ( '/vueuse-head' )
expect ( headHtml ) . toContain ( '<title>using provides usehead and updateDOM - VueUse head polyfill test</title>' )
} )
2022-09-03 12:31:09 +00:00
it ( 'should render http-equiv correctly' , async ( ) = > {
const html = await $fetch ( '/head' )
// http-equiv should be rendered kebab case
expect ( html ) . toContain ( '<meta content="default-src https" http-equiv="content-security-policy">' )
} )
2022-07-25 12:05:58 +00:00
// 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(/<link rel="stylesheet" href="\/_nuxt\/[^>]*.css"/)
// })
2022-04-05 14:02:29 +00:00
} )
2022-08-29 10:02:24 +00:00
describe ( 'legacy async data' , ( ) = > {
it ( 'should work with defineNuxtComponent' , async ( ) = > {
const html = await $fetch ( '/legacy/async-data' )
expect ( html ) . toContain ( '<div>Hello API</div>' )
2023-06-05 18:36:26 +00:00
expect ( html ) . toContain ( '<div>fooChild</div>' )
expect ( html ) . toContain ( '<div>fooParent</div>' )
2023-04-07 10:34:35 +00:00
const { script } = parseData ( html )
2023-06-05 18:36:26 +00:00
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" ,
} ,
]
` )
2022-08-29 10:02:24 +00:00
} )
} )
2022-03-22 18:12:54 +00:00
describe ( 'navigate' , ( ) = > {
it ( 'should redirect to index with navigateTo' , async ( ) = > {
2023-02-16 17:26:15 +00:00
const { headers , status } = await fetch ( '/navigate-to/' , { redirect : 'manual' } )
2022-03-22 18:12:54 +00:00
2022-05-11 17:33:29 +00:00
expect ( headers . get ( 'location' ) ) . toEqual ( '/' )
2023-02-16 17:26:15 +00:00
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 )
2023-12-11 18:20:11 +00:00
expect ( await res . text ( ) ) . toMatchInlineSnapshot ( ` "<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=/navigate-some-path"></head></html>" ` )
2022-03-08 18:03:21 +00:00
} )
2023-04-13 09:58:25 +00:00
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 )
} )
2023-06-07 12:18:50 +00:00
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 )
} )
2023-04-13 09:58:25 +00:00
it ( 'supports directly aborting navigation on SSR' , async ( ) = > {
const { status } = await fetch ( '/navigate-to-false' , { redirect : 'manual' } )
expect ( status ) . toEqual ( 404 )
} )
2022-03-22 18:12:54 +00:00
} )
2022-03-08 18:03:21 +00:00
2023-05-17 12:26:16 +00:00
describe ( 'preserves current instance' , ( ) = > {
2023-06-23 10:02:01 +00:00
// 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 ( ) = > {
2023-05-17 12:26:16 +00:00
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' )
}
} )
} )
2022-09-08 08:45:39 +00:00
describe ( 'errors' , ( ) = > {
it ( 'should render a JSON error page' , async ( ) = > {
const res = await fetch ( '/error' , {
headers : {
accept : 'application/json'
}
} )
expect ( res . status ) . toBe ( 422 )
2023-03-19 23:20:04 +00:00
expect ( res . statusText ) . toBe ( 'This is a custom error' )
2022-09-08 08:45:39 +00:00
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' )
2023-09-11 13:05:14 +00:00
expect ( res . headers . get ( 'Set-Cookie' ) ) . toBe ( 'set-in-plugin=true; Path=/, some-error=was%20set; Path=/' )
2022-09-08 08:45:39 +00:00
expect ( await res . text ( ) ) . toContain ( 'This is a custom error' )
} )
2023-02-16 12:43:58 +00:00
2023-04-25 14:47:02 +00:00
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" ,
}
` )
2023-12-11 18:20:11 +00:00
} )
2023-11-22 09:58:29 +00:00
2023-12-11 18:20:11 +00:00
it ( 'should not recursively throw an error when there is an error rendering the error page' , async ( ) = > {
const res = await $fetch ( '/' , {
headers : {
'x-test-recurse-error' : 'true' ,
accept : 'text/html'
}
2023-11-22 09:58:29 +00:00
} )
2023-12-11 18:20:11 +00:00
expect ( typeof res ) . toBe ( 'string' )
expect ( res ) . toContain ( 'Hello Nuxt 3!' )
2023-04-25 14:47:02 +00:00
} )
2023-02-16 12:43:58 +00:00
// TODO: need to create test for webpack
it . runIf ( ! isDev ( ) && ! isWebpack ) ( 'should handle chunk loading errors' , async ( ) = > {
const { page , consoleLogs } = await renderPage ( '/' )
2023-03-08 12:17:22 +00:00
await page . getByText ( 'Increment state' ) . click ( )
await page . getByText ( 'Increment state' ) . click ( )
2023-04-12 08:42:45 +00:00
expect ( await page . innerText ( 'div' ) ) . toContain ( 'Some value: 3' )
2023-02-16 12:43:58 +00:00
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' )
2023-08-12 07:18:58 +00:00
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/chunk-error' )
await page . locator ( 'div' ) . getByText ( 'State: 3' ) . waitFor ( )
2023-03-09 13:54:46 +00:00
await page . close ( )
2023-02-16 12:43:58 +00:00
} )
2022-09-08 08:45:39 +00:00
} )
2022-08-24 16:04:56 +00:00
describe ( 'navigate external' , ( ) = > {
it ( 'should redirect to example.com' , async ( ) = > {
const { headers } = await fetch ( '/navigate-to-external/' , { redirect : 'manual' } )
2023-06-11 21:27:02 +00:00
expect ( headers . get ( 'location' ) ) . toEqual ( 'https://example.com/?redirect=false#test' )
2022-08-24 16:04:56 +00:00
} )
2023-03-20 17:15:01 +00:00
it ( 'should redirect to api endpoint' , async ( ) = > {
const { headers } = await fetch ( '/navigate-to-api' , { redirect : 'manual' } )
expect ( headers . get ( 'location' ) ) . toEqual ( '/api/test' )
} )
2022-08-24 16:04:56 +00:00
} )
2023-12-19 11:00:11 +00:00
describe ( 'composables' , ( ) = > {
it ( 'should run code once' , async ( ) = > {
const html = await $fetch ( '/once' )
expect ( html ) . toContain ( 'once.vue' )
expect ( html ) . toContain ( 'once: 2' )
const { page } = await renderPage ( '/once' )
expect ( await page . getByText ( 'once:' ) . textContent ( ) ) . toContain ( 'once: 2' )
} )
} )
2022-03-22 18:12:54 +00:00
describe ( 'middlewares' , ( ) = > {
it ( 'should redirect to index with global middleware' , async ( ) = > {
const html = await $fetch ( '/redirect/' )
2022-03-16 21:39:47 +00:00
2022-03-22 18:12:54 +00:00
// Snapshot
// expect(html).toMatchInlineSnapshot()
2022-03-16 21:39:47 +00:00
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( 'Hello Nuxt 3!' )
2022-03-16 21:39:47 +00:00
} )
2022-09-08 08:52:00 +00:00
it ( 'should allow aborting navigation on server-side' , async ( ) = > {
const res = await fetch ( '/?abort' , {
headers : {
accept : 'application/json'
}
} )
expect ( res . status ) . toEqual ( 401 )
} )
2023-05-25 18:29:22 +00:00
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' )
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/middleware-abort' )
2023-05-25 18:29:22 +00:00
expect ( await page . innerHTML ( 'body' ) ) . toContain ( 'This is the error page' )
2023-05-28 15:38:36 +00:00
await page . close ( )
2023-05-25 18:29:22 +00:00
} )
2022-03-22 18:12:54 +00:00
it ( 'should inject auth' , async ( ) = > {
const html = await $fetch ( '/auth' )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// Snapshot
// expect(html).toMatchInlineSnapshot()
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( 'auth.vue' )
expect ( html ) . toContain ( 'auth: Injected by injectAuth middleware' )
} )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
it ( 'should not inject auth' , async ( ) = > {
const html = await $fetch ( '/no-auth' )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// Snapshot
// expect(html).toMatchInlineSnapshot()
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( 'no-auth.vue' )
expect ( html ) . toContain ( 'auth: ' )
expect ( html ) . not . toContain ( 'Injected by injectAuth middleware' )
} )
2022-09-19 08:54:35 +00:00
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 )
} )
2022-03-22 18:12:54 +00:00
} )
2022-03-08 18:03:21 +00:00
2022-04-01 09:55:23 +00:00
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' )
2022-12-10 22:44:29 +00:00
expect ( html ) . toContain ( 'useFetch works!' )
2022-04-01 09:55:23 +00:00
} )
} )
2022-03-22 18:12:54 +00:00
describe ( 'layouts' , ( ) = > {
it ( 'should apply custom layout' , async ( ) = > {
const html = await $fetch ( '/with-layout' )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// Snapshot
// expect(html).toMatchInlineSnapshot()
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( 'with-layout.vue' )
expect ( html ) . toContain ( 'Custom Layout:' )
2022-02-25 20:14:53 +00:00
} )
2022-08-31 08:02:48 +00:00
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' )
} )
2023-01-14 00:58:54 +00:00
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' )
} )
2022-11-29 12:16:41 +00:00
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' )
} )
2022-03-22 18:12:54 +00:00
} )
2022-02-25 20:14:53 +00:00
2023-03-07 09:30:05 +00:00
describe ( 'composable tree shaking' , ( ) = > {
it ( 'should work' , async ( ) = > {
const html = await $fetch ( '/tree-shake' )
expect ( html ) . toContain ( 'Tree Shake Example' )
2023-08-12 07:18:58 +00:00
const { page , pageErrors } = await renderPage ( '/tree-shake' )
2023-03-07 09:30:05 +00:00
// ensure scoped classes are correctly assigned between client and server
expect ( await page . $eval ( 'h1' , e = > getComputedStyle ( e ) . color ) ) . toBe ( 'rgb(255, 192, 203)' )
2023-08-12 07:18:58 +00:00
expect ( pageErrors ) . toEqual ( [ ] )
2023-03-09 13:54:46 +00:00
await page . close ( )
2023-03-07 09:30:05 +00:00
} )
} )
2023-08-24 12:42:15 +00:00
describe ( 'ignore list' , ( ) = > {
it ( 'should ignore composable files in .nuxtignore' , async ( ) = > {
const html = await $fetch ( '/ignore/composables' )
expect ( html ) . toContain ( 'was import ignored: true' )
} )
2023-08-25 12:08:38 +00:00
it ( 'should ignore scanned nitro handlers in .nuxtignore' , async ( ) = > {
const html = await $fetch ( '/ignore/scanned' )
expect ( html ) . not . toContain ( 'this should be ignored' )
} )
it . skipIf ( isDev ( ) ) ( 'should ignore public assets in .nuxtignore' , async ( ) = > {
const html = await $fetch ( '/ignore/public-asset' )
expect ( html ) . not . toContain ( 'this should be ignored' )
} )
2023-08-24 12:42:15 +00:00
} )
2022-07-07 16:04:38 +00:00
describe ( 'server tree shaking' , ( ) = > {
it ( 'should work' , async ( ) = > {
const html = await $fetch ( '/client' )
expect ( html ) . toContain ( 'This page should not crash when rendered' )
2023-02-08 08:59:57 +00:00
expect ( html ) . toContain ( 'fallback for ClientOnly' )
expect ( html ) . not . toContain ( 'rendered client-side' )
expect ( html ) . not . toContain ( 'id="client-side"' )
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/client' )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) )
2023-02-08 08:59:57 +00:00
// 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' )
2023-03-09 13:54:46 +00:00
await page . close ( )
2022-07-07 16:04:38 +00:00
} )
} )
2022-03-22 18:12:54 +00:00
describe ( 'extends support' , ( ) = > {
2022-03-24 12:33:42 +00:00
describe ( 'layouts & pages' , ( ) = > {
it ( 'extends foo/layouts/default & foo/pages/index' , async ( ) = > {
2022-03-22 18:12:54 +00:00
const html = await $fetch ( '/foo' )
2022-03-24 12:33:42 +00:00
expect ( html ) . toContain ( 'Extended layout from foo' )
expect ( html ) . toContain ( 'Extended page from foo' )
2022-03-22 18:12:54 +00:00
} )
2022-03-24 12:33:42 +00:00
it ( 'extends [bar/layouts/override & bar/pages/override] over [foo/layouts/override & foo/pages/override]' , async ( ) = > {
2022-03-22 18:12:54 +00:00
const html = await $fetch ( '/override' )
2022-03-24 12:33:42 +00:00
expect ( html ) . toContain ( 'Extended layout from bar' )
2022-03-22 18:12:54 +00:00
expect ( html ) . toContain ( 'Extended page from bar' )
2023-09-12 09:46:35 +00:00
expect ( html ) . toContain ( 'This child page should not be overridden by bar' )
2022-03-08 18:03:21 +00:00
} )
2022-02-18 18:14:57 +00:00
} )
2022-03-17 22:17:59 +00:00
2022-03-24 12:33:42 +00:00
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' )
} )
} )
2022-03-22 18:12:54 +00:00
describe ( 'middlewares' , ( ) = > {
2023-04-03 13:18:24 +00:00
it ( 'works with layer aliases' , async ( ) = > {
const html = await $fetch ( '/foo' )
expect ( html ) . toContain ( 'from layer alias' )
} )
2022-03-22 18:12:54 +00:00
it ( 'extends foo/middleware/foo' , async ( ) = > {
2022-03-24 12:33:42 +00:00
const html = await $fetch ( '/foo' )
expect ( html ) . toContain ( 'Middleware | foo: Injected by extended middleware from foo' )
} )
2023-10-12 22:21:02 +00:00
it ( 'extends bar/middleware/override over foo/middleware/override' , async ( ) = > {
2022-03-24 12:33:42 +00:00
const html = await $fetch ( '/override' )
expect ( html ) . toContain ( 'Middleware | override: Injected by extended middleware from bar' )
} )
2023-09-16 08:39:51 +00:00
it ( 'global middlewares sorting' , async ( ) = > {
const html = await $fetch ( '/middleware/ordering' )
expect ( html ) . toContain ( 'catchall at middleware' )
} )
2022-03-24 12:33:42 +00:00
} )
describe ( 'composables' , ( ) = > {
it ( 'extends foo/composables/foo' , async ( ) = > {
const html = await $fetch ( '/foo' )
expect ( html ) . toContain ( 'Composable | useExtendsFoo: foo' )
} )
2023-01-14 01:14:24 +00:00
it ( 'allows overriding composables' , async ( ) = > {
const html = await $fetch ( '/extends' )
expect ( html ) . toContain ( 'test from project' )
} )
2022-03-24 12:33:42 +00:00
} )
describe ( 'plugins' , ( ) = > {
it ( 'extends foo/plugins/foo' , async ( ) = > {
const html = await $fetch ( '/foo' )
expect ( html ) . toContain ( 'Plugin | foo: String generated from foo plugin!' )
} )
2023-09-04 22:41:51 +00:00
it ( 'respects plugin ordering within layers' , async ( ) = > {
const html = await $fetch ( '/plugins/ordering' )
expect ( html ) . toContain ( 'catchall at plugins' )
} )
2022-03-24 12:33:42 +00:00
} )
describe ( 'server' , ( ) = > {
it ( 'extends foo/server/api/foo' , async ( ) = > {
expect ( await $fetch ( '/api/foo' ) ) . toBe ( 'foo' )
2022-03-22 18:12:54 +00:00
} )
2022-03-17 22:17:59 +00:00
2022-03-24 12:33:42 +00:00
it ( 'extends foo/server/middleware/foo' , async ( ) = > {
const { headers } = await fetch ( '/' )
expect ( headers . get ( 'injected-header' ) ) . toEqual ( 'foo' )
2022-03-17 22:17:59 +00:00
} )
} )
2022-04-04 08:23:11 +00:00
describe ( 'app' , ( ) = > {
it ( 'extends foo/app/router.options & bar/app/router.options' , async ( ) = > {
const html : string = await $fetch ( '/' )
2022-08-26 15:47:29 +00:00
const routerLinkClasses = html . match ( /href="\/" class="([^"]*)"/ ) ? . [ 1 ] . split ( ' ' )
2022-04-04 08:23:11 +00:00
expect ( routerLinkClasses ) . toContain ( 'foo-active-class' )
expect ( routerLinkClasses ) . toContain ( 'bar-exact-active-class' )
} )
} )
2022-03-23 14:57:35 +00:00
} )
2022-03-22 15:51:26 +00:00
2022-10-08 14:18:57 +00:00
// Bug #7337
describe ( 'deferred app suspense resolve' , ( ) = > {
2023-08-12 07:18:58 +00:00
it . each ( [ '/async-parent/child' , '/internal-layout/async-parent/child' ] ) ( 'should wait for all suspense instance on initial hydration' , async ( path ) = > {
const { page , consoleLogs } = await renderPage ( path )
2023-07-19 06:55:53 +00:00
// 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 ) ) )
2023-08-12 07:18:58 +00:00
const hydrationLogs = consoleLogs . filter ( log = > log . text . includes ( 'isHydrating' ) )
expect ( hydrationLogs . length ) . toBe ( 3 )
expect ( hydrationLogs . every ( log = > log . text === 'isHydrating: true' ) )
await page . close ( )
2023-07-19 06:55:53 +00:00
} )
2023-07-18 15:21:53 +00:00
2023-08-12 07:18:58 +00:00
it ( 'should wait for suspense in parent layout' , async ( ) = > {
const { page } = await renderPage ( '/hydration/layout' )
await page . getByText ( 'Tests whether hydration is properly resolved within an async layout' ) . waitFor ( )
await page . close ( )
} )
2023-07-18 15:21:53 +00:00
2023-08-12 07:18:58 +00:00
it ( 'should fully hydrate even if there is a redirection on a page with `ssr: false`' , async ( ) = > {
const { page } = await renderPage ( '/hydration/spa-redirection/start' )
await page . getByText ( 'fully hydrated and ready to go' ) . waitFor ( )
await page . close ( )
2023-07-18 15:21:53 +00:00
} )
2022-10-08 14:18:57 +00:00
} )
2023-05-11 17:57:18 +00:00
describe ( 'nested suspense' , ( ) = > {
2023-07-05 10:39:39 +00:00
const navigations = ( [
2023-05-11 17:57:18 +00:00
[ '/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/' ]
2023-07-05 10:39:39 +00:00
] ) . flatMap ( ( [ start , end ] ) = > [
[ start , end ] ,
[ start , end + '?layout=custom' ] ,
[ start + '?layout=custom' , end ]
] )
2023-05-11 17:57:18 +00:00
it . each ( navigations ) ( 'should navigate from %s to %s with no white flash' , async ( start , nav ) = > {
2023-08-12 07:18:58 +00:00
const { page , consoleLogs } = await renderPage ( start )
2023-05-11 17:57:18 +00:00
2023-07-05 10:39:39 +00:00
const slug = nav . replace ( /\?.*$/ , '' ) . replace ( /[/-]+/g , '-' )
2023-05-11 17:57:18 +00:00
await page . click ( ` [href^=" ${ nav } "] ` )
2023-07-05 10:39:39 +00:00
const text = await page . waitForFunction ( slug = > document . querySelector ( ` main:has(#child ${ slug } ) ` ) ? . innerHTML , slug )
2023-05-11 17:57:18 +00:00
// @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' )
2023-07-05 10:39:39 +00:00
expect ( text ) . toContain ( 'parent: 2' )
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 !
2023-08-12 07:18:58 +00:00
expect ( consoleLogs . map ( l = > l . text ) . filter ( i = > ! i . includes ( '[vite]' ) && ! i . includes ( '<Suspense> is an experimental feature' ) ) . sort ( ) ) . toEqual ( [
2023-07-05 10:39:39 +00:00
// [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 ) = > {
2023-08-12 07:18:58 +00:00
const { page , consoleLogs } = await renderPage ( start )
2023-07-05 10:39:39 +00:00
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\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\// ) ! . groups !
const last = nav . match ( /\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\// ) ! . groups !
2023-10-31 12:56:28 +00:00
await new Promise < void > ( resolve = > setTimeout ( resolve , 50 ) )
2023-08-12 07:18:58 +00:00
expect ( consoleLogs . map ( l = > l . text ) . filter ( i = > ! i . includes ( '[vite]' ) && ! i . includes ( '<Suspense> is an experimental feature' ) ) . sort ( ) ) . toEqual ( [
2023-07-05 10:39:39 +00:00
// [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 ) = > {
2023-08-12 07:18:58 +00:00
const { page , consoleLogs } = await renderPage ( start )
2023-07-05 10:39:39 +00:00
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\/(?<parentType>a?sync)-(?<parentNum>\d)\// ) ! . groups !
const last = nav . match ( /\/suspense\/(?<parentType>a?sync)-(?<parentNum>\d)\/(?<childType>a?sync)-(?<childNum>\d)\// ) ! . groups !
2023-08-12 07:18:58 +00:00
expect ( consoleLogs . map ( l = > l . text ) . filter ( i = > ! i . includes ( '[vite]' ) && ! i . includes ( '<Suspense> is an experimental feature' ) ) . sort ( ) ) . toEqual ( [
2023-07-05 10:39:39 +00:00
// [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 ( ) )
2023-05-11 17:57:18 +00:00
await page . close ( )
} )
} )
2022-10-08 14:18:57 +00:00
// Bug #6592
describe ( 'page key' , ( ) = > {
2023-08-12 07:18:58 +00:00
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 ) = > {
const { page , consoleLogs } = await renderPage ( ` ${ path } /0 ` )
2022-10-08 14:18:57 +00:00
2023-08-12 07:18:58 +00:00
await page . click ( ` [href=" ${ path } /1"] ` )
await page . waitForSelector ( '#page-1' )
2022-10-08 14:18:57 +00:00
2023-08-12 07:18:58 +00:00
// 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 ) ) )
2022-10-08 14:18:57 +00:00
2023-08-12 07:18:58 +00:00
expect ( consoleLogs . filter ( l = > l . text . includes ( 'Child Setup' ) ) . length ) . toBe ( 1 )
await page . close ( )
2022-10-08 14:18:57 +00:00
} )
2023-08-12 07:18:58 +00:00
it . each ( [ '/keyed-child-parent' , '/internal-layout/keyed-child-parent' ] ) ( 'will cause run of setup if navigation changed page key' , async ( path ) = > {
const { page , consoleLogs } = await renderPage ( ` ${ path } /0 ` )
2022-10-08 14:18:57 +00:00
2023-08-12 07:18:58 +00:00
await page . click ( ` [href=" ${ path } /1"] ` )
await page . waitForSelector ( '#page-1' )
2022-10-08 14:18:57 +00:00
2023-08-12 07:18:58 +00:00
// 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 ( 'Child Setup' ) ) . length ) . toBe ( 2 )
await page . close ( )
2022-10-08 14:18:57 +00:00
} )
} )
2023-11-09 03:21:19 +00:00
describe ( 'route provider' , ( ) = > {
it ( 'should preserve current route when navigation is suspended' , async ( ) = > {
const { page } = await renderPage ( '/route-provider/foo' )
await page . click ( '[href="/route-provider/bar"]' )
expect ( await page . getByTestId ( 'foo' ) . innerText ( ) ) . toMatchInlineSnapshot ( '"foo: /route-provider/foo - /route-provider/foo"' )
expect ( await page . getByTestId ( 'bar' ) . innerText ( ) ) . toMatchInlineSnapshot ( '"bar: /route-provider/bar - /route-provider/bar"' )
await page . close ( )
} )
} )
2022-10-08 14:18:57 +00:00
// Bug #6592
describe ( 'layout change not load page twice' , ( ) = > {
2023-08-12 07:18:58 +00:00
const cases = {
'/with-layout' : '/with-layout2' ,
'/internal-layout/with-layout' : '/internal-layout/with-layout2'
2022-10-08 14:18:57 +00:00
}
2023-08-12 07:18:58 +00:00
it . each ( Object . entries ( cases ) ) ( 'should not cause run of page setup to repeat if layout changed' , async ( path1 , path2 ) = > {
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 )
2022-10-08 14:18:57 +00:00
} )
} )
2023-06-23 10:02:01 +00:00
describe ( 'layout switching' , ( ) = > {
// #13309
it ( 'does not cause TypeError: Cannot read properties of null' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page , consoleLogs , pageErrors } = await renderPage ( '/layout-switch/start' )
await page . click ( '[href="/layout-switch/end"]' )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/layout-switch/end' )
expect ( consoleLogs . map ( i = > i . text ) . filter ( l = > l . match ( /error/i ) ) ) . toMatchInlineSnapshot ( '[]' )
expect ( pageErrors ) . toMatchInlineSnapshot ( '[]' )
await page . close ( )
2023-06-23 10:02:01 +00:00
} )
} )
2022-07-07 16:26:04 +00:00
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' )
} )
2022-08-05 11:02:20 +00:00
it ( 'should match server-generated keys' , async ( ) = > {
await expectNoClientErrors ( '/keyed-composables' )
} )
2023-06-05 19:15:12 +00:00
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' )
} )
2022-07-07 16:26:04 +00:00
} )
2023-02-13 22:09:32 +00:00
describe . skipIf ( isDev ( ) || isWebpack ) ( 'inlining component styles' , ( ) = > {
2023-06-20 18:28:44 +00:00
const inlinedCSS = [
'{--plugin:"plugin"}' , // CSS imported ambiently in JS/TS
'{--global:"global";' , // global css from nuxt.config
'{--assets:"assets"}' , // <script>
'{--postcss:"postcss"}' , // <style lang=postcss>
2023-06-25 21:51:31 +00:00
'{--scoped:"scoped"}' , // <style lang=css>
'{--server-only:"server-only"}' // server-only component not in client build
2023-06-20 18:28:44 +00:00
// TODO: ideally both client/server components would have inlined css when used
// '{--client-only:"client-only"}', // client-only component not in server build
// TODO: currently functional component not associated with ssrContext (upstream bug or perf optimization?)
// '{--functional:"functional"}', // CSS imported ambiently in a functional component
]
2022-09-07 09:55:03 +00:00
it ( 'should inline styles' , async ( ) = > {
const html = await $fetch ( '/styles' )
2023-06-20 18:28:44 +00:00
for ( const style of inlinedCSS ) {
2022-09-07 09:55:03 +00:00
expect ( html ) . toContain ( style )
}
} )
2022-09-07 08:41:08 +00:00
2023-06-25 21:50:42 +00:00
it ( 'should inline global css when accessing a page with `ssr: false` override via route rules' , async ( ) = > {
const globalCSS = [
'{--plugin:"plugin"}' , // CSS imported ambiently in JS/TS
'{--global:"global";' // global css from nuxt.config
]
const html = await $fetch ( '/route-rules/spa' )
for ( const style of globalCSS ) {
expect ( html ) . toContain ( style )
}
} )
2023-06-26 12:11:12 +00:00
it ( 'should emit assets referenced in inlined CSS' , async ( ) = > {
// @ts-expect-error ssssh! untyped secret property
const publicDir = useTestContext ( ) . nuxt . _nitro . options . output . publicDir
const files = await readdir ( join ( publicDir , '_nuxt' ) ) . catch ( ( ) = > [ ] )
expect ( files . map ( m = > m . replace ( /\.\w+(\.\w+)$/ , '$1' ) ) ) . toContain ( 'css-only-asset.svg' )
} )
2023-06-20 18:28:44 +00:00
it ( 'should not include inlined CSS in generated CSS file' , async ( ) = > {
const html : string = await $fetch ( '/styles' )
const cssFiles = new Set ( [ . . . html . matchAll ( /<link [^>]*href="([^"]*\.css)">/g ) ] . map ( m = > m [ 1 ] ) )
let css = ''
for ( const file of cssFiles || [ ] ) {
css += await $fetch ( file )
}
// should not include inlined CSS in generated CSS files
for ( const style of inlinedCSS ) {
// TODO: remove 'ambient global' CSS from generated CSS file
if ( style === '{--plugin:"plugin"}' ) { continue }
expect . soft ( css ) . not . toContain ( style )
}
// should include unloadable CSS in generated CSS file
expect . soft ( css ) . toContain ( '--virtual:red' )
expect . soft ( css ) . toContain ( '--functional:"functional"' )
expect . soft ( css ) . toContain ( '--client-only:"client-only"' )
} )
2022-11-03 19:17:43 +00:00
it ( 'does not load stylesheet for page styles' , async ( ) = > {
2022-09-07 09:55:03 +00:00
const html : string = await $fetch ( '/styles' )
2022-11-02 10:28:41 +00:00
expect ( html . match ( /<link [^>]*href="[^"]*\.css">/g ) ? . filter ( m = > m . includes ( 'entry' ) ) ? . map ( m = > m . replace ( /\.[^.]*\.css/ , '.css' ) ) ) . toMatchInlineSnapshot ( `
[
2023-12-11 18:20:11 +00:00
"<link rel=" stylesheet " href=" / _nuxt / entry . css ">" ,
2022-11-02 10:28:41 +00:00
]
` )
2022-09-07 09:55:03 +00:00
} )
2022-09-07 08:41:08 +00:00
2022-10-18 16:13:50 +00:00
it ( 'still downloads client-only styles' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/styles' )
2022-09-07 09:55:03 +00:00
expect ( await page . $eval ( '.client-only-css' , e = > getComputedStyle ( e ) . color ) ) . toBe ( 'rgb(50, 50, 50)' )
2023-03-09 13:54:46 +00:00
await page . close ( )
2022-09-07 09:55:03 +00:00
} )
2022-09-07 08:41:08 +00:00
2022-09-07 09:55:03 +00:00
it . todo ( 'renders client-only styles only' , async ( ) = > {
const html = await $fetch ( '/styles' )
expect ( html ) . toContain ( '{--client-only:"client-only"}' )
2022-09-03 13:03:30 +00:00
} )
2022-09-07 09:55:03 +00:00
} )
2022-09-03 13:03:30 +00:00
2023-06-25 16:38:15 +00:00
describe ( 'server components/islands' , ( ) = > {
it ( '/islands' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/islands' )
2024-01-01 12:33:11 +00:00
const islandRequest = page . waitForResponse ( response = > response . url ( ) . includes ( '/__nuxt_island/' ) && response . status ( ) === 200 )
2023-06-25 16:38:15 +00:00
await page . locator ( '#increase-pure-component' ) . click ( )
2024-01-01 12:33:11 +00:00
await islandRequest
2024-01-02 15:37:19 +00:00
2023-08-12 07:18:58 +00:00
await page . locator ( '#slot-in-server' ) . getByText ( 'Slot with in .server component' ) . waitFor ( )
await page . locator ( '#test-slot' ) . getByText ( 'Slot with name test' ) . waitFor ( )
2023-06-25 16:38:15 +00:00
// test fallback slot with v-for
expect ( await page . locator ( '.fallback-slot-content' ) . all ( ) ) . toHaveLength ( 2 )
// test islands update
expect ( await page . locator ( '.box' ) . innerHTML ( ) ) . toContain ( '"number": 101,' )
2024-01-01 12:33:11 +00:00
const requests = [
2023-06-25 16:38:15 +00:00
page . waitForResponse ( response = > response . url ( ) . includes ( '/__nuxt_island/LongAsyncComponent' ) && response . status ( ) === 200 ) ,
page . waitForResponse ( response = > response . url ( ) . includes ( '/__nuxt_island/AsyncServerComponent' ) && response . status ( ) === 200 )
2024-01-01 12:33:11 +00:00
]
await page . locator ( '#update-server-components' ) . click ( )
await Promise . all ( requests )
2023-08-12 07:18:58 +00:00
await page . locator ( '#async-server-component-count' ) . getByText ( '1' ) . waitFor ( )
await page . locator ( '#long-async-component-count' ) . getByText ( '1' ) . waitFor ( )
2023-06-25 16:38:15 +00:00
// test islands slots interactivity
await page . locator ( '#first-sugar-counter button' ) . click ( )
expect ( await page . locator ( '#first-sugar-counter' ) . innerHTML ( ) ) . toContain ( 'Sugar Counter 13' )
// test islands mounted client side with slot
await page . locator ( '#show-island' ) . click ( )
expect ( await page . locator ( '#island-mounted-client-side' ) . innerHTML ( ) ) . toContain ( 'Interactive testing slot post SSR' )
2023-12-19 12:21:29 +00:00
if ( ! isWebpack ) {
// test client component interactivity
expect ( await page . locator ( '.interactive-component-wrapper' ) . innerHTML ( ) ) . toContain ( 'Sugar Counter 12' )
await page . locator ( '.interactive-component-wrapper button' ) . click ( )
expect ( await page . locator ( '.interactive-component-wrapper' ) . innerHTML ( ) ) . toContain ( 'Sugar Counter 13' )
}
2023-06-25 16:38:15 +00:00
await page . close ( )
} )
2023-07-31 08:51:09 +00:00
it ( 'lazy server components' , async ( ) = > {
2023-12-19 12:21:29 +00:00
const logs : ConsoleMessage [ ] = [ ]
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/server-components/lazy/start' )
2023-12-19 12:21:29 +00:00
page . on ( 'console' , ( msg ) = > { if ( msg . type ( ) === 'error' ) { logs . push ( msg ) } } )
2023-07-31 08:51:09 +00:00
await page . waitForLoadState ( 'networkidle' )
await page . getByText ( 'Go to page with lazy server component' ) . click ( )
const text = await page . innerText ( 'pre' )
2023-12-19 12:21:29 +00:00
expect ( text ) . toMatchInlineSnapshot ( '" End page <pre></pre><section id="fallback"> Loading server component </section><section id="no-fallback"><div></div></section><div></div>"' )
2023-07-31 08:51:09 +00:00
expect ( text ) . not . toContain ( 'async component that was very long' )
expect ( text ) . toContain ( 'Loading server component' )
// Wait for all pending micro ticks to be cleared
// await page.waitForLoadState('networkidle')
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
await page . waitForFunction ( ( ) = > ( document . querySelector ( '#no-fallback' ) as HTMLElement ) ? . innerText ? . includes ( 'async component' ) )
await page . waitForFunction ( ( ) = > ( document . querySelector ( '#fallback' ) as HTMLElement ) ? . innerText ? . includes ( 'async component' ) )
2023-12-19 12:21:29 +00:00
// test navigating back and forth for lazy <ServerWithClient> component (should not trigger any issue)
await page . goBack ( { waitUntil : 'networkidle' } )
await page . getByText ( 'Go to page with lazy server component' ) . click ( )
await page . waitForLoadState ( 'networkidle' )
expect ( logs ) . toHaveLength ( 0 )
await page . close ( )
} )
it ( 'should not preload ComponentWithRef' , async ( ) = > {
// should not add <ComponentWithRef> to the modulepreload list since it is used only server side
const { page } = await renderPage ( '/islands' )
const links = await page . locator ( 'link' ) . all ( )
for ( const link of links ) {
if ( await link . getAttribute ( 'rel' ) === 'modulepreload' ) {
expect ( await link . getAttribute ( 'href' ) ) . not . toContain ( 'ComponentWithRef' )
}
}
2023-07-31 08:51:09 +00:00
await page . close ( )
} )
it ( 'non-lazy server components' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/server-components/lazy/start' )
2023-07-31 08:51:09 +00:00
await page . waitForLoadState ( 'networkidle' )
await page . getByText ( 'Go to page without lazy server component' ) . click ( )
2024-01-16 16:33:45 +00:00
const text = ( await page . innerText ( 'pre' ) ) . replaceAll ( / data-island-uid="([^"]*)"/g , '' ) . replace ( /data-island-component="([^"]*)"/g , ( _ , content ) = > ` data-island-component=" ${ content . split ( '-' ) [ 0 ] } " ` )
2023-12-19 12:21:29 +00:00
if ( isWebpack ) {
2024-01-16 13:22:50 +00:00
expect ( text ) . toMatchInlineSnapshot ( '" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"' )
2023-12-19 12:21:29 +00:00
} else {
2024-01-16 16:33:45 +00:00
expect ( text ) . toMatchInlineSnapshot ( ` " End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>" ` )
2023-12-19 12:21:29 +00:00
}
2023-07-31 08:51:09 +00:00
expect ( text ) . toContain ( 'async component that was very long' )
// Wait for all pending micro ticks to be cleared
// await page.waitForLoadState('networkidle')
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
await page . waitForFunction ( ( ) = > ( document . querySelector ( '#no-fallback' ) as HTMLElement ) ? . innerText ? . includes ( 'async component' ) )
await page . waitForFunction ( ( ) = > ( document . querySelector ( '#fallback' ) as HTMLElement ) ? . innerText ? . includes ( 'async component' ) )
await page . close ( )
} )
2023-06-25 16:38:15 +00:00
it . skipIf ( isDev ) ( 'should allow server-only components to set prerender hints' , async ( ) = > {
// @ts-expect-error ssssh! untyped secret property
const publicDir = useTestContext ( ) . nuxt . _nitro . options . output . publicDir
expect ( await readdir ( join ( publicDir , 'some' , 'url' , 'from' , 'server-only' , 'component' ) ) . catch ( ( ) = > [ ] ) ) . toContain (
isRenderingJson
? '_payload.json'
: '_payload.js'
)
} )
} )
2023-06-14 09:09:27 +00:00
describe . skipIf ( isDev ( ) || isWindows || ! isRenderingJson ) ( 'prefetching' , ( ) = > {
2022-08-23 19:12:22 +00:00
it ( 'should prefetch components' , async ( ) = > {
await expectNoClientErrors ( '/prefetch/components' )
} )
2023-06-14 09:09:27 +00:00
it ( 'should prefetch server components' , async ( ) = > {
await expectNoClientErrors ( '/prefetch/server-components' )
} )
it ( 'should prefetch everything needed when NuxtLink is used' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page , requests } = await renderPage ( )
2023-06-14 09:09:27 +00:00
2023-08-12 07:18:58 +00:00
await gotoPath ( page , '/prefetch' )
2023-06-14 09:09:27 +00:00
await page . waitForLoadState ( 'networkidle' )
const snapshot = [ . . . requests ]
await page . click ( '[href="/prefetch/server-components"]' )
await page . waitForLoadState ( 'networkidle' )
expect ( await page . innerHTML ( '#async-server-component-count' ) ) . toBe ( '34' )
expect ( requests ) . toEqual ( snapshot )
await page . close ( )
} )
2022-08-30 14:41:11 +00:00
it ( 'should not prefetch certain dynamic imports by default' , async ( ) = > {
const html = await $fetch ( '/auth' )
// should not prefetch global components
expect ( html ) . not . toMatch ( /<link [^>]*\/_nuxt\/TestGlobal[^>]*\.js"/ )
// should not prefetch all other pages
expect ( html ) . not . toMatch ( /<link [^>]*\/_nuxt\/navigate-to[^>]*\.js"/ )
} )
2022-08-23 19:12:22 +00:00
} )
2023-02-14 00:02:41 +00:00
// TODO: make test less flakey on Windows
describe . runIf ( isDev ( ) && ( ! isWindows || ! isCI ) ) ( 'detecting invalid root nodes' , ( ) = > {
2023-02-13 22:09:32 +00:00
it . each ( [ '1' , '2' , '3' , '4' ] ) ( 'should detect invalid root nodes in pages (\'/invalid-root/%s\')' , async ( path ) = > {
const { consoleLogs , page } = await renderPage ( joinURL ( '/invalid-root' , path ) )
await page . evaluate ( ( ) = > new Promise ( resolve = > setTimeout ( resolve , 10 ) ) )
await expectWithPolling (
( ) = > consoleLogs
. map ( w = > w . text ) . join ( '\n' )
. includes ( 'does not have a single root node and will cause errors when navigating between routes' ) ,
true
)
2023-03-09 13:54:46 +00:00
await page . close ( )
2022-09-07 09:55:03 +00:00
} )
2022-08-23 10:25:48 +00:00
2023-02-13 22:09:32 +00:00
it . each ( [ 'fine' ] ) ( 'should not complain if there is no transition (%s)' , async ( path ) = > {
const { consoleLogs , page } = await renderPage ( joinURL ( '/invalid-root' , path ) )
await page . evaluate ( ( ) = > new Promise ( resolve = > setTimeout ( resolve , 10 ) ) )
2022-08-23 10:25:48 +00:00
2023-02-13 22:09:32 +00:00
const consoleLogsWarns = consoleLogs . filter ( i = > i . type === 'warning' )
expect ( consoleLogsWarns . length ) . toEqual ( 0 )
2023-03-09 13:54:46 +00:00
await page . close ( )
2022-08-23 10:25:48 +00:00
} )
2022-09-07 09:55:03 +00:00
} )
2022-08-23 10:25:48 +00:00
2022-09-07 10:41:25 +00:00
// TODO: dynamic paths in dev
2023-02-13 22:09:32 +00:00
describe . skipIf ( isDev ( ) ) ( 'dynamic paths' , ( ) = > {
2022-03-23 14:57:35 +00:00
it ( 'should work with no overrides' , async ( ) = > {
2022-08-26 15:47:29 +00:00
const html : string = await $fetch ( '/assets' )
2022-09-03 13:03:30 +00:00
for ( const match of html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*?)\)/g ) ) {
const url = match [ 2 ] || match [ 3 ]
2022-03-23 14:57:35 +00:00
expect ( url . startsWith ( '/_nuxt/' ) || url === '/public.svg' ) . toBeTruthy ( )
}
} )
2023-01-13 15:00:57 +00:00
// webpack injects CSS differently
2023-02-13 22:09:32 +00:00
it . skipIf ( isWebpack ) ( 'adds relative paths to CSS' , async ( ) = > {
2022-08-26 15:47:29 +00:00
const html : string = await $fetch ( '/assets' )
2022-09-03 13:03:30 +00:00
const urls = Array . from ( html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*?)\)/g ) ) . map ( m = > m [ 2 ] || m [ 3 ] )
2022-08-05 16:35:38 +00:00
const cssURL = urls . find ( u = > /_nuxt\/assets.*\.css$/ . test ( u ) )
2022-04-07 19:15:30 +00:00
expect ( cssURL ) . toBeDefined ( )
2022-08-26 15:47:29 +00:00
const css : string = await $fetch ( cssURL ! )
2022-03-23 14:57:35 +00:00
const imageUrls = Array . from ( css . matchAll ( /url\(([^)]*)\)/g ) ) . map ( m = > m [ 1 ] . replace ( /[-.][\w]{8}\./g , '.' ) )
expect ( imageUrls ) . toMatchInlineSnapshot ( `
2022-03-22 15:51:26 +00:00
[
"./logo.svg" ,
"../public.svg" ,
2022-06-10 15:17:11 +00:00
"../public.svg" ,
"../public.svg" ,
2022-03-22 15:51:26 +00:00
]
` )
2022-03-23 14:57:35 +00:00
} )
2022-03-22 15:51:26 +00:00
2022-03-23 14:57:35 +00:00
it ( 'should allow setting base URL and build assets directory' , async ( ) = > {
2024-01-17 17:58:23 +00:00
await startServer ( {
env : {
NUXT_APP_BUILD_ASSETS_DIR : '/_other/' ,
NUXT_APP_BASE_URL : '/foo/' ,
}
} )
2022-03-22 15:51:26 +00:00
2022-04-07 11:28:04 +00:00
const html = await $fetch ( '/foo/assets' )
2022-09-03 13:03:30 +00:00
for ( const match of html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*?)\)/g ) ) {
const url = match [ 2 ] || match [ 3 ]
2022-07-25 09:52:21 +00:00
expect (
url . startsWith ( '/foo/_other/' ) ||
url === '/foo/public.svg' ||
// TODO: webpack does not yet support dynamic static paths
2023-02-13 22:09:32 +00:00
( isWebpack && url === '/public.svg' )
2022-07-25 09:52:21 +00:00
) . toBeTruthy ( )
}
} )
it ( 'should allow setting relative baseURL' , async ( ) = > {
2024-01-17 17:58:23 +00:00
await startServer ( {
env : {
NUXT_APP_BASE_URL : './'
}
} )
2022-07-25 09:52:21 +00:00
const html = await $fetch ( '/assets' )
2022-09-03 13:03:30 +00:00
for ( const match of html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*?)\)/g ) ) {
const url = match [ 2 ] || match [ 3 ]
2022-07-25 09:52:21 +00:00
expect (
url . startsWith ( './_nuxt/' ) ||
url === './public.svg' ||
// TODO: webpack does not yet support dynamic static paths
2023-02-13 22:09:32 +00:00
( isWebpack && url === '/public.svg' )
2022-07-25 09:52:21 +00:00
) . toBeTruthy ( )
expect ( url . startsWith ( './_nuxt/_nuxt' ) ) . toBeFalsy ( )
2022-03-23 14:57:35 +00:00
}
} )
2022-05-11 17:33:29 +00:00
it ( 'should use baseURL when redirecting' , async ( ) = > {
2024-01-17 17:58:23 +00:00
await startServer ( {
env : {
NUXT_APP_BUILD_ASSETS_DIR : '/_other/' ,
NUXT_APP_BASE_URL : '/foo/' ,
}
} )
2022-05-11 17:33:29 +00:00
const { headers } = await fetch ( '/foo/navigate-to/' , { redirect : 'manual' } )
expect ( headers . get ( 'location' ) ) . toEqual ( '/foo/' )
} )
2022-03-23 14:57:35 +00:00
it ( 'should allow setting CDN URL' , async ( ) = > {
2024-01-17 17:58:23 +00:00
await startServer ( {
env : {
NUXT_APP_BASE_URL : '/foo/' ,
NUXT_APP_CDN_URL : 'https://example.com/' ,
NUXT_APP_BUILD_ASSETS_DIR : '/_cdn/'
}
} )
2022-03-23 14:57:35 +00:00
2022-04-07 11:28:04 +00:00
const html = await $fetch ( '/foo/assets' )
2022-09-03 13:03:30 +00:00
for ( const match of html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*?)\)/g ) ) {
const url = match [ 2 ] || match [ 3 ]
2022-07-25 09:52:21 +00:00
expect (
url . startsWith ( 'https://example.com/_cdn/' ) ||
url === 'https://example.com/public.svg' ||
// TODO: webpack does not yet support dynamic static paths
2023-02-13 22:09:32 +00:00
( isWebpack && url === '/public.svg' )
2022-07-25 09:52:21 +00:00
) . toBeTruthy ( )
2022-03-23 14:57:35 +00:00
}
2022-03-22 15:51:26 +00:00
} )
2022-08-17 15:23:13 +00:00
it ( 'restore server' , async ( ) = > {
await startServer ( )
} )
} )
describe ( 'app config' , ( ) = > {
it ( 'should work' , async ( ) = > {
const html = await $fetch ( '/app-config' )
2023-09-19 21:31:18 +00:00
const expectedAppConfig : Record < string , any > = {
2022-08-17 15:23:13 +00:00
fromNuxtConfig : true ,
nested : {
val : 2
} ,
2023-09-19 21:31:18 +00:00
nuxt : { } ,
2022-08-17 15:23:13 +00:00
fromLayer : true ,
userConfig : 123
}
2023-09-19 21:31:18 +00:00
if ( isTestingAppManifest ) {
expectedAppConfig . nuxt . buildId = 'test'
}
expect . soft ( html . replace ( /"nuxt":\{"buildId":"[^"]+"\}/ , '"nuxt":{"buildId":"test"}' ) ) . toContain ( JSON . stringify ( expectedAppConfig ) )
2023-03-14 09:54:59 +00:00
const serverAppConfig = await $fetch ( '/api/app-config' )
2023-09-19 21:31:18 +00:00
if ( isTestingAppManifest ) {
serverAppConfig . appConfig . nuxt . buildId = 'test'
}
2023-03-14 09:54:59 +00:00
expect ( serverAppConfig ) . toMatchObject ( { appConfig : expectedAppConfig } )
2022-08-17 15:23:13 +00:00
} )
2022-02-18 18:14:57 +00:00
} )
2022-08-30 10:34:09 +00:00
2022-11-24 12:24:14 +00:00
describe ( 'component islands' , ( ) = > {
it ( 'renders components with route' , async ( ) = > {
2023-10-20 15:58:02 +00:00
const result : NuxtIslandResponse = await $fetch ( '/__nuxt_island/RouteComponent.json?url=/foo' )
2022-11-24 12:24:14 +00:00
2024-01-16 13:22:50 +00:00
result . html = result . html . replace ( / data-island-uid="[^"]*"/g , '' )
2023-02-13 22:09:32 +00:00
if ( isDev ( ) ) {
2023-04-20 21:41:20 +00:00
result . head . link = result . head . link . filter ( l = > ! l . href . includes ( '@nuxt+ui-templates' ) && ( l . href . startsWith ( '_nuxt/components/islands/' ) && l . href . includes ( '_nuxt/components/islands/RouteComponent' ) ) )
2022-11-24 12:24:14 +00:00
}
expect ( result ) . toMatchInlineSnapshot ( `
{
2024-01-16 13:22:50 +00:00
"components" : { } ,
2022-11-24 12:24:14 +00:00
"head" : {
"link" : [ ] ,
"style" : [ ] ,
} ,
2024-01-16 13:22:50 +00:00
"html" : " < pre data - island - uid > Route : / f o o
2022-11-24 12:24:14 +00:00
< / pre > " ,
2024-01-16 13:22:50 +00:00
"slots" : { } ,
2022-11-24 12:24:14 +00:00
"state" : { } ,
}
` )
} )
2023-03-20 21:47:06 +00:00
it ( 'render async component' , async ( ) = > {
2023-10-20 15:58:02 +00:00
const result : NuxtIslandResponse = await $fetch ( withQuery ( '/__nuxt_island/LongAsyncComponent.json' , {
2023-03-20 21:47:06 +00:00
props : JSON.stringify ( {
count : 3
} )
} ) )
if ( isDev ( ) ) {
2023-04-20 21:41:20 +00:00
result . head . link = result . head . link . filter ( l = > ! l . href . includes ( '@nuxt+ui-templates' ) && ( l . href . startsWith ( '_nuxt/components/islands/' ) && l . href . includes ( '_nuxt/components/islands/LongAsyncComponent' ) ) )
2023-03-20 21:47:06 +00:00
}
2024-01-16 16:33:45 +00:00
result . html = result . html . replaceAll ( / (data-island-uid|data-island-component)="([^"]*)"/g , '' )
2023-03-20 21:47:06 +00:00
expect ( result ) . toMatchInlineSnapshot ( `
2024-01-16 13:22:50 +00:00
{
"components" : { } ,
"head" : {
"link" : [ ] ,
"style" : [ ] ,
} ,
"html" : "<div data-island-uid><div> count is above 2 </div><!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" default "></div><!--]--> that was very long ... <div id=" long - async - component - count ">3</div> <!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" test "></div><!--]--><p>hello world !!!</p><!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" hello "></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" fallback "></div><!--teleport start--><!--teleport end--><!--]--></div>" ,
"slots" : {
"default" : {
"props" : [ ] ,
} ,
"fallback" : {
"fallback" : "<!--[--><div style=" display :contents ; "><div>fall slot -- index: 0</div><div class=" fallback - slot - content "> wonderful fallback </div></div><div style=" display :contents ; "><div>back slot -- index: 1</div><div class=" fallback - slot - content "> wonderful fallback </div></div><!--]--><!--teleport anchor-->" ,
"props" : [
{
"t" : "fall" ,
} ,
{
"t" : "back" ,
} ,
] ,
} ,
"hello" : {
"fallback" : "<!--[--><div style=" display :contents ; "><div> fallback slot -- index: 0</div></div><div style=" display :contents ; "><div> fallback slot -- index: 1</div></div><div style=" display :contents ; "><div> fallback slot -- index: 2</div></div><!--]--><!--teleport anchor-->" ,
"props" : [
{
"t" : 0 ,
} ,
{
"t" : 1 ,
} ,
{
"t" : 2 ,
} ,
] ,
} ,
"test" : {
"props" : [
{
"count" : 3 ,
} ,
] ,
} ,
} ,
"state" : { } ,
}
` )
2023-03-20 21:47:06 +00:00
} )
it ( 'render .server async component' , async ( ) = > {
2023-10-20 15:58:02 +00:00
const result : NuxtIslandResponse = await $fetch ( withQuery ( '/__nuxt_island/AsyncServerComponent.json' , {
2023-03-20 21:47:06 +00:00
props : JSON.stringify ( {
count : 2
} )
} ) )
if ( isDev ( ) ) {
2023-04-20 21:41:20 +00:00
result . head . link = result . head . link . filter ( l = > ! l . href . includes ( '@nuxt+ui-templates' ) && ( l . href . startsWith ( '_nuxt/components/islands/' ) && l . href . includes ( '_nuxt/components/islands/AsyncServerComponent' ) ) )
2023-03-20 21:47:06 +00:00
}
2023-12-19 12:21:29 +00:00
result . props = { }
2024-01-16 13:22:50 +00:00
result . components = { }
result . slots = { }
2024-01-16 16:33:45 +00:00
result . html = result . html . replaceAll ( / (data-island-uid|data-island-component)="([^"]*)"/g , '' )
2023-12-19 12:21:29 +00:00
2023-03-20 21:47:06 +00:00
expect ( result ) . toMatchInlineSnapshot ( `
2024-01-16 13:22:50 +00:00
{
"components" : { } ,
"head" : {
"link" : [ ] ,
"style" : [ ] ,
} ,
"html" : "<div data-island-uid> This is a .server (20ms) async component that was very long ... <div id=" async - server - component - count ">2</div><div class=" sugar - counter "> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" default "></div><!--]--></div>" ,
"props" : { } ,
"slots" : { } ,
"state" : { } ,
}
` )
2023-12-19 12:21:29 +00:00
} )
if ( ! isWebpack ) {
it ( 'render server component with selective client hydration' , async ( ) = > {
const result : NuxtIslandResponse = await $fetch ( '/__nuxt_island/ServerWithClient' )
if ( isDev ( ) ) {
result . head . link = result . head . link . filter ( l = > ! l . href . includes ( '@nuxt+ui-templates' ) && ( l . href . startsWith ( '_nuxt/components/islands/' ) && l . href . includes ( '_nuxt/components/islands/AsyncServerComponent' ) ) )
}
2024-01-16 13:22:50 +00:00
const { components } = result
result . components = { }
result . slots = { }
2024-01-16 16:33:45 +00:00
result . html = result . html . replace ( / data-island-component="([^"]*)"/g , ( _ , content ) = > ` data-island-component=" ${ content . split ( '-' ) [ 0 ] } " ` )
2023-12-19 12:21:29 +00:00
2024-01-16 13:22:50 +00:00
const teleportsEntries = Object . entries ( components || { } )
2023-12-19 12:21:29 +00:00
expect ( result ) . toMatchInlineSnapshot ( `
{
2024-01-16 13:22:50 +00:00
"components" : { } ,
2023-12-19 12:21:29 +00:00
"head" : {
"link" : [ ] ,
"style" : [ ] ,
} ,
2024-01-16 16:33:45 +00:00
"html" : "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class=" sugar - counter "> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class=" interactive - component - wrapper " style=" border :solid 1 px red ; "> The component bellow is not a slot but declared as interactive <!--[--><div style=" display : contents ; " data-island-uid data-island-component=" Counter "></div><!--teleport start--><!--teleport end--><!--]--></div></div>" ,
2024-01-16 13:22:50 +00:00
"slots" : { } ,
2023-12-19 12:21:29 +00:00
"state" : { } ,
}
` )
expect ( teleportsEntries ) . toHaveLength ( 1 )
expect ( teleportsEntries [ 0 ] [ 0 ] . startsWith ( 'Counter-' ) ) . toBeTruthy ( )
2024-01-16 13:22:50 +00:00
expect ( teleportsEntries [ 0 ] [ 1 ] . props ) . toMatchInlineSnapshot ( `
2023-05-15 22:43:53 +00:00
{
2023-12-19 12:21:29 +00:00
"multiplier" : 1 ,
2023-05-15 22:43:53 +00:00
}
` )
2024-01-16 13:22:50 +00:00
expect ( teleportsEntries [ 0 ] [ 1 ] . html ) . toMatchInlineSnapshot ( '"<div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--teleport anchor-->"' )
2023-12-19 12:21:29 +00:00
} )
}
2023-03-20 21:47:06 +00:00
2022-11-24 12:24:14 +00:00
it ( 'renders pure components' , async ( ) = > {
2023-10-20 15:58:02 +00:00
const result : NuxtIslandResponse = await $fetch ( withQuery ( '/__nuxt_island/PureComponent.json' , {
2022-11-24 12:24:14 +00:00
props : JSON.stringify ( {
bool : false ,
number : 3487 ,
str : 'something' ,
obj : { foo : 42 , bar : false , me : 'hi' }
} )
} ) )
2024-01-16 13:22:50 +00:00
result . html = result . html . replace ( / data-island-uid="([^"]*)"/g , '' )
2022-11-24 12:24:14 +00:00
2023-02-13 22:09:32 +00:00
if ( isDev ( ) ) {
2022-11-24 12:24:14 +00:00
result . head . link = result . head . link . filter ( l = > ! l . href . includes ( '@nuxt+ui-templates' ) )
2022-12-08 15:16:22 +00:00
const fixtureDir = normalize ( fileURLToPath ( new URL ( './fixtures/basic' , import . meta . url ) ) )
for ( const link of result . head . link ) {
link . href = link . href . replace ( fixtureDir , '/<rootDir>' ) . replaceAll ( '//' , '/' )
link . key = link . key . replace ( /-[a-zA-Z0-9]+$/ , '' )
}
2022-11-24 12:24:14 +00:00
}
2023-02-13 22:09:32 +00:00
// TODO: fix rendering of styles in webpack
if ( ! isDev ( ) && ! isWebpack ) {
2023-06-20 18:28:44 +00:00
expect ( normaliseIslandResult ( result ) . head ) . toMatchInlineSnapshot ( `
2022-11-24 12:24:14 +00:00
{
"link" : [ ] ,
"style" : [
{
"innerHTML" : "pre[data-v-xxxxx]{color:blue}" ,
"key" : "island-style" ,
} ,
] ,
}
2023-06-20 18:28:44 +00:00
` )
2023-02-13 22:09:32 +00:00
} else if ( isDev ( ) && ! isWebpack ) {
2022-11-24 12:24:14 +00:00
expect ( result . head ) . toMatchInlineSnapshot ( `
{
"link" : [
{
"href" : "/_nuxt/components/islands/PureComponent.vue?vue&type=style&index=0&scoped=c0c0cf89&lang.css" ,
2022-12-08 15:16:22 +00:00
"key" : "island-link" ,
"rel" : "stylesheet" ,
} ,
2022-11-24 12:24:14 +00:00
] ,
"style" : [ ] ,
}
` )
}
2024-01-16 13:22:50 +00:00
expect ( result . html . replace ( /data-v-\w+|"|<!--.*-->/g , '' ) . replace ( /data-island-uid="[^"]"/g , '' ) ) . toMatchInlineSnapshot ( `
" < div data - island - uid > Was router enabled : true < br > Props : < pre > {
2023-05-15 22:43:53 +00:00
number : 3487 ,
str : something ,
obj : {
foo : 42 ,
bar : false ,
me : hi
} ,
bool : false
} < / pre > < / div > "
` )
2022-11-24 12:24:14 +00:00
expect ( result . state ) . toMatchInlineSnapshot ( `
{
"$shasRouter" : true ,
}
` )
} )
2023-05-15 22:43:53 +00:00
it ( 'test client-side navigation' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page } = await renderPage ( '/' )
2023-05-15 22:43:53 +00:00
await page . click ( '#islands' )
2023-08-12 07:18:58 +00:00
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/islands' )
2023-05-15 22:43:53 +00:00
await page . locator ( '#increase-pure-component' ) . click ( )
await page . waitForResponse ( response = > response . url ( ) . includes ( '/__nuxt_island/' ) && response . status ( ) === 200 )
2023-08-12 07:18:58 +00:00
await page . locator ( '#slot-in-server' ) . getByText ( 'Slot with in .server component' ) . waitFor ( )
await page . locator ( '#test-slot' ) . getByText ( 'Slot with name test' ) . waitFor ( )
2023-05-15 22:43:53 +00:00
// test islands update
expect ( await page . locator ( '.box' ) . innerHTML ( ) ) . toContain ( '"number": 101,' )
2024-01-01 12:33:11 +00:00
const islandRequests = [
2023-05-15 22:43:53 +00:00
page . waitForResponse ( response = > response . url ( ) . includes ( '/__nuxt_island/LongAsyncComponent' ) && response . status ( ) === 200 ) ,
page . waitForResponse ( response = > response . url ( ) . includes ( '/__nuxt_island/AsyncServerComponent' ) && response . status ( ) === 200 )
2024-01-01 12:33:11 +00:00
]
await page . locator ( '#update-server-components' ) . click ( )
await Promise . all ( islandRequests )
2023-08-12 07:18:58 +00:00
await page . locator ( '#long-async-component-count' ) . getByText ( '1' ) . waitFor ( )
2023-05-15 22:43:53 +00:00
// test islands slots interactivity
await page . locator ( '#first-sugar-counter button' ) . click ( )
expect ( await page . locator ( '#first-sugar-counter' ) . innerHTML ( ) ) . toContain ( 'Sugar Counter 13' )
2023-05-28 15:38:36 +00:00
2023-12-19 12:21:29 +00:00
if ( ! isWebpack ) {
// test client component interactivity
expect ( await page . locator ( '.interactive-component-wrapper' ) . innerHTML ( ) ) . toContain ( 'Sugar Counter 12' )
await page . locator ( '.interactive-component-wrapper button' ) . click ( )
expect ( await page . locator ( '.interactive-component-wrapper' ) . innerHTML ( ) ) . toContain ( 'Sugar Counter 13' )
}
2023-05-28 15:38:36 +00:00
await page . close ( )
2023-05-15 22:43:53 +00:00
} )
2023-07-18 15:07:35 +00:00
it . skipIf ( isDev ( ) ) ( 'should not render an error when having a baseURL' , async ( ) = > {
2024-01-17 17:58:23 +00:00
await startServer ( {
env : {
NUXT_APP_BASE_URL : '/foo/'
}
} )
2023-07-18 15:07:35 +00:00
const result = await fetch ( '/foo/islands' )
expect ( result . status ) . toBe ( 200 )
await startServer ( )
} )
2022-11-24 12:24:14 +00:00
} )
2023-02-13 22:09:32 +00:00
describe . runIf ( isDev ( ) && ! isWebpack ) ( 'vite plugins' , ( ) = > {
2023-01-16 16:04:16 +00:00
it ( 'does not override vite plugins' , async ( ) = > {
expect ( await $fetch ( '/vite-plugin-without-path' ) ) . toBe ( 'vite-plugin without path' )
expect ( await $fetch ( '/__nuxt-test' ) ) . toBe ( 'vite-plugin with __nuxt prefix' )
} )
it ( 'does not allow direct access to nuxt source folder' , async ( ) = > {
2023-02-16 12:56:14 +00:00
expect ( await $fetch ( '/app.config' ) ) . toContain ( 'catchall at' )
2023-01-16 16:04:16 +00:00
} )
} )
2023-04-11 22:57:12 +00:00
describe . skipIf ( isDev ( ) || isWindows || ! isRenderingJson ) ( 'payload rendering' , ( ) = > {
2022-09-10 13:57:16 +00:00
it ( 'renders a payload' , async ( ) = > {
2023-04-07 10:34:35 +00:00
const payload = await $fetch ( '/random/a/_payload.json' , { responseType : 'text' } )
const data = parsePayload ( payload )
expect ( typeof data . prerenderedAt ) . toEqual ( 'number' )
expect ( data . data ) . toMatchObject ( {
hey : {
baz : 'qux' ,
foo : 'bar'
} ,
rand_a : expect.arrayContaining ( [ expect . anything ( ) ] )
} )
2022-09-10 13:57:16 +00:00
} )
it ( 'does not fetch a prefetched payload' , async ( ) = > {
2023-08-12 07:18:58 +00:00
const { page , requests } = await renderPage ( )
2022-09-13 20:20:23 +00:00
2023-08-12 07:18:58 +00:00
await gotoPath ( page , '/random/a' )
2022-09-10 13:57:16 +00:00
// We are manually prefetching other payloads
2023-08-12 07:18:58 +00:00
await page . waitForRequest ( url ( '/random/c/_payload.json' ) )
2022-09-10 13:57:16 +00:00
// We are not triggering API requests in the payload
expect ( requests ) . not . toContain ( expect . stringContaining ( '/api/random' ) )
2023-01-20 12:10:58 +00:00
expect ( requests ) . not . toContain ( expect . stringContaining ( '/__nuxt_island' ) )
2022-09-13 20:20:23 +00:00
// requests.length = 0
2022-09-10 13:57:16 +00:00
await page . click ( '[href="/random/b"]' )
await page . waitForLoadState ( 'networkidle' )
2022-09-13 20:20:23 +00:00
2022-09-10 13:57:16 +00:00
// We are not triggering API requests in the payload in client-side nav
expect ( requests ) . not . toContain ( '/api/random' )
2023-01-20 12:10:58 +00:00
expect ( requests ) . not . toContain ( expect . stringContaining ( '/__nuxt_island' ) )
2022-09-13 20:20:23 +00:00
2022-09-10 13:57:16 +00:00
// We are fetching a payload we did not prefetch
2023-04-07 10:34:35 +00:00
expect ( requests ) . toContain ( '/random/b/_payload.json' )
2022-09-13 20:20:23 +00:00
2022-09-10 13:57:16 +00:00
// We are not refetching payloads we've already prefetched
2022-09-13 20:20:23 +00:00
// expect(requests.filter(p => p.includes('_payload')).length).toBe(1)
// requests.length = 0
2022-09-10 13:57:16 +00:00
await page . click ( '[href="/random/c"]' )
await page . waitForLoadState ( 'networkidle' )
2022-09-13 20:20:23 +00:00
2022-09-10 13:57:16 +00:00
// We are not triggering API requests in the payload in client-side nav
expect ( requests ) . not . toContain ( '/api/random' )
2023-01-20 12:10:58 +00:00
expect ( requests ) . not . toContain ( expect . stringContaining ( '/__nuxt_island' ) )
2022-09-13 20:20:23 +00:00
2022-09-10 13:57:16 +00:00
// We are not refetching payloads we've already prefetched
// Note: we refetch on dev as urls differ between '' and '?import'
2023-02-13 22:09:32 +00:00
// expect(requests.filter(p => p.includes('_payload')).length).toBe(isDev() ? 1 : 0)
2023-03-09 13:54:46 +00:00
await page . close ( )
2022-09-10 13:57:16 +00:00
} )
2023-06-14 09:09:27 +00:00
it . skipIf ( ! isRenderingJson ) ( 'should not include server-component HTML in payload' , async ( ) = > {
const payload = await $fetch ( '/prefetch/server-components/_payload.json' , { responseType : 'text' } )
const entries = Object . entries ( parsePayload ( payload ) )
2023-10-10 11:14:55 +00:00
const [ key , serializedComponent ] = entries . find ( ( [ key ] ) = > key . startsWith ( 'AsyncServerComponent' ) ) || [ ]
expect ( serializedComponent ) . toEqual ( key )
2023-06-14 09:09:27 +00:00
} )
2022-09-10 13:57:16 +00:00
} )
2023-12-16 11:09:41 +00:00
describe . skipIf ( process . env . TEST_CONTEXT !== 'async' ) ( 'Async context' , ( ) = > {
it ( 'should be available' , async ( ) = > {
2023-08-07 22:57:35 +00:00
expect ( await $fetch ( '/async-context' ) ) . toContain ( '"hasApp": true' )
} )
} )
2024-01-18 09:59:59 +00:00
describe . skipIf ( process . env . TEST_CONTEXT === 'async' ) ( 'Async context' , ( ) = > {
it ( 'should be unavailable' , async ( ) = > {
expect ( await $fetch ( '/async-context' ) ) . toContain ( '"hasApp": false' )
} )
} )
2022-09-14 15:11:00 +00:00
describe . skipIf ( isWindows ) ( 'useAsyncData' , ( ) = > {
2023-10-16 19:20:02 +00:00
it ( 'works after useNuxtData call' , async ( ) = > {
const page = await createPage ( '/useAsyncData/nuxt-data' )
expect ( await page . locator ( 'body' ) . getByText ( 'resolved:true' ) . textContent ( ) ) . toContain ( 'resolved:true' )
await page . close ( )
} )
2022-08-30 10:34:09 +00:00
it ( 'single request resolves' , async ( ) = > {
await expectNoClientErrors ( '/useAsyncData/single' )
} )
it ( 'two requests resolve' , async ( ) = > {
await expectNoClientErrors ( '/useAsyncData/double' )
} )
it ( 'two requests resolve and sync' , async ( ) = > {
await $fetch ( '/useAsyncData/refresh' )
} )
2022-10-10 10:33:16 +00:00
it ( 'requests can be cancelled/overridden' , async ( ) = > {
await expectNoClientErrors ( '/useAsyncData/override' )
} )
2022-08-30 10:34:09 +00:00
it ( 'two requests made at once resolve and sync' , async ( ) = > {
await expectNoClientErrors ( '/useAsyncData/promise-all' )
} )
2023-06-09 21:38:14 +00:00
it ( 'requests status can be used' , async ( ) = > {
const html = await $fetch ( '/useAsyncData/status' )
expect ( html ) . toContain ( 'true' )
expect ( html ) . not . toContain ( 'false' )
const page = await createPage ( '/useAsyncData/status' )
2023-08-12 07:18:58 +00:00
await page . locator ( '#status5-values' ) . getByText ( 'idle,pending,success' ) . waitFor ( )
2023-06-09 21:38:14 +00:00
await page . close ( )
} )
2023-08-24 12:06:29 +00:00
it ( 'data is null after navigation when immediate false' , async ( ) = > {
const page = await createPage ( '/useAsyncData/immediate-remove-unmounted' )
await page . waitForLoadState ( 'networkidle' )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/useAsyncData/immediate-remove-unmounted' )
expect ( await page . locator ( '#immediate-data' ) . getByText ( 'null' ) . textContent ( ) ) . toBe ( 'null' )
await page . click ( '#execute-btn' )
expect ( await page . locator ( '#immediate-data' ) . getByText ( ',' ) . textContent ( ) ) . not . toContain ( 'null' )
await page . click ( '#to-index' )
await page . click ( '#to-immediate-remove-unmounted' )
expect ( await page . locator ( '#immediate-data' ) . getByText ( 'null' ) . textContent ( ) ) . toBe ( 'null' )
await page . click ( '#execute-btn' )
expect ( await page . locator ( '#immediate-data' ) . getByText ( ',' ) . textContent ( ) ) . not . toContain ( 'null' )
await page . close ( )
} )
2022-08-30 10:34:09 +00:00
} )
2023-04-06 12:07:22 +00:00
describe . runIf ( isDev ( ) ) ( 'component testing' , ( ) = > {
it ( 'should work' , async ( ) = > {
2023-11-27 23:02:02 +00:00
const comp1 = await $fetchComponent ( 'components/Counter.vue' , { multiplier : 2 } )
2023-04-06 12:07:22 +00:00
expect ( comp1 ) . toContain ( '12 x 2 = 24' )
2023-11-27 23:02:02 +00:00
const comp2 = await $fetchComponent ( 'components/Counter.vue' , { multiplier : 4 } )
2023-04-06 12:07:22 +00:00
expect ( comp2 ) . toContain ( '12 x 4 = 48' )
} )
} )
2023-06-20 18:28:44 +00:00
2023-11-14 16:56:31 +00:00
describe ( 'keepalive' , ( ) = > {
it ( 'should not keepalive by default' , async ( ) = > {
const { page , consoleLogs } = await renderPage ( '/keepalive' )
const pageName = 'not-keepalive'
await page . click ( ` # ${ pageName } ` )
await page . waitForTimeout ( 25 )
expect ( consoleLogs . map ( l = > l . text ) . filter ( t = > t . includes ( 'keepalive' ) ) ) . toEqual ( [ ` ${ pageName } : onMounted ` ] )
await page . close ( )
} )
it ( 'should not keepalive when included in app config but config in nuxt-page is not undefined' , async ( ) = > {
const { page , consoleLogs } = await renderPage ( '/keepalive' )
const pageName = 'keepalive-in-config'
await page . click ( ` # ${ pageName } ` )
await page . waitForTimeout ( 25 )
expect ( consoleLogs . map ( l = > l . text ) . filter ( t = > t . includes ( 'keepalive' ) ) ) . toEqual ( [ ` ${ pageName } : onMounted ` ] )
await page . close ( )
} )
it ( 'should not keepalive when included in app config but exclueded in nuxt-page' , async ( ) = > {
const { page , consoleLogs } = await renderPage ( '/keepalive' )
const pageName = 'not-keepalive-in-nuxtpage'
await page . click ( ` # ${ pageName } ` )
await page . waitForTimeout ( 25 )
expect ( consoleLogs . map ( l = > l . text ) . filter ( t = > t . includes ( 'keepalive' ) ) ) . toEqual ( [ ` ${ pageName } : onMounted ` ] )
await page . close ( )
} )
it ( 'should keepalive when included in nuxt-page' , async ( ) = > {
const { page , consoleLogs } = await renderPage ( '/keepalive' )
const pageName = 'keepalive-in-nuxtpage'
await page . click ( ` # ${ pageName } ` )
await page . waitForTimeout ( 25 )
expect ( consoleLogs . map ( l = > l . text ) . filter ( t = > t . includes ( 'keepalive' ) ) ) . toEqual ( [ ` ${ pageName } : onMounted ` , ` ${ pageName } : onActivated ` ] )
await page . close ( )
} )
it ( 'should preserve keepalive config when navigate routes in nuxt-page' , async ( ) = > {
const { page , consoleLogs } = await renderPage ( '/keepalive' )
await page . click ( '#keepalive-in-nuxtpage' )
await page . waitForTimeout ( 25 )
await page . click ( '#keepalive-in-nuxtpage-2' )
await page . waitForTimeout ( 25 )
await page . click ( '#keepalive-in-nuxtpage' )
await page . waitForTimeout ( 25 )
await page . click ( '#not-keepalive' )
await page . waitForTimeout ( 25 )
await page . click ( '#keepalive-in-nuxtpage-2' )
await page . waitForTimeout ( 25 )
expect ( consoleLogs . map ( l = > l . text ) . filter ( t = > t . includes ( 'keepalive' ) ) ) . toEqual ( [
'keepalive-in-nuxtpage: onMounted' ,
'keepalive-in-nuxtpage: onActivated' ,
'keepalive-in-nuxtpage: onDeactivated' ,
'keepalive-in-nuxtpage-2: onMounted' ,
'keepalive-in-nuxtpage-2: onActivated' ,
'keepalive-in-nuxtpage: onActivated' ,
'keepalive-in-nuxtpage-2: onDeactivated' ,
'keepalive-in-nuxtpage: onDeactivated' ,
'not-keepalive: onMounted' ,
'keepalive-in-nuxtpage-2: onActivated' ,
'not-keepalive: onUnmounted'
] )
await page . close ( )
} )
} )
2024-01-18 16:09:27 +00:00
describe ( 'Node.js compatibility for client-side' , ( ) = > {
it ( 'should work' , async ( ) = > {
const { page } = await renderPage ( '/node-compat' )
const html = await page . innerHTML ( 'body' )
expect ( html ) . toContain ( 'Nuxt is Awesome!' )
expect ( html ) . toContain ( 'CWD: [available]' )
await page . close ( )
} )
} )
2023-06-20 18:28:44 +00:00
function normaliseIslandResult ( result : NuxtIslandResponse ) {
return {
. . . result ,
head : {
. . . result . head ,
style : result.head.style.map ( s = > ( {
. . . s ,
innerHTML : ( s . innerHTML || '' ) . replace ( /data-v-[a-z0-9]+/ , 'data-v-xxxxx' ) . replace ( /\.[a-zA-Z0-9]+\.svg/ , '.svg' ) ,
key : s.key.replace ( /-[a-zA-Z0-9]+$/ , '' )
} ) )
}
}
}