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'
2024-06-26 13:18:05 +00:00
import { $fetch as _ $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
2024-08-22 12:05:39 +00:00
import { resolveUnrefHeadInput } from '@unhead/vue'
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
2024-03-08 22:58:37 +00:00
import type { NuxtIslandResponse } from '#app'
2024-06-26 13:18:05 +00:00
// TODO: update @nuxt/test-utils
const $fetch = _ $fetch as import ( 'nitro/types' ) . $Fetch < unknown , import ( ' nitro / types ' ) .NitroFetchRequest >
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 : {
2024-06-29 23:02:51 +00:00
hooks : {
'modules:done' ( ) {
// TODO: investigate whether to upstream a fix to vite-plugin-vue or nuxt/test-utils
// Vite reads its `isProduction` value from NODE_ENV and passes this to some plugins
// like vite-plugin-vue
if ( process . env . TEST_ENV !== 'dev' ) {
process . env . NODE_ENV = 'production'
}
} ,
} ,
2023-02-13 22:09:32 +00:00
builder : isWebpack ? 'webpack' : 'vite' ,
2024-04-05 18:08:32 +00:00
} ,
2022-03-22 18:12:54 +00:00
} )
describe ( 'server api' , ( ) = > {
it ( 'should serialize' , async ( ) = > {
2024-05-20 06:02:54 +00:00
expect ( await $fetch < string > ( '/api/hello' ) ) . toBe ( 'Hello API' )
expect ( await $fetch < string > ( '/api/hey' ) ) . toEqual ( {
2022-03-22 18:12:54 +00:00
foo : 'bar' ,
2024-04-05 18:08:32 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
expect ( await $fetch < string > ( '/api/counter' ) ) . toEqual ( { count : 0 } )
expect ( await $fetch < string > ( '/api/counter' ) ) . toEqual ( { count : 1 } )
expect ( await $fetch < string > ( '/api/counter' ) ) . toEqual ( { count : 2 } )
expect ( await $fetch < string > ( '/api/counter' ) ) . toEqual ( { count : 3 } )
2022-03-22 18:12:54 +00:00
} )
2023-09-28 10:08:02 +00:00
it ( 'should auto-import' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const res = await $fetch < string > ( '/api/auto-imports' )
2023-09-28 10:08:02 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const headHtml = await $fetch < string > ( '/route-rules/spa' )
2024-05-13 17:45:21 +00:00
// SPA should render appHead tags
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">' )
2024-06-07 14:24:56 +00:00
expect ( headHtml . match ( /<meta name="viewport" content="width=1024, initial-scale=1">/g ) ) . toHaveLength ( 1 )
2024-05-13 17:45:21 +00:00
const { script , attrs } = parseData ( headHtml )
2023-04-07 10:34:35 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/route-rules/spa' )
2023-12-13 11:54:56 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/no-scripts' )
2023-08-12 07:18:58 +00:00
expect ( html ) . not . toContain ( '<script' )
2023-04-11 14:17:44 +00:00
} )
2024-03-16 18:53:01 +00:00
it . runIf ( isTestingAppManifest ) ( 'should run middleware defined in routeRules config' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/route-rules/middleware' )
2024-03-16 18:53:01 +00:00
expect ( html ) . toContain ( 'Hello from routeRules!' )
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const result = await $fetch < string > ( '/auto-registered-module' )
2023-03-03 17:52:55 +00:00
expect ( result ) . toEqual ( 'handler added by auto-registered module' )
} )
} )
2022-03-22 18:12:54 +00:00
describe ( 'pages' , ( ) = > {
it ( 'render index' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/' )
2022-03-22 18:12:54 +00:00
// 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-08-22 12:05:39 +00:00
expect ( html . replaceAll ( / data-island-uid="[^"]*"/g , '' ) ) . toContain ( '<div class="server-only" style="background-color:gray;"> server-only component <div> server-only component child (non-server-only) </div></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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/jsx' )
2023-02-16 12:45:08 +00:00
// 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/some-alias' )
2022-09-22 13:54:34 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/add-route-test' )
2023-06-27 10:15:35 +00:00
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' )
} )
2024-06-13 16:59:24 +00:00
it ( 'preserves page metadata added in pages:extend hook' , async ( ) = > {
const html = await $fetch < string > ( '/some-custom-path' )
expect ( html . match ( /<pre>([^<]*)<\/pre>/ ) ? . [ 1 ] ? . trim ( ) . replace ( /"/g , '"' ) . replace ( />/g , '>' ) ) . toMatchInlineSnapshot ( `
" {
"name" : "some-custom-name" ,
"path" : "/some-custom-path" ,
"validate" : "() => true" ,
"middleware" : [
"() => true"
] ,
"otherValue" : "{\\" foo \ \ ":\\" bar \ \ "}"
} "
` )
} )
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
} )
2024-08-21 11:51:22 +00:00
it ( 'validates routes with custom statusCode and statusMessage' , async ( ) = > {
const CUSTOM_ERROR_CODE = 401
const CUSTOM_ERROR_MESSAGE = 'Custom error message'
const ERROR_PAGE_TEXT = 'This is the error page'
const PAGE_TEXT = 'You should not see this'
// Check status code and message on fetch
const res = await fetch ( '/validate-custom-error' )
const serverText = await res . text ( )
expect ( res . status ) . toEqual ( CUSTOM_ERROR_CODE )
expect ( serverText ) . toContain ( CUSTOM_ERROR_MESSAGE )
expect ( serverText ) . not . toContain ( PAGE_TEXT )
// Client-side navigation
const { page } = await renderPage ( '/navigate-to-validate-custom-error' )
await page . getByText ( 'should throw a 401 error with custom message' ) . click ( )
// error.vue has an h1 tag
await page . waitForSelector ( 'h1' )
const clientText = await page . innerText ( 'body' )
expect ( clientText ) . toContain ( CUSTOM_ERROR_MESSAGE )
expect ( clientText ) . toContain ( ERROR_PAGE_TEXT )
expect ( clientText ) . not . toContain ( PAGE_TEXT )
await page . close ( )
// Server-side navigation
const { page : serverPage } = await renderPage ( '/validate-custom-error' )
const serverPageText = await serverPage . innerText ( 'body' )
expect ( serverPageText ) . toContain ( CUSTOM_ERROR_MESSAGE )
expect ( serverPageText ) . toContain ( ERROR_PAGE_TEXT )
expect ( serverPageText ) . not . toContain ( PAGE_TEXT )
await serverPage . close ( )
} )
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-06-06 21:47:32 +00:00
it ( 'should render correctly when loaded on a different path' , async ( ) = > {
2024-03-16 20:19:07 +00:00
const { page , pageErrors } = await renderPage ( )
await page . goto ( url ( '/proxy' ) )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) && ! window . useNuxtApp ? . ( ) . isHydrating )
2023-06-06 21:47:32 +00:00
expect ( await page . innerText ( 'body' ) ) . toContain ( 'Composable | foo: auto imported from ~/composables/foo.ts' )
2024-03-16 20:19:07 +00:00
expect ( pageErrors ) . toEqual ( [ ] )
2023-06-06 21:47:32 +00:00
2023-06-14 09:09:27 +00:00
await page . close ( )
2023-06-06 21:47:32 +00:00
} )
2022-05-03 09:31:58 +00:00
it ( 'preserves query' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/?test=true' )
2022-05-03 09:31:58 +00:00
// 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/nested/foobar' )
2022-03-08 18:03:21 +00:00
2022-03-22 18:12:54 +00:00
// TODO: should resolved to same entry
2024-05-20 06:02:54 +00:00
// const html2 = await $fetch<string>('/nested/foobar/index')
2022-03-22 18:12:54 +00:00
// 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/parent' )
2022-03-25 11:55:05 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/another-parent' )
2022-03-25 11:55:05 +00:00
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' )
2024-01-24 11:49:47 +00:00
const { page } = await renderPage ( '/client-server' )
2023-10-16 13:09:54 +00:00
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' )
2024-01-24 11:49:47 +00:00
await page . close ( )
2023-10-16 13:09:54 +00:00
} )
2022-08-17 15:26:51 +00:00
it ( '/client-only-components' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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' ,
2024-04-05 18:08:32 +00:00
'.no-state-hidden' ,
2022-10-03 14:14:55 +00:00
]
const visibleSelectors = [
'.string-stateful' ,
'.string-stateful-script' ,
'.client-only-script' ,
'.client-only-script-setup' ,
2024-04-05 18:08:32 +00:00
'.no-state' ,
2022-10-03 14:14:55 +00:00
]
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' )
2024-08-12 08:37:43 +00:00
const waitForConsoleLog = page . waitForEvent ( 'console' , consoleLog = > consoleLog . text ( ) === 'has $el' )
2022-10-03 14:14:55 +00:00
// 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
2024-08-12 08:37:43 +00:00
await waitForConsoleLog
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 ( )
2024-03-09 06:48:15 +00:00
await page2 . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/client-only-components' )
2023-10-25 00:34:22 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/client-only-explicit-import' )
2022-10-11 15:26:03 +00:00
// 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' )
2024-03-16 20:38:29 +00:00
await page . waitForLoadState ( 'networkidle' )
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' ,
2024-04-05 18:08:32 +00:00
'clientfallback-async-setup' ,
2023-03-08 21:13:06 +00:00
]
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/client-fallback' )
2023-03-08 21:13:06 +00:00
// 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' )
2024-04-23 12:53:11 +00:00
// async setup
2023-11-03 21:04:26 +00:00
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
} )
2024-03-06 14:38:39 +00:00
it ( 'client only page' , async ( ) = > {
2024-06-25 06:48:39 +00:00
const response = await fetch ( '/client-only-page' ) . then ( r = > r . text ( ) )
2024-03-06 14:38:39 +00:00
// Should not contain rendered page on initial request
expect ( response ) . not . toContain ( '"hasAccessToWindow": true' )
expect ( response ) . not . toContain ( '"isServer": false' )
const errors : string [ ] = [ ]
const { page : clientInitialPage } = await renderPage ( '/client-only-page' )
clientInitialPage . on ( 'console' , ( message ) = > {
const type = message . type ( )
if ( type === 'error' || type === 'warning' ) {
errors . push ( message . text ( ) )
}
} )
// But after hydration element should appear and contain this object
expect ( await clientInitialPage . locator ( '#state' ) . textContent ( ) ) . toMatchInlineSnapshot ( `
" {
"hasAccessToWindow" : true ,
"isServer" : false
} "
` )
2024-03-09 06:48:15 +00:00
expect ( await clientInitialPage . locator ( '#server-rendered' ) . textContent ( ) ) . toMatchInlineSnapshot ( '"false"' )
2024-03-06 14:38:39 +00:00
// Then go to non client only page
await clientInitialPage . click ( 'a' )
2024-06-25 06:48:39 +00:00
await clientInitialPage . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/client-only-page/normal' )
2024-03-06 14:38:39 +00:00
// that page should be client rendered
2024-07-23 20:44:35 +00:00
expect ( await clientInitialPage . locator ( '#server-rendered' ) . textContent ( ) ) . toMatchInlineSnapshot ( '"false"' )
2024-03-06 14:38:39 +00:00
// and not contain any errors or warnings
expect ( errors . length ) . toBe ( 0 )
await clientInitialPage . close ( )
errors . length = 0
const { page : normalInitialPage } = await renderPage ( '/client-only-page/normal' )
normalInitialPage . on ( 'console' , ( message ) = > {
const type = message . type ( )
if ( type === 'error' || type === 'warning' ) {
errors . push ( message . text ( ) )
}
} )
// Now non client only page should be sever rendered
2024-03-09 06:48:15 +00:00
expect ( await normalInitialPage . locator ( '#server-rendered' ) . textContent ( ) ) . toMatchInlineSnapshot ( '"true"' )
2024-03-06 14:38:39 +00:00
// Go to client only page
await normalInitialPage . click ( 'a' )
2024-06-25 06:48:39 +00:00
await normalInitialPage . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/client-only-page' )
2024-03-06 14:38:39 +00:00
// and expect same object to be present
expect ( await normalInitialPage . locator ( '#state' ) . textContent ( ) ) . toMatchInlineSnapshot ( `
" {
"hasAccessToWindow" : true ,
"isServer" : false
} "
` )
// also there should not be any errors
expect ( errors . length ) . toBe ( 0 )
await normalInitialPage . close ( )
} )
2024-08-12 21:16:04 +00:00
it ( 'groups routes' , async ( ) = > {
for ( const targetRoute of [ '/group-page' , '/nested-group/group-page' , '/nested-group' ] ) {
const { status } = await fetch ( targetRoute )
expect ( status ) . toBe ( 200 )
}
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/url' )
2023-05-13 21:09:37 +00:00
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' ,
2024-04-05 18:08:32 +00:00
'browser-set-to-null-with-default' : 'provided-by-browser' ,
} ) . map ( ( [ key , value ] ) = > ` ${ key } = ${ value } ` ) . join ( '; ' ) ,
} ,
2023-05-26 19:42:12 +00:00
} )
const cookies = res . headers . get ( 'set-cookie' )
2024-08-30 13:37:19 +00:00
expect ( cookies ) . toMatchInlineSnapshot ( '"set-in-plugin=true; Path=/, accessed-with-default-value=default; Path=/, set=set; Path=/, browser-set=set; Path=/, browser-set-to-null=; Max-Age=0; Path=/, browser-set-to-null-with-default=; Max-Age=0; Path=/, browser-object-default=%7B%22foo%22%3A%22bar%22%7D; Path=/, theCookie=show; Path=/"' )
2024-01-02 15:37:19 +00:00
} )
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' } )
2024-01-29 10:37:32 +00:00
await page . getByText ( 'Change cookie' ) . click ( )
2024-01-02 15:37:19 +00:00
expect ( await extractCookie ( ) ) . toEqual ( { foo : 'baz' } )
2024-02-03 23:15:26 +00:00
let text = await page . innerText ( 'pre' )
expect ( text ) . toContain ( 'baz' )
2024-01-29 10:37:32 +00:00
await page . getByText ( 'Change cookie' ) . click ( )
2024-01-17 11:53:14 +00:00
expect ( await extractCookie ( ) ) . toEqual ( { foo : 'bar' } )
2024-03-09 06:48:15 +00:00
await page . evaluate ( ( ) = > { document . cookie = ` browser-object-default= ${ encodeURIComponent ( '{"foo":"foobar"}' ) } ` } )
2024-01-29 10:37:32 +00:00
await page . getByText ( 'Refresh cookie' ) . click ( )
2024-02-03 23:15:26 +00:00
text = await page . innerText ( 'pre' )
2024-01-29 10:37:32 +00:00
expect ( text ) . toContain ( 'foobar' )
2024-01-02 15:37:19 +00:00
await page . close ( )
2023-05-26 19:42:12 +00:00
} )
2024-03-06 17:14:15 +00:00
2024-08-30 13:37:19 +00:00
it ( 'sets cookies in composable to null in all components' , async ( ) = > {
const { page } = await renderPage ( '/cookies' )
const parentBannerText = await page . locator ( '#parent-banner' ) . textContent ( )
expect ( parentBannerText ) . toContain ( 'parent banner' )
const childBannerText = await page . locator ( '#child-banner' ) . innerText ( )
expect ( childBannerText ) . toContain ( 'child banner' )
// Clear the composable cookie
await page . getByText ( 'Toggle cookie banner' ) . click ( )
await page . evaluate ( ( ) = > new Promise ( resolve = > setTimeout ( resolve , 10 ) ) )
const parentBannerAfterToggle = await page . locator ( '#parent-banner' ) . isVisible ( )
expect ( parentBannerAfterToggle ) . toBe ( false )
const childBannerAfterToggle = await page . locator ( '#child-banner' ) . isVisible ( )
expect ( childBannerAfterToggle ) . toBe ( false )
await page . close ( )
} )
2024-05-07 14:04:21 +00:00
it ( 'supports onPrehydrate' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/composables/on-prehydrate' ) as string
2024-05-07 14:04:21 +00:00
/ * *
* Should look something like this :
*
* ` ` ` html
* < div data - prehydrate - id = ":b3qlvSiBeH::df1mQEC9xH:" > onPrehydrate testing < / div >
* < script > ( ( ) = > { console . log ( window ) } ) ( ) < / script >
* < script > document . querySelectorAll ( '[data-prehydrate-id*=":b3qlvSiBeH:"]' ) . forEach ( o = > { console . log ( o . outerHTML ) } ) < / script >
* < script > document . querySelectorAll ( '[data-prehydrate-id*=":df1mQEC9xH:"]' ) . forEach ( o = > { console . log ( "other" , o . outerHTML ) } ) < / script >
* ` ` `
* /
const { id1 , id2 } = html . match ( /<div[^>]* data-prehydrate-id=":(?<id1>[^:]+)::(?<id2>[^:]+):"> onPrehydrate testing <\/div>/ ) ? . groups || { }
expect ( id1 ) . toBeTruthy ( )
const matches = [
2024-05-14 17:54:37 +00:00
html . match ( /<script[^>]*>\(\(\)=>\{console.log\(window\)\}\)\(\)<\/script>/ ) ,
2024-09-11 10:41:47 +00:00
html . match ( new RegExp ( ` <script[^>]*>document.querySelectorAll \\ (' \\ [data-prehydrate-id \\ *=": ${ id1 } :"]' \\ ).forEach \\ (o=>{console.log \\ (o.outerHTML \\ )} \\ )</script> ` , 'i' ) ) ,
html . match ( new RegExp ( ` <script[^>]*>document.querySelectorAll \\ (' \\ [data-prehydrate-id \\ *=": ${ id2 } :"]' \\ ).forEach \\ (o=>{console.log \\ ("other",o.outerHTML \\ )} \\ )</script> ` , 'i' ) ) ,
2024-05-07 14:04:21 +00:00
]
// This tests we inject all scripts correctly, and only have one occurrence of multiple calls of a composable
expect ( matches . every ( s = > s ? . length === 1 ) ) . toBeTruthy ( )
// Check for hydration/syntax errors on client side
await expectNoClientErrors ( '/composables/on-prehydrate' )
} )
2024-03-06 17:14:15 +00:00
it ( 'respects preview mode with a token' , async ( ) = > {
const token = 'hehe'
const page = await createPage ( ` /preview?preview=true&token= ${ token } ` )
const hasRerunFetchOnClient = await new Promise < boolean > ( ( resolve ) = > {
page . on ( 'console' , ( message ) = > {
setTimeout ( ( ) = > resolve ( false ) , 4000 )
if ( message . text ( ) === 'true' ) { resolve ( true ) }
} )
} )
expect ( hasRerunFetchOnClient ) . toBe ( true )
expect ( await page . locator ( '#fetched-on-client' ) . textContent ( ) ) . toContain ( 'fetched on client' )
expect ( await page . locator ( '#preview-mode' ) . textContent ( ) ) . toContain ( 'preview mode enabled' )
await page . click ( '#use-fetch-check' )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath . includes ( '/preview/with-use-fetch' ) )
expect ( await page . locator ( '#token-check' ) . textContent ( ) ) . toContain ( token )
expect ( await page . locator ( '#correct-api-key-check' ) . textContent ( ) ) . toContain ( 'true' )
await page . close ( )
} )
it ( 'respects preview mode with custom state' , async ( ) = > {
const { page } = await renderPage ( '/preview/with-custom-state?preview=true' )
expect ( await page . locator ( '#data1' ) . textContent ( ) ) . toContain ( 'data1 updated' )
expect ( await page . locator ( '#data2' ) . textContent ( ) ) . toContain ( 'data2' )
await page . click ( '#toggle-preview' ) // manually turns off preview mode
await page . click ( '#with-use-fetch' )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath . includes ( '/preview/with-use-fetch' ) )
expect ( await page . locator ( '#enabled' ) . textContent ( ) ) . toContain ( 'false' )
expect ( await page . locator ( '#token-check' ) . textContent ( ) ) . toEqual ( '' )
expect ( await page . locator ( '#correct-api-key-check' ) . textContent ( ) ) . toContain ( 'false' )
await page . close ( )
} )
it ( 'respects preview mode with custom enable' , async ( ) = > {
const { page } = await renderPage ( '/preview/with-custom-enable?preview=true' )
expect ( await page . locator ( '#enabled' ) . textContent ( ) ) . toContain ( 'false' )
await page . close ( )
} )
it ( 'respects preview mode with custom enable and customPreview' , async ( ) = > {
const { page } = await renderPage ( '/preview/with-custom-enable?customPreview=true' )
expect ( await page . locator ( '#enabled' ) . textContent ( ) ) . toContain ( 'true' )
await page . close ( )
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/json-payload' )
2023-04-07 10:34:35 +00:00
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' ,
2024-04-05 18:08:32 +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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/nuxt-link/trailing-slash' )
2023-03-07 07:17:42 +00:00
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' ) ) ) {
2024-06-27 14:27:08 +00:00
data [ selector ] ! . push ( match [ 1 ] ! )
2023-03-07 07:17:42 +00:00
}
}
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" ,
] ,
}
` )
} )
2024-06-13 15:39:37 +00:00
it ( 'respects external links in edge cases' , async ( ) = > {
const html = await $fetch < string > ( '/nuxt-link/custom-external' )
const hrefs = html . match ( /<a[^>]*href="([^"]+)"/g )
expect ( hrefs ) . toMatchInlineSnapshot ( `
[
"<a href=" https : //thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html"",
"<a href=" https : //thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html"",
"<a href=" / missing - page / "" ,
"<a href=" / missing - page / "" ,
]
` )
const { page , consoleLogs } = await renderPage ( '/nuxt-link/custom-external' )
const warnings = consoleLogs . filter ( c = > c . text . includes ( 'No match found for location' ) )
expect ( warnings ) . toMatchInlineSnapshot ( ` [] ` )
await page . close ( )
} )
2023-03-07 07:17:42 +00:00
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
2024-03-16 17:11:13 +00:00
it ( 'expect scroll to top on routes with same component' , async ( ) = > {
// #22402
const page = await createPage ( '/big-page-1' , {
viewport : {
width : 1000 ,
2024-04-05 18:08:32 +00:00
height : 1000 ,
} ,
2024-03-16 17:11:13 +00:00
} )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/big-page-1' )
await page . locator ( '#big-page-2' ) . scrollIntoViewIfNeeded ( )
await page . waitForFunction ( ( ) = > window . scrollY > 0 )
await page . locator ( '#big-page-2' ) . click ( )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/big-page-2' )
await page . waitForFunction ( ( ) = > window . scrollY === 0 )
await page . locator ( '#big-page-1' ) . scrollIntoViewIfNeeded ( )
await page . waitForFunction ( ( ) = > window . scrollY > 0 )
await page . locator ( '#big-page-1' ) . click ( )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/big-page-1' )
await page . waitForFunction ( ( ) = > window . scrollY === 0 )
await page . close ( )
} )
it ( 'expect scroll to top on nested pages' , async ( ) = > {
// #20523
const page = await createPage ( '/nested/foo/test' , {
viewport : {
width : 1000 ,
2024-04-05 18:08:32 +00:00
height : 1000 ,
} ,
2024-03-16 17:11:13 +00:00
} )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/nested/foo/test' )
await page . locator ( '#user-test' ) . scrollIntoViewIfNeeded ( )
2024-03-16 20:58:22 +00:00
await page . waitForFunction ( ( ) = > window . scrollY > 0 )
2024-03-16 17:11:13 +00:00
await page . locator ( '#user-test' ) . click ( )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/nested/foo/user-test' )
2024-03-16 20:58:22 +00:00
await page . waitForFunction ( ( ) = > window . scrollY === 0 )
2024-03-16 17:11:13 +00:00
await page . locator ( '#test' ) . scrollIntoViewIfNeeded ( )
2024-03-16 20:58:22 +00:00
await page . waitForFunction ( ( ) = > window . scrollY > 0 )
2024-03-16 17:11:13 +00:00
await page . locator ( '#test' ) . click ( )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/nested/foo/test' )
2024-03-16 20:58:22 +00:00
await page . waitForFunction ( ( ) = > window . scrollY === 0 )
2024-03-16 17:11:13 +00:00
await page . close ( )
} )
2024-04-19 09:48:49 +00:00
it ( 'useLink works' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/nuxt-link/use-link' )
2024-04-19 09:48:49 +00:00
expect ( html ) . toContain ( '<div>useLink in NuxtLink: true</div>' )
expect ( html ) . toContain ( '<div>route using useLink: /nuxt-link/trailing-slash</div>' )
expect ( html ) . toContain ( '<div>href using useLink: /nuxt-link/trailing-slash</div>' )
expect ( html ) . toContain ( '<div>useLink2 in NuxtLink: true</div>' )
expect ( html ) . toContain ( '<div>route2 using useLink: /nuxt-link/trailing-slash</div>' )
expect ( html ) . toContain ( '<div>href2 using useLink: /nuxt-link/trailing-slash</div>' )
expect ( html ) . toContain ( '<div>useLink3 in NuxtLink: true</div>' )
expect ( html ) . toContain ( '<div>route3 using useLink: /nuxt-link/trailing-slash</div>' )
expect ( html ) . toContain ( '<div>href3 using useLink: /nuxt-link/trailing-slash</div>' )
} )
it ( 'useLink navigate importing NuxtLink works' , async ( ) = > {
const page = await createPage ( '/nuxt-link/use-link' )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/nuxt-link/use-link' )
await page . locator ( '#button1' ) . click ( )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/nuxt-link/trailing-slash' )
await page . close ( )
} )
it ( 'useLink navigate using resolveComponent works' , async ( ) = > {
const page = await createPage ( '/nuxt-link/use-link' )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/nuxt-link/use-link' )
await page . locator ( '#button2' ) . click ( )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/nuxt-link/trailing-slash' )
await page . close ( )
} )
it ( 'useLink navigate using resolveDynamicComponent works' , async ( ) = > {
const page = await createPage ( '/nuxt-link/use-link' )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/nuxt-link/use-link' )
await page . locator ( '#button3' ) . click ( )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , '/nuxt-link/trailing-slash' )
await page . close ( )
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const headHtml = await $fetch < string > ( '/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">' )
2024-05-20 06:02:54 +00:00
expect ( headHtml . match ( 'meta charset' ) ! . length ) . toEqual ( 1 )
2022-08-07 09:53:53 +00:00
expect ( headHtml ) . toContain ( '<meta name="viewport" content="width=1024, initial-scale=1">' )
2024-05-20 06:02:54 +00:00
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
2024-05-20 06:02:54 +00:00
const indexHtml = await $fetch < string > ( '/' )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const headHtml = await $fetch < string > ( '/head-script-setup' )
2023-03-10 08:01:21 +00:00
// 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)">' )
} )
2024-06-19 15:02:35 +00:00
it ( 'SPA should render appHead tags' , async ( ) = > {
const headHtml = await $fetch < string > ( '/head-spa' )
2023-03-08 15:32:24 +00:00
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">' )
} )
2022-09-03 12:31:09 +00:00
it ( 'should render http-equiv correctly' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/head' )
2022-09-03 12:31:09 +00:00
// 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 () => {
2024-05-20 06:02:54 +00:00
// const html = await $fetch<string>('/head', { headers: { 'x-nuxt-no-ssr': '1' } })
2022-07-25 12:05:58 +00:00
// 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/legacy/async-data' )
2022-08-29 10:02:24 +00:00
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 )
2024-03-09 06:48:15 +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 )
} )
2024-05-20 20:02:46 +00:00
it ( 'expect to redirect with encoding' , async ( ) = > {
2024-06-26 09:58:45 +00:00
const { status , headers } = await fetch ( '/redirect-with-encode' , { redirect : 'manual' } )
2024-05-20 20:02:46 +00:00
expect ( status ) . toEqual ( 302 )
2024-06-26 09:58:45 +00:00
expect ( headers . get ( 'location' ) || '' ) . toEqual ( encodeURI ( '/cœur' ) + '?redirected=' + encodeURIComponent ( 'https://google.com' ) )
2024-05-20 20:02:46 +00:00
} )
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' )
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/instance/next-request' )
2023-05-17 12:26:16 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const requests = await Promise . all ( Array . from ( { length : 100 } ) . map ( ( ) = > $fetch < string > ( '/instance/next-request' ) ) )
2023-05-17 12:26:16 +00:00
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 : {
2024-04-05 18:08:32 +00:00
accept : 'application/json' ,
} ,
2022-09-08 08:45:39 +00:00
} )
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' ,
2024-04-05 18:08:32 +00:00
url : '/error' ,
2022-09-08 08:45:39 +00:00
} )
} )
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 : {
2024-04-05 18:08:32 +00:00
accept : 'application/json' ,
} ,
2023-04-25 14:47:02 +00:00
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const res = await $fetch < string > ( '/' , {
2023-12-11 18:20:11 +00:00
headers : {
'x-test-recurse-error' : 'true' ,
2024-04-05 18:08:32 +00:00
'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
2024-09-11 10:01:55 +00:00
it . runIf ( ! isDev ( ) ) ( 'should handle chunk loading errors' , async ( ) = > {
2024-09-13 11:40:56 +00:00
const { page , consoleLogs } = await renderPage ( )
await page . route ( /\.css/ , route = > route . abort ( 'timedout' ) ) // verify CSS link preload failure doesn't break the page
await page . goto ( url ( '/' ) )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/' && ! window . useNuxtApp ? . ( ) . isHydrating )
const initialLogs = consoleLogs . map ( c = > c . text ) . join ( '' )
expect ( initialLogs ) . toContain ( 'caught chunk load error' )
consoleLogs . length = 0
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' )
2024-09-11 10:01:55 +00:00
await page . route ( /.*/ , route = > route . abort ( 'timedout' ) , { times : 1 } )
2023-02-16 12:43:58 +00:00
await page . getByText ( 'Chunk error' ) . click ( )
2024-09-13 11:40:56 +00:00
2023-02-16 12:43:58 +00:00
await page . waitForURL ( url ( '/chunk-error' ) )
2024-09-13 11:40:56 +00:00
const logs = consoleLogs . map ( c = > c . text ) . join ( '' )
expect ( logs ) . toContain ( 'caught chunk load error' )
expect ( logs ) . toContain ( 'Failed to load resource' )
2023-08-12 07:18:58 +00:00
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/chunk-error' )
2024-09-13 11:40:56 +00:00
expect ( await page . innerText ( 'div' ) ) . toContain ( 'Chunk error page' )
2023-08-12 07:18:58 +00:00
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' , ( ) = > {
2024-01-30 09:10:13 +00:00
it ( '`callOnce` should run code once' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/once' )
2023-12-19 11:00:11 +00:00
expect ( html ) . toContain ( 'once.vue' )
expect ( html ) . toContain ( 'once: 2' )
const { page } = await renderPage ( '/once' )
expect ( await page . getByText ( 'once:' ) . textContent ( ) ) . toContain ( 'once: 2' )
} )
2024-01-30 09:10:13 +00:00
it ( '`useId` should generate unique ids' , async ( ) = > {
// TODO: work around interesting Vue bug where async components are loaded in a different order on first import
2024-05-20 06:02:54 +00:00
await $fetch < string > ( '/use-id' )
2024-01-30 09:10:13 +00:00
const sanitiseHTML = ( html : string ) = > html . replace ( / data-[^= ]+="[^"]+"/g , '' ) . replace ( /<!--[[\]]-->/ , '' )
2024-05-20 06:02:54 +00:00
const serverHTML = await $fetch < string > ( '/use-id' ) . then ( html = > sanitiseHTML ( html . match ( /<form.*<\/form>/ ) ! [ 0 ] ) )
2024-01-30 09:10:13 +00:00
const ids = serverHTML . match ( /id="[^"]*"/g ) ? . map ( id = > id . replace ( /id="([^"]*)"/ , '$1' ) ) as string [ ]
const renderedForm = [
` <h2 id=" ${ ids [ 0 ] } "> id: ${ ids [ 0 ] } </h2><div><label for=" ${ ids [ 1 ] } ">Email</label><input id=" ${ ids [ 1 ] } " name="email" type="email"><label for=" ${ ids [ 2 ] } ">Password</label><input id=" ${ ids [ 2 ] } " name="password" type="password"></div> ` ,
2024-04-05 18:08:32 +00:00
` <div><label for=" ${ ids [ 3 ] } ">Email</label><input id=" ${ ids [ 3 ] } " name="email" type="email"><label for=" ${ ids [ 4 ] } ">Password</label><input id=" ${ ids [ 4 ] } " name="password" type="password"></div> ` ,
2024-01-30 09:10:13 +00:00
]
const clientOnlyServer = '<span></span>'
expect ( serverHTML ) . toEqual ( ` <form> ${ renderedForm . join ( clientOnlyServer ) } </form> ` )
const { page , pageErrors } = await renderPage ( '/use-id' )
const clientHTML = await page . innerHTML ( 'form' )
const clientIds = clientHTML
. match ( /id="[^"]*"/g ) ? . map ( id = > id . replace ( /id="([^"]*)"/ , '$1' ) )
. filter ( i = > ! ids . includes ( i ) ) as string [ ]
const clientOnlyClient = ` <div><label for=" ${ clientIds [ 0 ] } ">Email</label><input id=" ${ clientIds [ 0 ] } " name="email" type="email"><label for=" ${ clientIds [ 1 ] } ">Password</label><input id=" ${ clientIds [ 1 ] } " name="password" type="password"></div> `
expect ( sanitiseHTML ( clientHTML ) ) . toEqual ( ` ${ renderedForm . join ( clientOnlyClient ) } ` )
expect ( pageErrors ) . toEqual ( [ ] )
await page . close ( )
} )
2024-04-17 15:58:13 +00:00
it ( '`useRouteAnnouncer` should change message on route change' , async ( ) = > {
const { page } = await renderPage ( '/route-announcer' )
expect ( await page . getByRole ( 'alert' ) . textContent ( ) ) . toContain ( 'First Page' )
await page . getByRole ( 'link' ) . click ( )
await page . getByText ( 'Second page content' ) . waitFor ( )
expect ( await page . getByRole ( 'alert' ) . textContent ( ) ) . toContain ( 'Second Page' )
await page . close ( )
} )
it ( '`useRouteAnnouncer` should change message on dynamically changed title' , async ( ) = > {
const { page } = await renderPage ( '/route-announcer' )
await page . getByRole ( 'button' ) . click ( )
await page . waitForFunction ( ( ) = > document . title . includes ( 'Dynamically set title' ) )
expect ( await page . getByRole ( 'alert' ) . textContent ( ) ) . toContain ( 'Dynamically set title' )
await page . close ( )
} )
2023-12-19 11:00:11 +00:00
} )
2022-03-22 18:12:54 +00:00
describe ( 'middlewares' , ( ) = > {
it ( 'should redirect to index with global middleware' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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 : {
2024-04-05 18:08:32 +00:00
accept : 'application/json' ,
} ,
2022-09-08 08:52:00 +00:00
} )
expect ( res . status ) . toEqual ( 401 )
} )
2023-05-25 18:29:22 +00:00
it ( 'should allow aborting navigation fatally on client-side' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/middleware-abort' )
2023-05-25 18:29:22 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/plugins' )
2022-04-01 09:55:23 +00:00
expect ( html ) . toContain ( 'myPlugin: Injected by my-plugin' )
} )
it ( 'async plugin' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/plugins' )
2022-04-01 09:55:23 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/with-dynamic-layout' )
2022-08-31 08:02:48 +00:00
// 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/with-computed-layout' )
2023-01-14 00:58:54 +00:00
// 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/layouts/with-props' )
2022-11-29 12:16:41 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/tree-shake' )
2023-03-07 09:30:05 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/ignore/composables' )
2023-08-24 12:42:15 +00:00
expect ( html ) . toContain ( 'was import ignored: true' )
} )
2023-08-25 12:08:38 +00:00
it ( 'should ignore scanned nitro handlers in .nuxtignore' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/ignore/scanned' )
2023-08-25 12:08:38 +00:00
expect ( html ) . not . toContain ( 'this should be ignored' )
} )
it . skipIf ( isDev ( ) ) ( 'should ignore public assets in .nuxtignore' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/ignore/public-asset' )
2023-08-25 12:08:38 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/client' )
2022-07-07 16:04:38 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/foo' )
2022-03-24 12:33:42 +00:00
expect ( html ) . toContain ( 'Extended component from foo' )
} )
it ( 'extends bar/components/ExtendsOverride over foo/components/ExtendsOverride' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/override' )
2022-03-24 12:33:42 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/foo' )
2023-04-03 13:18:24 +00:00
expect ( html ) . toContain ( 'from layer alias' )
} )
2022-03-22 18:12:54 +00:00
it ( 'extends foo/middleware/foo' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/foo' )
2022-03-24 12:33:42 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/override' )
2022-03-24 12:33:42 +00:00
expect ( html ) . toContain ( 'Middleware | override: Injected by extended middleware from bar' )
} )
2023-09-16 08:39:51 +00:00
it ( 'global middlewares sorting' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/middleware/ordering' )
2023-09-16 08:39:51 +00:00
expect ( html ) . toContain ( 'catchall at middleware' )
} )
2022-03-24 12:33:42 +00:00
} )
describe ( 'composables' , ( ) = > {
it ( 'extends foo/composables/foo' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/foo' )
2022-03-24 12:33:42 +00:00
expect ( html ) . toContain ( 'Composable | useExtendsFoo: foo' )
} )
2023-01-14 01:14:24 +00:00
it ( 'allows overriding composables' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/extends' )
2023-01-14 01:14:24 +00:00
expect ( html ) . toContain ( 'test from project' )
} )
2022-03-24 12:33:42 +00:00
} )
describe ( 'plugins' , ( ) = > {
it ( 'extends foo/plugins/foo' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/foo' )
2022-03-24 12:33:42 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/plugins/ordering' )
2023-09-04 22:41:51 +00:00
expect ( html ) . toContain ( 'catchall at plugins' )
} )
2022-03-24 12:33:42 +00:00
} )
describe ( 'server' , ( ) = > {
it ( 'extends foo/server/api/foo' , async ( ) = > {
2024-05-20 06:02:54 +00:00
expect ( await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html : string = await $fetch < string > ( '/' )
2024-06-27 14:27:08 +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
2024-06-25 06:48:39 +00:00
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) && ! window . useNuxtApp ? . ( ) . isHydrating )
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 ( ) = > {
2024-03-16 20:19:07 +00:00
const { page } = await renderPage ( )
await page . goto ( url ( '/hydration/spa-redirection/start' ) )
2023-08-12 07:18:58 +00:00
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/' ] ,
2024-04-05 18:08:32 +00:00
[ '/suspense/async-1/sync-1/' , '/suspense/async-2/async-1/' ] ,
2024-06-27 14:27:08 +00:00
] as const ) . flatMap ( ( [ start , end ] ) = > [
2023-07-05 10:39:39 +00:00
[ start , end ] ,
[ start , end + '?layout=custom' ] ,
2024-04-05 18:08:32 +00:00
[ start + '?layout=custom' , end ] ,
2023-07-05 10:39:39 +00:00
] )
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
. 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 } ] ` ,
2024-04-05 18:08:32 +00:00
. . . last . childType === 'async' ? [ ` [ ${ last . parentType } ] [ ${ last . parentNum } ] [async] [ ${ last . childNum } ] running async data ` ] : [ ] ,
2023-07-05 10:39:39 +00:00
] . sort ( ) )
await page . close ( )
} )
const outwardNavigations = [
[ '/suspense/async-2/async-1/' , '/suspense/async-1/' ] ,
2024-04-05 18:08:32 +00:00
[ '/suspense/async-2/sync-1/' , '/suspense/async-1/' ] ,
2023-07-05 10:39:39 +00:00
]
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 )
. 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 !
2024-06-25 06:48:39 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , nav )
2023-10-31 12:56:28 +00:00
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 } ] ` ,
2024-04-05 18:08:32 +00:00
. . . last . parentType === 'async' ? [ '[async] running async data' ] : [ ] ,
2023-07-05 10:39:39 +00:00
] . sort ( ) )
await page . close ( )
} )
const inwardNavigations = [
[ '/suspense/async-2/' , '/suspense/async-1/async-1/' ] ,
2024-04-05 18:08:32 +00:00
[ '/suspense/async-2/' , '/suspense/async-1/sync-1/' ] ,
2023-07-05 10:39:39 +00:00
]
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 )
. 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 } ] ` ,
2024-04-05 18:08:32 +00:00
. . . last . childType === 'async' ? [ ` [ ${ last . parentType } ] [ ${ last . parentNum } ] [async] [ ${ last . childNum } ] running async data ` ] : [ ] ,
2023-07-05 10:39:39 +00:00
] . 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
2024-06-25 06:48:39 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , ` ${ path } /1 ` )
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
2024-06-25 06:48:39 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , ` ${ path } /1 ` )
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' ,
2024-04-05 18:08:32 +00:00
'/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' )
2024-06-25 06:48:39 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , path2 )
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 ( '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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/keyed-composables' )
2022-07-07 16:26:04 +00:00
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' )
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/keyed-composables/local' )
2023-06-05 19:15:12 +00:00
expect ( html ) . toContain ( 'true' )
expect ( html ) . not . toContain ( 'false' )
} )
2022-07-07 16:26:04 +00:00
} )
2024-02-16 17:04:37 +00:00
describe . runIf ( isDev ( ) && ! isWebpack ) ( 'css links' , ( ) = > {
it ( 'should not inject links to CSS files that are inlined' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/inline-only-css' )
2024-02-16 17:04:37 +00:00
expect ( html ) . toContain ( '--inline-only' )
expect ( html ) . not . toContain ( 'inline-only.css' )
expect ( html ) . toContain ( 'assets/plugin.css' )
} )
} )
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>
2024-01-28 21:25:42 +00:00
'{--shared-component:"shared-component"}' , // styles in a chunk shared between pages
'{--server-only-child:"server-only-child"}' , // child of a server-only component
2024-04-05 18:08:32 +00:00
'{--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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/styles' )
2023-06-20 18:28:44 +00:00
for ( const style of inlinedCSS ) {
2024-01-28 21:25:42 +00:00
expect . soft ( html ) . toContain ( style )
2022-09-07 09:55:03 +00:00
}
} )
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
2024-04-05 18:08:32 +00:00
'{--global:"global";' , // global css from nuxt.config
2023-06-25 21:50:42 +00:00
]
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/route-rules/spa' )
2023-06-25 21:50:42 +00:00
for ( const style of globalCSS ) {
2024-01-28 21:25:42 +00:00
expect . soft ( html ) . toContain ( style )
2023-06-25 21:50:42 +00:00
}
} )
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 ( ( ) = > [ ] )
2024-02-14 10:56:21 +00:00
expect ( files . map ( m = > m . replace ( /\.[\w-]+(\.\w+)$/ , '$1' ) ) ) . toContain ( 'css-only-asset.svg' )
2023-06-26 12:11:12 +00:00
} )
2024-01-30 09:10:13 +00:00
it ( 'should not include inlined CSS in generated CSS file' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html : string = await $fetch < string > ( '/styles' )
2024-06-27 14:27:08 +00:00
const cssFiles = new Set ( [ . . . html . matchAll ( /<link [^>]*href="([^"]*\.css)">/g ) ] . map ( m = > m [ 1 ] ! ) )
2023-06-20 18:28:44 +00:00
let css = ''
for ( const file of cssFiles || [ ] ) {
2024-05-20 06:02:54 +00:00
css += await $fetch < string > ( file )
2023-06-20 18:28:44 +00:00
}
// 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html : string = await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/styles' )
2022-09-07 09:55:03 +00:00
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
2024-03-16 20:19:07 +00:00
await page . locator ( '.box' ) . getByText ( '"number": 101,' ) . waitFor ( )
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 ) ,
2024-04-05 18:08:32 +00:00
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' )
2024-02-05 10:36:20 +00:00
// test islands wrapped with client-only
expect ( await page . locator ( '#wrapped-client-only' ) . innerHTML ( ) ) . toContain ( 'Was router enabled' )
2024-03-06 15:26:19 +00:00
if ( ! isWebpack ) {
// test nested client components
await page . locator ( '.server-with-nested-client button' ) . click ( )
expect ( await page . locator ( '.server-with-nested-client .sugar-counter' ) . innerHTML ( ) ) . toContain ( 'Sugar Counter 13 x 1 = 13' )
}
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 ( ) = > {
2024-01-24 11:49:47 +00:00
const { page , consoleLogs } = await renderPage ( '/server-components/lazy/start' )
2023-12-19 12:21:29 +00:00
2023-07-31 08:51:09 +00:00
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' )
2024-01-24 11:49:47 +00:00
expect ( consoleLogs . filter ( l = > l . type === 'error' ) ) . toHaveLength ( 0 )
2023-12-19 12:21:29 +00:00
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-08-12 09:42:47 +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"><!--teleport start--><!--teleport end--></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"><!--teleport start--><!--teleport end--></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 below 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-08-12 09:42:47 +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"><!--teleport start--><!--teleport end--></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"><!--teleport start--><!--teleport end--></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 below 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 ( )
} )
2024-08-22 12:05:39 +00:00
it ( '/server-page' , async ( ) = > {
const html = await $fetch < string > ( '/server-page' )
// test island head
expect ( html ) . toContain ( '<meta name="author" content="Nuxt">' )
} )
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'
2024-04-05 18:08:32 +00:00
: '_payload.js' ,
2023-06-25 16:38:15 +00:00
)
} )
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/auth' )
2022-08-30 14:41:11 +00:00
// 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 ) )
2024-06-25 06:48:39 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , joinURL ( '/invalid-root' , path ) )
2023-02-13 22:09:32 +00:00
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' ) ,
2024-04-05 18:08:32 +00:00
true ,
2023-02-13 22:09:32 +00:00
)
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 ) )
2024-06-25 06:48:39 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , joinURL ( '/invalid-root' , path ) )
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
2024-03-14 00:18:44 +00:00
describe ( 'public directories' , ( ) = > {
it ( 'should directly return public directory paths' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/assets-custom' )
2024-03-14 00:18:44 +00:00
expect ( html ) . toContain ( '"/public.svg"' )
expect ( html ) . toContain ( '"/custom/file.svg"' )
} )
} )
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' , ( ) = > {
2024-05-23 14:34:06 +00:00
const publicFiles = [ '/public.svg' , '/css-only-public-asset.svg' ]
const isPublicFile = ( base = '/' , file : string ) = > {
if ( isWebpack ) {
// TODO: webpack does not yet support dynamic static paths
expect ( publicFiles ) . toContain ( file )
return true
}
expect ( file ) . toMatch ( new RegExp ( ` ^ ${ base . replace ( /\//g , '\\/' ) } ` ) )
expect ( publicFiles ) . toContain ( file . replace ( base , '/' ) )
return true
}
2022-03-23 14:57:35 +00:00
it ( 'should work with no overrides' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html : string = await $fetch < string > ( '/assets' )
2024-05-14 17:54:37 +00:00
for ( const match of html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*)\)/g ) ) {
2024-06-27 14:27:08 +00:00
const url = match [ 2 ] || match [ 3 ] !
2024-05-23 14:34:06 +00:00
expect ( url . startsWith ( '/_nuxt/' ) || isPublicFile ( '/' , url ) ) . toBeTruthy ( )
2022-03-23 14:57:35 +00:00
}
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html : string = await $fetch < string > ( '/assets' )
2024-05-14 17:54:37 +00:00
const urls = Array . from ( html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*)\)/g ) ) . map ( m = > m [ 2 ] || m [ 3 ] )
2024-06-27 14:27:08 +00:00
const cssURL = urls . find ( u = > /_nuxt\/assets.*\.css$/ . test ( u ! ) )
2022-04-07 19:15:30 +00:00
expect ( cssURL ) . toBeDefined ( )
2024-05-23 14:34:06 +00:00
const css = await $fetch < string > ( cssURL ! )
2024-06-27 14:27:08 +00:00
const imageUrls = new Set ( Array . from ( css . matchAll ( /url\(([^)]*)\)/g ) ) . map ( m = > m [ 1 ] ! . replace ( /[-.]\w{8}\./g , '.' ) ) )
2024-05-23 14:34:06 +00:00
expect ( [ . . . imageUrls ] ) . toMatchInlineSnapshot ( `
[
"./logo.svg" ,
"../public.svg" ,
]
` )
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/' ,
2024-04-05 18:08:32 +00:00
NUXT_APP_BASE_URL : '/foo/' ,
} ,
2024-01-17 17:58:23 +00:00
} )
2022-03-22 15:51:26 +00:00
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/foo/assets' )
2024-05-14 17:54:37 +00:00
for ( const match of html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*)\)/g ) ) {
2024-06-27 14:27:08 +00:00
const url = match [ 2 ] || match [ 3 ] !
2024-05-23 14:34:06 +00:00
expect ( url . startsWith ( '/foo/_other/' ) || isPublicFile ( '/foo/' , url ) ) . toBeTruthy ( )
2022-07-25 09:52:21 +00:00
}
2024-02-13 10:30:39 +00:00
2024-05-20 06:02:54 +00:00
expect ( await $fetch < string > ( '/foo/url' ) ) . toContain ( 'path: /foo/url' )
2022-07-25 09:52:21 +00:00
} )
it ( 'should allow setting relative baseURL' , async ( ) = > {
2024-01-17 17:58:23 +00:00
await startServer ( {
env : {
2024-04-05 18:08:32 +00:00
NUXT_APP_BASE_URL : './' ,
} ,
2024-01-17 17:58:23 +00:00
} )
2022-07-25 09:52:21 +00:00
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/assets' )
2024-05-14 17:54:37 +00:00
for ( const match of html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*)\)/g ) ) {
2024-06-27 14:27:08 +00:00
const url = match [ 2 ] || match [ 3 ] !
2024-05-23 14:34:06 +00:00
expect ( url . startsWith ( './_nuxt/' ) || isPublicFile ( './' , url ) ) . toBeTruthy ( )
2022-07-25 09:52:21 +00:00
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/' ,
2024-04-05 18:08:32 +00:00
NUXT_APP_BASE_URL : '/foo/' ,
} ,
2024-01-17 17:58:23 +00:00
} )
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/' ,
2024-04-05 18:08:32 +00:00
NUXT_APP_BUILD_ASSETS_DIR : '/_cdn/' ,
} ,
2024-01-17 17:58:23 +00:00
} )
2022-03-23 14:57:35 +00:00
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/foo/assets' )
2024-05-14 17:54:37 +00:00
for ( const match of html . matchAll ( /(href|src)="(.*?)"|url\(([^)]*)\)/g ) ) {
2024-06-27 14:27:08 +00:00
const url = match [ 2 ] || match [ 3 ] !
2024-05-23 14:34:06 +00:00
expect ( url . startsWith ( 'https://example.com/_cdn/' ) || isPublicFile ( 'https://example.com/' , url ) ) . 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/app-config' )
2022-08-17 15:23:13 +00:00
2023-09-19 21:31:18 +00:00
const expectedAppConfig : Record < string , any > = {
2022-08-17 15:23:13 +00:00
fromNuxtConfig : true ,
nested : {
2024-04-05 18:08:32 +00:00
val : 2 ,
2022-08-17 15:23:13 +00:00
} ,
2023-09-19 21:31:18 +00:00
nuxt : { } ,
2022-08-17 15:23:13 +00:00
fromLayer : true ,
2024-04-05 18:08:32 +00:00
userConfig : 123 ,
2022-08-17 15:23:13 +00:00
}
2024-05-17 03:41:31 +00:00
expect . soft ( html ) . toContain ( JSON . stringify ( expectedAppConfig ) )
2023-03-14 09:54:59 +00:00
2024-05-20 06:02:54 +00:00
const serverAppConfig = await $fetch < Record < string , any > > ( '/api/app-config' )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const result = await $fetch < NuxtIslandResponse > ( '/__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 ( ) ) {
2024-08-22 13:57:10 +00:00
result . head . link = result . head . link ? . filter ( l = > typeof l . href !== 'string' || ( ! l . href . includes ( '_nuxt/components/islands/RouteComponent' ) && ! l . href . includes ( 'PureComponent' ) /* TODO: fix dev bug triggered by previous fetch of /islands */ ) )
2022-11-24 12:24:14 +00:00
}
expect ( result ) . toMatchInlineSnapshot ( `
{
"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 > " ,
}
` )
} )
2023-03-20 21:47:06 +00:00
it ( 'render async component' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const result = await $fetch < NuxtIslandResponse > ( withQuery ( '/__nuxt_island/LongAsyncComponent.json' , {
2023-03-20 21:47:06 +00:00
props : JSON.stringify ( {
2024-04-05 18:08:32 +00:00
count : 3 ,
} ) ,
2023-03-20 21:47:06 +00:00
} ) )
if ( isDev ( ) ) {
2024-08-22 13:57:10 +00:00
result . head . link = result . head . link ? . filter ( l = > typeof l . href !== 'string' || ( ! l . href . includes ( '_nuxt/components/islands/LongAsyncComponent' ) && ! l . href . includes ( 'PureComponent' ) /* TODO: fix dev bug triggered by previous fetch of /islands */ ) )
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
{
"head" : {
"link" : [ ] ,
"style" : [ ] ,
} ,
2024-03-06 15:26:19 +00:00
"html" : "<div data-island-uid><div> count is above 2 </div><!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" default "><!--teleport start--><!--teleport end--></div><!--]--> that was very long ... <div id=" long - async - component - count ">3</div> <!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" test "><!--teleport start--><!--teleport end--></div><!--]--><p>hello world !!!</p><!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" hello "><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style=" display : contents ; " data-island-uid data-island-slot=" fallback "><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--></div>" ,
2024-01-16 13:22:50 +00:00
"slots" : {
"default" : {
"props" : [ ] ,
} ,
"fallback" : {
2024-08-08 10:04:22 +00:00
"fallback" : "<!--teleport start anchor--><!--[--><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-->" ,
2024-01-16 13:22:50 +00:00
"props" : [
{
"t" : "fall" ,
} ,
{
"t" : "back" ,
} ,
] ,
} ,
"hello" : {
2024-08-08 10:04:22 +00:00
"fallback" : "<!--teleport start anchor--><!--[--><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-->" ,
2024-01-16 13:22:50 +00:00
"props" : [
{
"t" : 0 ,
} ,
{
"t" : 1 ,
} ,
{
"t" : 2 ,
} ,
] ,
} ,
"test" : {
"props" : [
{
"count" : 3 ,
} ,
] ,
} ,
} ,
}
` )
2023-03-20 21:47:06 +00:00
} )
it ( 'render .server async component' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const result = await $fetch < NuxtIslandResponse > ( withQuery ( '/__nuxt_island/AsyncServerComponent.json' , {
2023-03-20 21:47:06 +00:00
props : JSON.stringify ( {
2024-04-05 18:08:32 +00:00
count : 2 ,
} ) ,
2023-03-20 21:47:06 +00:00
} ) )
if ( isDev ( ) ) {
2024-08-22 13:57:10 +00:00
result . head . link = result . head . link ? . filter ( l = > typeof l . href === 'string' && ! l . href . includes ( 'PureComponent' ) /* TODO: fix dev bug triggered by previous fetch of /islands */ && ( ! l . href . startsWith ( '_nuxt/components/islands/' ) || l . href . includes ( '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" : [ ] ,
} ,
2024-03-06 15:26:19 +00:00
"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 "><!--teleport start--><!--teleport end--></div><!--]--></div>" ,
2024-01-16 13:22:50 +00:00
"props" : { } ,
"slots" : { } ,
}
` )
2023-12-19 12:21:29 +00:00
} )
if ( ! isWebpack ) {
it ( 'render server component with selective client hydration' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const result = await $fetch < NuxtIslandResponse > ( '/__nuxt_island/ServerWithClient' )
2023-12-19 12:21:29 +00:00
if ( isDev ( ) ) {
2024-08-22 13:57:10 +00:00
result . head . link = result . head . link ? . filter ( l = > typeof l . href !== 'string' || ( ! l . href . includes ( '_nuxt/components/islands/LongAsyncComponent' ) && ! l . href . includes ( 'PureComponent' ) /* TODO: fix dev bug triggered by previous fetch of /islands */ ) )
if ( ! result . head . link ) {
delete result . head . link
}
2023-12-19 12:21:29 +00:00
}
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-08-12 09:42:47 +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 below 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
}
` )
expect ( teleportsEntries ) . toHaveLength ( 1 )
2024-06-27 14:27:08 +00:00
expect ( teleportsEntries [ 0 ] ! [ 0 ] . startsWith ( 'Counter-' ) ) . toBeTruthy ( )
expect ( teleportsEntries [ 0 ] ! [ 1 ] . props ) . toMatchInlineSnapshot ( `
2024-08-22 12:05:39 +00:00
{
"multiplier" : 1 ,
}
` )
2024-08-08 10:04:22 +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 ( ) = > {
2024-05-20 06:02:54 +00:00
const result = await $fetch < NuxtIslandResponse > ( withQuery ( '/__nuxt_island/PureComponent.json' , {
2022-11-24 12:24:14 +00:00
props : JSON.stringify ( {
bool : false ,
number : 3487 ,
str : 'something' ,
2024-04-05 18:08:32 +00:00
obj : { foo : 42 , bar : false , me : 'hi' } ,
} ) ,
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 , '' )
2022-11-24 12:24:14 +00:00
2023-02-13 22:09:32 +00:00
if ( isDev ( ) ) {
2022-12-08 15:16:22 +00:00
const fixtureDir = normalize ( fileURLToPath ( new URL ( './fixtures/basic' , import . meta . url ) ) )
2024-08-22 13:57:10 +00:00
for ( const key in result . head ) {
if ( key === 'link' ) {
result . head [ key ] = result . head [ key ] ? . map ( ( h ) = > {
if ( h . href ) {
h . href = resolveUnrefHeadInput ( h . href ) . replace ( fixtureDir , '/<rootDir>' ) . replaceAll ( '//' , '/' )
}
return h
} )
2024-08-22 12:05:39 +00:00
}
2022-12-08 15:16:22 +00:00
}
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}" ,
} ,
] ,
}
2023-06-20 18:28:44 +00:00
` )
2023-02-13 22:09:32 +00:00
} else if ( isDev ( ) && ! isWebpack ) {
2024-05-13 17:45:21 +00:00
// TODO: resolve dev bug triggered by earlier fetch of /vueuse-head page
// https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/runtime/nitro/renderer.ts#L139
2024-08-22 13:57:10 +00:00
result . head . link = result . head . link ? . filter ( l = > typeof l . href !== 'string' || ! l . href . includes ( 'SharedComponent' ) )
2024-08-22 12:05:39 +00:00
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
"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
} )
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 ) ,
2024-04-05 18:08:32 +00:00
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 : {
2024-04-05 18:08:32 +00:00
NUXT_APP_BASE_URL : '/foo/' ,
} ,
2024-01-17 17:58:23 +00:00
} )
2023-07-18 15:07:35 +00:00
const result = await fetch ( '/foo/islands' )
expect ( result . status ) . toBe ( 200 )
await startServer ( )
} )
2024-02-26 17:39:26 +00:00
it ( 'render island page' , async ( ) = > {
const { page } = await renderPage ( '/' )
const islandPageRequest = page . waitForRequest ( ( req ) = > {
return req . url ( ) . includes ( '/__nuxt_island/page:server-page' )
} )
await page . getByText ( 'to server page' ) . click ( )
await islandPageRequest
await page . locator ( '#server-page' ) . waitFor ( )
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
expect ( await $fetch < string > ( '/vite-plugin-without-path' ) ) . toBe ( 'vite-plugin without path' )
expect ( await $fetch < string > ( '/__nuxt-test' ) ) . toBe ( 'vite-plugin with __nuxt prefix' )
2023-01-16 16:04:16 +00:00
} )
it ( 'does not allow direct access to nuxt source folder' , async ( ) = > {
2024-05-20 06:02:54 +00:00
expect ( await $fetch < string > ( '/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 ( ) = > {
2024-05-20 06:02:54 +00:00
const payload = await $fetch < string > ( '/random/a/_payload.json' , { responseType : 'text' } )
2023-04-07 10:34:35 +00:00
const data = parsePayload ( payload )
expect ( typeof data . prerenderedAt ) . toEqual ( 'number' )
expect ( data . data ) . toMatchObject ( {
hey : {
baz : 'qux' ,
2024-04-05 18:08:32 +00:00
foo : 'bar' ,
2023-04-07 10:34:35 +00:00
} ,
2024-04-05 18:08:32 +00:00
rand_a : expect.arrayContaining ( [ expect . anything ( ) ] ) ,
2023-04-07 10:34:35 +00:00
} )
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
2024-03-06 11:55:06 +00:00
await page . waitForRequest ( request = > request . url ( ) . includes ( '/random/c/_payload.json' ) )
2022-09-10 13:57:16 +00:00
// We are not triggering API requests in the payload
2024-03-06 11:55:06 +00:00
expect ( requests ) . not . toContainEqual ( expect . stringContaining ( '/api/random' ) )
expect ( requests ) . not . toContainEqual ( 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' )
2024-03-06 11:55:06 +00:00
expect ( requests ) . not . toContainEqual ( 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
2024-03-06 11:55:06 +00:00
expect ( requests ) . toContainEqual ( expect . stringContaining ( '/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' )
2024-03-06 11:55:06 +00:00
expect ( requests ) . not . toContainEqual ( 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 ( ) = > {
2024-05-20 06:02:54 +00:00
const payload = await $fetch < string > ( '/prefetch/server-components/_payload.json' , { responseType : 'text' } )
2023-06-14 09:09:27 +00:00
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 ( ) = > {
2024-05-20 06:02:54 +00:00
expect ( await $fetch < string > ( '/async-context' ) ) . toContain ( '"hasApp": true' )
2023-08-07 22:57:35 +00:00
} )
} )
2024-01-18 09:59:59 +00:00
describe . skipIf ( process . env . TEST_CONTEXT === 'async' ) ( 'Async context' , ( ) = > {
it ( 'should be unavailable' , async ( ) = > {
2024-05-20 06:02:54 +00:00
expect ( await $fetch < string > ( '/async-context' ) ) . toContain ( '"hasApp": false' )
2024-01-18 09:59:59 +00:00
} )
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
await $fetch < string > ( '/useAsyncData/refresh' )
2022-08-30 10:34:09 +00:00
} )
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 ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/useAsyncData/status' )
2023-06-09 21:38:14 +00:00
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 ( ) = > {
2024-06-19 15:02:35 +00:00
const defaultValue = 'undefined'
2024-05-21 22:58:38 +00:00
2024-01-24 11:49:47 +00:00
const { page } = await renderPage ( '/useAsyncData/immediate-remove-unmounted' )
2024-05-21 22:58:38 +00:00
expect ( await page . locator ( '#immediate-data' ) . getByText ( defaultValue ) . textContent ( ) ) . toBe ( defaultValue )
2023-08-24 12:06:29 +00:00
await page . click ( '#execute-btn' )
2024-05-21 22:58:38 +00:00
expect ( await page . locator ( '#immediate-data' ) . getByText ( ',' ) . textContent ( ) ) . not . toContain ( defaultValue )
2023-08-24 12:06:29 +00:00
await page . click ( '#to-index' )
2024-01-24 11:49:47 +00:00
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/' )
2023-08-24 12:06:29 +00:00
await page . click ( '#to-immediate-remove-unmounted' )
2024-01-24 11:49:47 +00:00
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) . _route . fullPath === '/useAsyncData/immediate-remove-unmounted' )
2024-05-21 22:58:38 +00:00
expect ( await page . locator ( '#immediate-data' ) . getByText ( defaultValue ) . textContent ( ) ) . toBe ( defaultValue )
2023-08-24 12:06:29 +00:00
await page . click ( '#execute-btn' )
2024-05-21 22:58:38 +00:00
expect ( await page . locator ( '#immediate-data' ) . getByText ( ',' ) . textContent ( ) ) . not . toContain ( defaultValue )
2023-08-24 12:06:29 +00:00
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 } ` )
2024-01-24 11:49:47 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , ` /keepalive/ ${ pageName } ` )
2023-11-14 16:56:31 +00:00
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 } ` )
2024-01-24 11:49:47 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , ` /keepalive/ ${ pageName } ` )
2023-11-14 16:56:31 +00:00
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 } ` )
2024-01-24 11:49:47 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , ` /keepalive/ ${ pageName } ` )
2023-11-14 16:56:31 +00:00
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 } ` )
2024-01-24 11:49:47 +00:00
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , ` /keepalive/ ${ pageName } ` )
2023-11-14 16:56:31 +00:00
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' )
2024-01-24 11:49:47 +00:00
const slugs = [
'keepalive-in-nuxtpage' ,
'keepalive-in-nuxtpage-2' ,
'keepalive-in-nuxtpage' ,
'not-keepalive' ,
2024-04-05 18:08:32 +00:00
'keepalive-in-nuxtpage-2' ,
2024-01-24 11:49:47 +00:00
]
for ( const slug of slugs ) {
await page . click ( ` # ${ slug } ` )
await page . waitForFunction ( path = > window . useNuxtApp ? . ( ) . _route . fullPath === path , ` /keepalive/ ${ slug } ` )
}
2023-11-14 16:56:31 +00:00
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' ,
2024-04-05 18:08:32 +00:00
'not-keepalive: onUnmounted' ,
2023-11-14 16:56:31 +00:00
] )
await page . close ( )
} )
} )
2024-03-11 14:33:49 +00:00
describe ( 'teleports' , ( ) = > {
it ( 'should append teleports to body' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/teleport' )
2024-03-11 14:33:49 +00:00
// Teleport is prepended to body, before the __nuxt div
expect ( html ) . toContain ( '<div>Teleport</div><!--teleport anchor--><div id="__nuxt">' )
// Teleport start and end tag are rendered as expected
expect ( html ) . toContain ( '<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div>' )
} )
it ( 'should render teleports to app teleports element' , async ( ) = > {
2024-05-20 06:02:54 +00:00
const html = await $fetch < string > ( '/nuxt-teleport' )
2024-03-11 14:33:49 +00:00
// Teleport is appended to body, after the __nuxt div
2024-08-08 10:04:22 +00:00
expect ( html ) . toContain ( '<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div></div></div><span id="nuxt-teleport"><!--teleport start anchor--><div>Nuxt Teleport</div><!--teleport anchor--></span><script' )
2024-03-11 14:33:49 +00:00
} )
} )
2023-11-14 16:56:31 +00:00
2024-01-18 16:09:27 +00:00
describe ( 'Node.js compatibility for client-side' , ( ) = > {
it ( 'should work' , async ( ) = > {
2024-06-25 06:48:39 +00:00
const { page } = await renderPage ( '/experimental/node-compat' )
2024-03-16 20:38:29 +00:00
await page . locator ( 'body' ) . getByText ( 'Nuxt is Awesome!' ) . waitFor ( )
expect ( await page . innerHTML ( 'body' ) ) . toContain ( 'CWD: [available]' )
2024-01-18 16:09:27 +00:00
await page . close ( )
2024-06-25 06:48:39 +00:00
} , 40 _000 )
} )
2024-01-18 16:09:27 +00:00
2023-06-20 18:28:44 +00:00
function normaliseIslandResult ( result : NuxtIslandResponse ) {
2024-08-22 13:57:10 +00:00
if ( result . head . style ) {
for ( const style of result . head . style ) {
if ( typeof style !== 'string' ) {
if ( style . innerHTML ) {
style . innerHTML = ( style . innerHTML as string ) . replace ( /data-v-[a-z0-9]+/g , 'data-v-xxxxx' )
}
if ( style . key ) {
style . key = style . key . replace ( /-[a-z0-9]+$/i , '' )
2024-08-22 12:05:39 +00:00
}
}
2024-08-22 13:57:10 +00:00
}
2023-06-20 18:28:44 +00:00
}
2024-08-22 13:57:10 +00:00
return result
2023-06-20 18:28:44 +00:00
}
2024-01-19 21:43:19 +00:00
2024-01-20 20:43:11 +00:00
describe ( 'import components' , ( ) = > {
let html = ''
it . sequential ( 'fetch import-components page' , async ( ) = > {
2024-05-20 06:02:54 +00:00
html = await $fetch < string > ( '/import-components' )
2024-01-20 20:43:11 +00:00
} )
it ( 'load default component with mode all' , ( ) = > {
expect ( html ) . toContain ( 'default-comp-all' )
} )
it ( 'load default component with mode client' , ( ) = > {
expect ( html ) . toContain ( 'default-comp-client' )
} )
it ( 'load default component with mode server' , ( ) = > {
expect ( html ) . toContain ( 'default-comp-server' )
} )
it ( 'load named component with mode all' , ( ) = > {
expect ( html ) . toContain ( 'named-comp-all' )
} )
it ( 'load named component with mode client' , ( ) = > {
expect ( html ) . toContain ( 'named-comp-client' )
} )
it ( 'load named component with mode server' , ( ) = > {
expect ( html ) . toContain ( 'named-comp-server' )
} )
} )
2024-04-09 21:14:44 +00:00
describe ( 'lazy import components' , ( ) = > {
2024-01-19 21:43:19 +00:00
let html = ''
it . sequential ( 'fetch lazy-import-components page' , async ( ) = > {
2024-05-20 06:02:54 +00:00
html = await $fetch < string > ( '/lazy-import-components' )
2024-01-19 21:43:19 +00:00
} )
it ( 'lazy load named component with mode all' , ( ) = > {
expect ( html ) . toContain ( 'lazy-named-comp-all' )
} )
it ( 'lazy load named component with mode client' , ( ) = > {
expect ( html ) . toContain ( 'lazy-named-comp-client' )
} )
it ( 'lazy load named component with mode server' , ( ) = > {
expect ( html ) . toContain ( 'lazy-named-comp-server' )
} )
2024-04-08 15:02:03 +00:00
2024-04-08 15:56:25 +00:00
it ( 'lazy load delayed hydration comps at the right time' , async ( ) = > {
2024-06-09 21:20:49 +00:00
expect ( html ) . toContain ( 'This should be visible at first with network!' )
2024-04-08 15:52:27 +00:00
const { page } = await renderPage ( '/lazy-import-components' )
2024-08-29 08:58:48 +00:00
await page . waitForLoadState ( 'networkidle' )
2024-06-10 08:41:33 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This shouldn\'t be visible at first with network!' ) . all ( ) ) . toHaveLength ( 1 )
2024-06-09 20:43:41 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with viewport!' ) . all ( ) ) . toHaveLength ( 1 )
2024-06-16 07:20:51 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with events!' ) . all ( ) ) . toHaveLength ( 2 )
2024-08-29 21:04:39 +00:00
// The default value is immediately truthy, however, there is a hydration mismatch without the hack
2024-09-13 09:36:52 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with conditions!' ) . all ( ) ) . toHaveLength ( 1 )
2024-08-25 10:53:29 +00:00
const component = page . locator ( '#lazyevent' )
2024-06-15 05:25:03 +00:00
const rect = ( await component . boundingBox ( ) ) !
2024-08-20 11:51:44 +00:00
await page . mouse . move ( rect . x + rect . width / 2 , rect . y + rect . height / 2 )
2024-08-20 12:19:36 +00:00
await page . waitForLoadState ( 'networkidle' )
2024-06-14 22:06:47 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This shouldn\'t be visible at first with events!' ) . all ( ) ) . toHaveLength ( 1 )
2024-08-25 10:53:29 +00:00
await page . locator ( '#conditionbutton' ) . click ( )
await page . waitForLoadState ( 'networkidle' )
2024-08-29 21:04:39 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This shouldn\'t be visible at first with conditions!' ) . all ( ) ) . toHaveLength ( 2 )
2024-08-20 11:51:44 +00:00
await page . evaluate ( ( ) = > window . scrollTo ( 0 , document . body . scrollHeight ) )
2024-09-13 12:04:38 +00:00
await page . waitForTimeout ( 300 ) // Wait for the intersection observer to fire the callback
2024-09-13 10:01:03 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This shouldn\'t be visible at first with viewport!' ) . all ( ) ) . toHaveLength ( 2 )
2024-08-29 21:04:39 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This should always be visible!' ) . all ( ) ) . toHaveLength ( 1 )
2024-06-27 17:32:02 +00:00
await page . close ( )
2024-04-08 14:59:41 +00:00
} )
2024-06-16 07:02:27 +00:00
2024-06-16 16:51:37 +00:00
it ( 'respects custom delayed hydration triggers and overrides defaults' , async ( ) = > {
2024-06-16 07:02:27 +00:00
const { page } = await renderPage ( '/lazy-import-components' )
await page . waitForLoadState ( 'networkidle' )
2024-08-25 10:53:29 +00:00
const component = page . locator ( '#lazyevent2' )
2024-06-16 16:51:37 +00:00
const rect = ( await component . boundingBox ( ) ) !
await page . mouse . move ( rect . x + rect . width / 2 , rect . y + rect . height / 2 )
await page . waitForTimeout ( 500 )
await page . waitForLoadState ( 'networkidle' )
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with events!' ) . all ( ) ) . toHaveLength ( 2 )
2024-06-16 08:11:09 +00:00
await page . locator ( '#lazyevent2' ) . click ( )
2024-08-20 12:19:36 +00:00
await page . waitForLoadState ( 'networkidle' )
2024-06-16 07:02:27 +00:00
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with events!' ) . all ( ) ) . toHaveLength ( 1 )
expect ( await page . locator ( 'body' ) . getByText ( 'This shouldn\'t be visible at first with events!' ) . all ( ) ) . toHaveLength ( 1 )
2024-06-27 17:32:02 +00:00
await page . close ( )
2024-06-16 07:04:51 +00:00
} )
2024-08-29 08:58:48 +00:00
it ( 'does not delay hydration of components named after modifiers' , async ( ) = > {
const { page } = await renderPage ( '/lazy-import-components' )
expect ( await page . locator ( 'body' ) . getByText ( 'This fake lazy event should be visible!' ) . all ( ) ) . toHaveLength ( 1 )
expect ( await page . locator ( 'body' ) . getByText ( 'This fake lazy event shouldn\'t be visible!' ) . all ( ) ) . toHaveLength ( 0 )
2024-06-17 20:40:30 +00:00
} )
2024-09-14 11:01:50 +00:00
it ( 'handles time-based hydration correctly' , async ( ) = > {
const { page } = await renderPage ( '/lazy-import-components/time' )
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with time!' ) . all ( ) ) . toHaveLength ( 2 )
await page . waitForTimeout ( 500 )
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with time!' ) . all ( ) ) . toHaveLength ( 1 )
await page . waitForTimeout ( 1600 ) // Some room for falkiness and intermittent lag
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with time!' ) . all ( ) ) . toHaveLength ( 0 )
} )
it ( 'handles promise-based hydration correctly' , async ( ) = > {
const { page } = await renderPage ( '/lazy-import-components/promise' )
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with promise!' ) . all ( ) ) . toHaveLength ( 1 )
await page . waitForTimeout ( 2100 ) // Some room for falkiness and intermittent lag
expect ( await page . locator ( 'body' ) . getByText ( 'This should be visible at first with promise!' ) . all ( ) ) . toHaveLength ( 0 )
} )
2024-01-19 21:43:19 +00:00
} )
2024-05-03 10:27:38 +00:00
describe ( 'defineNuxtComponent watch duplicate' , ( ) = > {
it ( 'test after navigation duplicate' , async ( ) = > {
const { page } = await renderPage ( '/define-nuxt-component' )
await page . getByTestId ( 'define-nuxt-component-bar' ) . click ( )
await page . getByTestId ( 'define-nuxt-component-state' ) . click ( )
await page . getByTestId ( 'define-nuxt-component-foo' ) . click ( )
expect ( await page . getByTestId ( 'define-nuxt-component-state' ) . first ( ) . innerText ( ) ) . toBe ( '2' )
} )
} )
2024-05-08 12:32:45 +00:00
describe ( 'namespace access to useNuxtApp' , ( ) = > {
2024-05-17 03:41:31 +00:00
it ( 'should return the nuxt instance when used with correct appId' , async ( ) = > {
2024-05-08 12:32:45 +00:00
const { page , pageErrors } = await renderPage ( '/namespace-nuxt-app' )
expect ( pageErrors ) . toEqual ( [ ] )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) && ! window . useNuxtApp ? . ( ) . isHydrating )
2024-05-17 03:41:31 +00:00
// Defaulting to appId
2024-05-08 12:32:45 +00:00
await page . evaluate ( ( ) = > window . useNuxtApp ? . ( ) )
2024-05-17 03:41:31 +00:00
// Using correct configured appId
2024-05-08 12:32:45 +00:00
// @ts-expect-error not public API yet
await page . evaluate ( ( ) = > window . useNuxtApp ? . ( 'nuxt-app-basic' ) )
await page . close ( )
} )
2024-05-17 03:41:31 +00:00
it ( 'should throw an error when used with wrong appId' , async ( ) = > {
2024-05-08 12:32:45 +00:00
const { page , pageErrors } = await renderPage ( '/namespace-nuxt-app' )
expect ( pageErrors ) . toEqual ( [ ] )
await page . waitForFunction ( ( ) = > window . useNuxtApp ? . ( ) && ! window . useNuxtApp ? . ( ) . isHydrating )
let error : unknown
try {
2024-05-17 03:41:31 +00:00
// Using wrong/unknown appId
2024-05-08 12:32:45 +00:00
// @ts-expect-error not public API yet
await page . evaluate ( ( ) = > window . useNuxtApp ? . ( 'nuxt-app-unknown' ) )
} catch ( err ) {
error = err
}
expect ( error ) . toBeTruthy ( )
await page . close ( )
} )
} )