2023-07-02 08:59:16 +00:00
/// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
import { describe , expect , it , vi } from 'vitest'
2023-09-19 21:31:18 +00:00
import { defineEventHandler } from 'h3'
2024-08-08 09:36:11 +00:00
import { destr } from 'destr'
2023-09-19 21:31:18 +00:00
2023-12-11 18:20:11 +00:00
import { mountSuspended , registerEndpoint } from '@nuxt/test-utils/runtime'
2023-07-02 08:59:16 +00:00
2024-06-26 09:58:45 +00:00
import { hasProtocol } from 'ufo'
2023-07-02 08:59:16 +00:00
import * as composables from '#app/composables'
import { clearNuxtData , refreshNuxtData , useAsyncData , useNuxtData } from '#app/composables/asyncData'
import { clearError , createError , isNuxtError , showError , useError } from '#app/composables/error'
import { onNuxtReady } from '#app/composables/ready'
2024-10-08 16:16:31 +00:00
import { setResponseStatus , useRequestEvent , useRequestFetch , useRequestHeaders , useResponseHeader } from '#app/composables/ssr'
2023-07-02 08:59:16 +00:00
import { clearNuxtState , useState } from '#app/composables/state'
import { useRequestURL } from '#app/composables/url'
2023-09-19 21:31:18 +00:00
import { getAppManifest , getRouteRules } from '#app/composables/manifest'
2023-12-19 11:00:11 +00:00
import { callOnce } from '#app/composables/once'
2023-12-19 10:18:10 +00:00
import { useLoadingIndicator } from '#app/composables/loading-indicator'
2024-04-17 15:58:13 +00:00
import { useRouteAnnouncer } from '#app/composables/route-announcer'
2024-06-26 09:58:45 +00:00
import { encodeURL , resolveRouteObject } from '#app/composables/router'
2024-11-02 22:25:05 +00:00
import { useRuntimeHook } from '#app/composables/runtime-hook'
2023-07-02 08:59:16 +00:00
2024-03-09 06:48:15 +00:00
registerEndpoint ( '/api/test' , defineEventHandler ( event = > ( {
2023-11-15 19:40:55 +00:00
method : event.method ,
2024-04-05 18:08:32 +00:00
headers : Object.fromEntries ( event . headers . entries ( ) ) ,
2023-11-15 19:40:55 +00:00
} ) ) )
2023-09-19 21:31:18 +00:00
2023-11-14 12:44:39 +00:00
describe ( 'app config' , ( ) = > {
it ( 'can be updated' , ( ) = > {
const appConfig = useAppConfig ( )
2024-09-02 11:12:58 +00:00
expect ( appConfig ) . toStrictEqual ( { nuxt : { } } )
type UpdateAppConfig = Parameters < typeof updateAppConfig > [ 0 ]
const initConfig : UpdateAppConfig = {
2023-11-14 12:44:39 +00:00
new : 'value' ,
2024-04-05 18:08:32 +00:00
nuxt : { nested : 42 } ,
2024-09-02 11:12:58 +00:00
regExp : /foo/g ,
date : new Date ( 1111 , 11 , 11 ) ,
arr : [ 1 , 2 , 3 ] ,
}
updateAppConfig ( initConfig )
expect ( appConfig ) . toStrictEqual ( initConfig )
const newConfig : UpdateAppConfig = {
nuxt : { anotherNested : 24 } ,
regExp : /bar/g ,
date : new Date ( 2222 , 12 , 12 ) ,
arr : [ 4 , 5 ] ,
}
updateAppConfig ( newConfig )
expect ( appConfig ) . toStrictEqual ( {
. . . initConfig ,
. . . newConfig ,
nuxt : { . . . initConfig . nuxt , . . . newConfig . nuxt } ,
arr : [ 4 , 5 , 3 ] ,
2023-11-14 12:44:39 +00:00
} )
} )
} )
2023-07-02 08:59:16 +00:00
describe ( 'composables' , ( ) = > {
it ( 'are all tested' , ( ) = > {
const testedComposables : string [ ] = [
2024-06-12 16:46:17 +00:00
'useRouteAnnouncer' ,
2023-07-02 08:59:16 +00:00
'clearNuxtData' ,
'refreshNuxtData' ,
'useAsyncData' ,
'useNuxtData' ,
'createError' ,
'isNuxtError' ,
'clearError' ,
'showError' ,
'useError' ,
2023-09-19 21:31:18 +00:00
'getAppManifest' ,
2023-11-14 12:44:39 +00:00
'useHydration' ,
2023-09-19 21:31:18 +00:00
'getRouteRules' ,
2023-07-02 08:59:16 +00:00
'onNuxtReady' ,
2023-12-19 11:00:11 +00:00
'callOnce' ,
2023-07-02 08:59:16 +00:00
'setResponseStatus' ,
2023-09-28 10:54:22 +00:00
'prerenderRoutes' ,
2023-07-02 08:59:16 +00:00
'useRequestEvent' ,
'useRequestFetch' ,
2023-09-19 21:31:18 +00:00
'isPrerendered' ,
2023-07-02 08:59:16 +00:00
'useRequestHeaders' ,
2024-10-08 16:16:31 +00:00
'useResponseHeader' ,
2024-03-08 17:03:31 +00:00
'useCookie' ,
2023-07-02 08:59:16 +00:00
'clearNuxtState' ,
'useState' ,
2023-11-14 12:44:39 +00:00
'useRequestURL' ,
'useRoute' ,
'navigateTo' ,
'abortNavigation' ,
'setPageLayout' ,
2024-04-05 18:08:32 +00:00
'defineNuxtComponent' ,
2024-11-02 22:25:05 +00:00
'useRuntimeHook' ,
2023-07-02 08:59:16 +00:00
]
const skippedComposables : string [ ] = [
'addRouteMiddleware' ,
'defineNuxtRouteMiddleware' ,
'definePayloadReducer' ,
'definePayloadReviver' ,
'loadPayload' ,
'onBeforeRouteLeave' ,
'onBeforeRouteUpdate' ,
'prefetchComponents' ,
'preloadComponents' ,
'preloadPayload' ,
'preloadRouteComponents' ,
'reloadNuxtApp' ,
2024-02-05 20:25:11 +00:00
'refreshCookie' ,
2024-05-07 14:04:21 +00:00
'onPrehydrate' ,
2024-09-03 13:33:21 +00:00
'useId' ,
2023-07-02 08:59:16 +00:00
'useFetch' ,
'useHead' ,
'useLazyFetch' ,
'useLazyAsyncData' ,
'useRouter' ,
'useSeoMeta' ,
2024-03-06 17:14:15 +00:00
'useServerSeoMeta' ,
2024-04-05 18:08:32 +00:00
'usePreviewMode' ,
2023-07-02 08:59:16 +00:00
]
expect ( Object . keys ( composables ) . sort ( ) ) . toEqual ( [ . . . new Set ( [ . . . testedComposables , . . . skippedComposables ] ) ] . sort ( ) )
} )
} )
describe ( 'useAsyncData' , ( ) = > {
it ( 'should work at basic level' , async ( ) = > {
const res = useAsyncData ( ( ) = > Promise . resolve ( 'test' ) )
expect ( Object . keys ( res ) ) . toMatchInlineSnapshot ( `
[
"data" ,
"pending" ,
"error" ,
"status" ,
"execute" ,
"refresh" ,
2024-03-17 00:19:44 +00:00
"clear" ,
2023-07-02 08:59:16 +00:00
]
` )
expect ( res instanceof Promise ) . toBeTruthy ( )
2024-06-19 17:04:40 +00:00
expect ( res . data . value ) . toBe ( undefined )
2023-07-02 08:59:16 +00:00
await res
expect ( res . data . value ) . toBe ( 'test' )
} )
it ( 'should not execute with immediate: false' , async ( ) = > {
const immediate = await useAsyncData ( ( ) = > Promise . resolve ( 'test' ) )
expect ( immediate . data . value ) . toBe ( 'test' )
expect ( immediate . status . value ) . toBe ( 'success' )
expect ( immediate . pending . value ) . toBe ( false )
const nonimmediate = await useAsyncData ( ( ) = > Promise . resolve ( 'test' ) , { immediate : false } )
2024-06-19 17:04:40 +00:00
expect ( nonimmediate . data . value ) . toBe ( undefined )
2023-07-02 08:59:16 +00:00
expect ( nonimmediate . status . value ) . toBe ( 'idle' )
expect ( nonimmediate . pending . value ) . toBe ( true )
} )
it ( 'should capture errors' , async ( ) = > {
2023-11-08 13:28:52 +00:00
const { data , error , status , pending } = await useAsyncData ( 'error-test' , ( ) = > Promise . reject ( new Error ( 'test' ) ) , { default : ( ) = > 'default' } )
expect ( data . value ) . toMatchInlineSnapshot ( '"default"' )
2023-07-02 08:59:16 +00:00
expect ( error . value ) . toMatchInlineSnapshot ( '[Error: test]' )
expect ( status . value ) . toBe ( 'error' )
expect ( pending . value ) . toBe ( false )
2023-11-08 13:28:52 +00:00
expect ( useNuxtApp ( ) . payload . _errors [ 'error-test' ] ) . toMatchInlineSnapshot ( '[Error: test]' )
// TODO: fix the below
// const { data: syncedData, error: syncedError, status: syncedStatus, pending: syncedPending } = await useAsyncData('error-test', () => ({}), { immediate: false })
// expect(syncedData.value).toEqual(null)
// expect(syncedError.value).toEqual(error.value)
// expect(syncedStatus.value).toEqual('idle')
// expect(syncedPending.value).toEqual(true)
2023-07-02 08:59:16 +00:00
} )
2023-09-27 13:43:53 +00:00
// https://github.com/nuxt/nuxt/issues/23411
it ( 'should initialize with error set to null when immediate: false' , async ( ) = > {
2024-06-07 15:57:37 +00:00
const { error , execute } = useAsyncData ( ( ) = > Promise . resolve ( { } ) , { immediate : false } )
2024-06-19 17:04:40 +00:00
expect ( error . value ) . toBe ( undefined )
2023-09-27 13:43:53 +00:00
await execute ( )
2024-06-19 17:04:40 +00:00
expect ( error . value ) . toBe ( undefined )
2023-09-27 13:43:53 +00:00
} )
2023-07-02 08:59:16 +00:00
it ( 'should be accessible with useNuxtData' , async ( ) = > {
await useAsyncData ( 'key' , ( ) = > Promise . resolve ( 'test' ) )
const data = useNuxtData ( 'key' )
expect ( data . data . value ) . toMatchInlineSnapshot ( '"test"' )
clearNuxtData ( 'key' )
expect ( data . data . value ) . toBeUndefined ( )
expect ( useNuxtData ( 'key' ) . data . value ) . toBeUndefined ( )
} )
2023-10-16 19:20:02 +00:00
it ( 'should be usable _after_ a useNuxtData call' , async ( ) = > {
useNuxtApp ( ) . payload . data . call = null
const { data : cachedData } = useNuxtData ( 'call' )
expect ( cachedData . value ) . toMatchInlineSnapshot ( 'null' )
const { data } = await useAsyncData ( 'call' , ( ) = > Promise . resolve ( { resolved : true } ) , { server : false } )
expect ( cachedData . value ) . toMatchInlineSnapshot ( `
{
"resolved" : true ,
}
` )
expect ( data . value ) . toEqual ( cachedData . value )
clearNuxtData ( 'call' )
} )
2023-07-02 08:59:16 +00:00
it ( 'should be refreshable' , async ( ) = > {
await useAsyncData ( 'key' , ( ) = > Promise . resolve ( 'test' ) )
clearNuxtData ( 'key' )
const data = useNuxtData ( 'key' )
expect ( data . data . value ) . toBeUndefined ( )
await refreshNuxtData ( 'key' )
expect ( data . data . value ) . toMatchInlineSnapshot ( '"test"' )
} )
2023-10-16 19:54:39 +00:00
2024-03-17 00:19:44 +00:00
it ( 'should be clearable' , async ( ) = > {
const { data , error , pending , status , clear } = await useAsyncData ( ( ) = > Promise . resolve ( 'test' ) )
expect ( data . value ) . toBe ( 'test' )
clear ( )
expect ( data . value ) . toBeUndefined ( )
2024-06-19 17:04:40 +00:00
expect ( error . value ) . toBe ( undefined )
2024-03-17 00:19:44 +00:00
expect ( p ending . value ) . toBe ( false )
expect ( status . value ) . toBe ( 'idle' )
} )
2023-10-16 19:54:39 +00:00
it ( 'allows custom access to a cache' , async ( ) = > {
2024-06-07 15:57:37 +00:00
const { data } = await useAsyncData ( ( ) = > Promise . resolve ( { val : true } ) , { getCachedData : ( ) = > ( { val : false } ) } )
2023-10-16 19:54:39 +00:00
expect ( data . value ) . toMatchInlineSnapshot ( `
{
"val" : false ,
}
` )
} )
2023-11-08 13:28:52 +00:00
2024-08-13 19:24:32 +00:00
it ( 'should only call getCachedData once' , async ( ) = > {
const getCachedData = vi . fn ( ( ) = > ( { val : false } ) )
const { data } = await useAsyncData ( ( ) = > Promise . resolve ( { val : true } ) , { getCachedData } )
expect ( data . value ) . toMatchInlineSnapshot ( `
{
"val" : false ,
}
` )
expect ( getCachedData ) . toHaveBeenCalledTimes ( 1 )
} )
2023-11-08 13:28:52 +00:00
it ( 'should use default while pending' , async ( ) = > {
const promise = useAsyncData ( ( ) = > Promise . resolve ( 'test' ) , { default : ( ) = > 'default' } )
const { data , pending } = promise
expect ( pending . value ) . toBe ( true )
expect ( data . value ) . toMatchInlineSnapshot ( '"default"' )
await promise
expect ( data . value ) . toMatchInlineSnapshot ( '"test"' )
} )
it ( 'should use default after reject' , async ( ) = > {
const { data } = await useAsyncData ( ( ) = > Promise . reject ( new Error ( 'test' ) ) , { default : ( ) = > 'default' } )
expect ( data . value ) . toMatchInlineSnapshot ( '"default"' )
} )
2023-12-14 11:08:43 +00:00
2024-03-09 06:48:15 +00:00
it ( 'should execute the promise function once when dedupe option is "defer" for multiple calls' , ( ) = > {
2023-12-14 11:08:43 +00:00
const promiseFn = vi . fn ( ( ) = > Promise . resolve ( 'test' ) )
useAsyncData ( 'dedupedKey' , promiseFn , { dedupe : 'defer' } )
useAsyncData ( 'dedupedKey' , promiseFn , { dedupe : 'defer' } )
useAsyncData ( 'dedupedKey' , promiseFn , { dedupe : 'defer' } )
expect ( promiseFn ) . toHaveBeenCalledTimes ( 1 )
} )
2024-03-09 06:48:15 +00:00
it ( 'should execute the promise function multiple times when dedupe option is not specified for multiple calls' , ( ) = > {
2023-12-14 11:08:43 +00:00
const promiseFn = vi . fn ( ( ) = > Promise . resolve ( 'test' ) )
useAsyncData ( 'dedupedKey' , promiseFn )
useAsyncData ( 'dedupedKey' , promiseFn )
useAsyncData ( 'dedupedKey' , promiseFn )
expect ( promiseFn ) . toHaveBeenCalledTimes ( 3 )
} )
2024-03-09 06:48:15 +00:00
it ( 'should execute the promise function as per dedupe option when different dedupe options are used for multiple calls' , ( ) = > {
2023-12-14 11:08:43 +00:00
const promiseFn = vi . fn ( ( ) = > Promise . resolve ( 'test' ) )
useAsyncData ( 'dedupedKey' , promiseFn , { dedupe : 'defer' } )
useAsyncData ( 'dedupedKey' , promiseFn )
useAsyncData ( 'dedupedKey' , promiseFn , { dedupe : 'defer' } )
expect ( promiseFn ) . toHaveBeenCalledTimes ( 2 )
} )
2024-03-15 16:40:00 +00:00
it ( 'should be synced with useNuxtData' , async ( ) = > {
const { data : nuxtData } = useNuxtData ( 'nuxtdata-sync' )
const promise = useAsyncData ( 'nuxtdata-sync' , ( ) = > Promise . resolve ( 'test' ) , { default : ( ) = > 'default' } )
const { data : fetchData } = promise
expect ( fetchData . value ) . toMatchInlineSnapshot ( '"default"' )
nuxtData . value = 'before-fetch'
expect ( fetchData . value ) . toMatchInlineSnapshot ( '"before-fetch"' )
await promise
expect ( fetchData . value ) . toMatchInlineSnapshot ( '"test"' )
expect ( nuxtData . value ) . toMatchInlineSnapshot ( '"test"' )
nuxtData . value = 'new value'
expect ( fetchData . value ) . toMatchInlineSnapshot ( '"new value"' )
fetchData . value = 'another value'
expect ( nuxtData . value ) . toMatchInlineSnapshot ( '"another value"' )
} )
2023-07-02 08:59:16 +00:00
} )
2023-11-15 19:40:55 +00:00
describe ( 'useFetch' , ( ) = > {
it ( 'should match with/without computed values' , async ( ) = > {
const nuxtApp = useNuxtApp ( )
const getPayloadEntries = ( ) = > Object . keys ( nuxtApp . payload . data ) . length
const baseCount = getPayloadEntries ( )
await useFetch ( '/api/test' )
expect ( getPayloadEntries ( ) ) . toBe ( baseCount + 1 )
/* @ts-expect-error Overriding auto-key */
await useFetch ( '/api/test' , { method : 'POST' } , '' )
/* @ts-expect-error Overriding auto-key */
await useFetch ( '/api/test' , { method : ref ( 'POST' ) } , '' )
expect . soft ( getPayloadEntries ( ) ) . toBe ( baseCount + 2 )
/* @ts-expect-error Overriding auto-key */
2023-11-16 14:04:48 +00:00
await useFetch ( '/api/test' , { query : { id : '3' } } , '' )
2023-11-15 19:40:55 +00:00
/* @ts-expect-error Overriding auto-key */
2023-11-16 14:04:48 +00:00
await useFetch ( '/api/test' , { query : { id : ref ( '3' ) } } , '' )
2023-11-15 19:40:55 +00:00
/* @ts-expect-error Overriding auto-key */
2023-11-16 14:04:48 +00:00
await useFetch ( '/api/test' , { params : { id : '3' } } , '' )
2023-11-15 19:40:55 +00:00
/* @ts-expect-error Overriding auto-key */
2023-11-16 14:04:48 +00:00
await useFetch ( '/api/test' , { params : { id : ref ( '3' ) } } , '' )
2023-11-15 19:40:55 +00:00
expect . soft ( getPayloadEntries ( ) ) . toBe ( baseCount + 3 )
} )
2023-11-20 13:59:41 +00:00
it ( 'should timeout' , async ( ) = > {
const { status , error } = await useFetch (
2024-06-07 15:57:37 +00:00
// @ts-expect-error should resolve to a string
2023-11-20 13:59:41 +00:00
( ) = > new Promise ( resolve = > setTimeout ( resolve , 5000 ) ) ,
2024-04-05 18:08:32 +00:00
{ timeout : 1 } ,
2023-11-20 13:59:41 +00:00
)
await new Promise ( resolve = > setTimeout ( resolve , 2 ) )
expect ( status . value ) . toBe ( 'error' )
expect ( error . value ) . toMatchInlineSnapshot ( '[Error: [GET] "[object Promise]": <no response> The operation was aborted.]' )
} )
2023-11-15 19:40:55 +00:00
} )
2023-07-02 08:59:16 +00:00
describe ( 'errors' , ( ) = > {
it ( 'createError' , ( ) = > {
expect ( createError ( { statusCode : 404 } ) . toJSON ( ) ) . toMatchInlineSnapshot ( `
{
"message" : "" ,
"statusCode" : 404 ,
}
` )
expect ( createError ( 'Message' ) . toJSON ( ) ) . toMatchInlineSnapshot ( `
{
"message" : "Message" ,
"statusCode" : 500 ,
}
` )
} )
it ( 'isNuxtError' , ( ) = > {
const error = createError ( { statusCode : 404 } )
expect ( isNuxtError ( error ) ) . toBe ( true )
expect ( isNuxtError ( new Error ( 'test' ) ) ) . toBe ( false )
} )
it ( 'global nuxt errors' , ( ) = > {
2024-05-21 22:58:38 +00:00
const error = useError ( )
expect ( error . value ) . toBeUndefined ( )
2023-07-02 08:59:16 +00:00
showError ( 'new error' )
2024-05-21 22:58:38 +00:00
expect ( error . value ) . toMatchInlineSnapshot ( '[Error: new error]' )
2023-07-02 08:59:16 +00:00
clearError ( )
2024-06-19 17:04:40 +00:00
expect ( error . value ) . toBe ( undefined )
2023-07-02 08:59:16 +00:00
} )
} )
describe ( 'onNuxtReady' , ( ) = > {
2023-12-11 18:20:11 +00:00
it ( 'should call callback once nuxt is hydrated' , async ( ) = > {
2023-07-02 08:59:16 +00:00
const fn = vi . fn ( )
onNuxtReady ( fn )
2023-12-11 18:20:11 +00:00
await new Promise ( resolve = > setTimeout ( resolve , 1 ) )
2023-07-02 08:59:16 +00:00
expect ( fn ) . toHaveBeenCalled ( )
} )
} )
describe ( 'ssr composables' , ( ) = > {
it ( 'work on client' , ( ) = > {
// @ts-expect-error This should work for backward compatibility
expect ( setResponseStatus ( ) ) . toBeUndefined ( )
expect ( useRequestEvent ( ) ) . toBeUndefined ( )
expect ( useRequestFetch ( ) ) . toEqual ( $fetch )
expect ( useRequestHeaders ( ) ) . toEqual ( { } )
2023-09-28 10:54:22 +00:00
expect ( prerenderRoutes ( '/' ) ) . toBeUndefined ( )
2024-10-08 16:16:31 +00:00
expect ( useResponseHeader ( 'x-test' ) . value ) . toBeUndefined ( )
2023-07-02 08:59:16 +00:00
} )
} )
2023-11-14 12:44:39 +00:00
describe ( 'useHydration' , ( ) = > {
it ( 'should hydrate value from payload' , async ( ) = > {
let val : any
const nuxtApp = useNuxtApp ( )
useHydration ( 'key' , ( ) = > { } , ( fromPayload ) = > { val = fromPayload } )
await nuxtApp . hooks . callHook ( 'app:created' , nuxtApp . vueApp )
expect ( val ) . toMatchInlineSnapshot ( 'undefined' )
nuxtApp . payload . key = 'from payload'
await nuxtApp . hooks . callHook ( 'app:created' , nuxtApp . vueApp )
expect ( val ) . toMatchInlineSnapshot ( '"from payload"' )
} )
} )
2023-07-02 08:59:16 +00:00
describe ( 'useState' , ( ) = > {
it ( 'default' , ( ) = > {
expect ( useState ( ( ) = > 'default' ) . value ) . toBe ( 'default' )
} )
it ( 'registers state in payload' , ( ) = > {
useState ( 'key' , ( ) = > 'value' )
expect ( Object . entries ( useNuxtApp ( ) . payload . state ) ) . toContainEqual ( [ '$skey' , 'value' ] )
} )
2023-10-01 08:37:53 +00:00
} )
2023-07-02 08:59:16 +00:00
2023-10-01 08:37:53 +00:00
describe ( 'clearNuxtState' , ( ) = > {
it ( 'clears state in payload for single key' , ( ) = > {
const key = 'clearNuxtState-test'
const state = useState ( key , ( ) = > 'test' )
2023-07-02 08:59:16 +00:00
expect ( state . value ) . toBe ( 'test' )
2023-10-01 08:37:53 +00:00
clearNuxtState ( key )
expect ( state . value ) . toBeUndefined ( )
} )
it ( 'clears state in payload for array of keys' , ( ) = > {
const key1 = 'clearNuxtState-test'
const key2 = 'clearNuxtState-test2'
const state1 = useState ( key1 , ( ) = > 'test' )
const state2 = useState ( key2 , ( ) = > 'test' )
expect ( state1 . value ) . toBe ( 'test' )
expect ( state2 . value ) . toBe ( 'test' )
clearNuxtState ( [ key1 , 'other' ] )
expect ( state1 . value ) . toBeUndefined ( )
expect ( state2 . value ) . toBe ( 'test' )
clearNuxtState ( [ key1 , key2 ] )
expect ( state1 . value ) . toBeUndefined ( )
expect ( state2 . value ) . toBeUndefined ( )
} )
it ( 'clears state in payload for function' , ( ) = > {
const key = 'clearNuxtState-test'
const state = useState ( key , ( ) = > 'test' )
expect ( state . value ) . toBe ( 'test' )
clearNuxtState ( ( ) = > false )
expect ( state . value ) . toBe ( 'test' )
clearNuxtState ( k = > k === key )
expect ( state . value ) . toBeUndefined ( )
} )
it ( 'clears all state when no key is provided' , ( ) = > {
const state1 = useState ( 'clearNuxtState-test' , ( ) = > 'test' )
const state2 = useState ( 'clearNuxtState-test2' , ( ) = > 'test' )
expect ( state1 . value ) . toBe ( 'test' )
expect ( state2 . value ) . toBe ( 'test' )
2023-07-02 08:59:16 +00:00
clearNuxtState ( )
2023-10-01 08:37:53 +00:00
expect ( state1 . value ) . toBeUndefined ( )
expect ( state2 . value ) . toBeUndefined ( )
2023-07-02 08:59:16 +00:00
} )
} )
describe ( 'url' , ( ) = > {
it ( 'useRequestURL' , ( ) = > {
const url = useRequestURL ( )
expect ( url ) . toMatchInlineSnapshot ( '"http://localhost:3000/"' )
expect ( url . hostname ) . toMatchInlineSnapshot ( '"localhost"' )
expect ( url . port ) . toMatchInlineSnapshot ( '"3000"' )
expect ( url . protocol ) . toMatchInlineSnapshot ( '"http:"' )
} )
} )
2023-09-19 21:31:18 +00:00
2023-12-19 10:18:10 +00:00
describe ( 'loading state' , ( ) = > {
it ( 'expect loading state to be changed by hooks' , async ( ) = > {
2024-08-05 15:33:27 +00:00
vi . stubGlobal ( 'setTimeout' , vi . fn ( ( cb : ( ) = > void ) = > cb ( ) ) )
2023-12-19 10:18:10 +00:00
const nuxtApp = useNuxtApp ( )
const { isLoading } = useLoadingIndicator ( )
expect ( isLoading . value ) . toBeFalsy ( )
await nuxtApp . callHook ( 'page:loading:start' )
expect ( isLoading . value ) . toBeTruthy ( )
await nuxtApp . callHook ( 'page:loading:end' )
expect ( isLoading . value ) . toBeFalsy ( )
vi . mocked ( setTimeout ) . mockRestore ( )
} )
} )
2024-03-06 16:27:05 +00:00
describe ( 'loading state' , ( ) = > {
it ( 'expect loading state to be changed by force starting/stoping' , async ( ) = > {
2024-08-05 15:33:27 +00:00
vi . stubGlobal ( 'setTimeout' , vi . fn ( ( cb : ( ) = > void ) = > cb ( ) ) )
2024-03-06 16:27:05 +00:00
const nuxtApp = useNuxtApp ( )
const { isLoading , start , finish } = useLoadingIndicator ( )
expect ( isLoading . value ) . toBeFalsy ( )
await nuxtApp . callHook ( 'page:loading:start' )
expect ( isLoading . value ) . toBeTruthy ( )
start ( )
expect ( isLoading . value ) . toBeTruthy ( )
finish ( )
expect ( isLoading . value ) . toBeFalsy ( )
} )
} )
2024-05-16 14:23:18 +00:00
describe ( 'loading state' , ( ) = > {
it ( 'expect error from loading state to be changed by finish({ error: true })' , async ( ) = > {
2024-08-05 15:33:27 +00:00
vi . stubGlobal ( 'setTimeout' , vi . fn ( ( cb : ( ) = > void ) = > cb ( ) ) )
2024-05-16 14:23:18 +00:00
const nuxtApp = useNuxtApp ( )
const { error , start , finish } = useLoadingIndicator ( )
expect ( error . value ) . toBeFalsy ( )
await nuxtApp . callHook ( 'page:loading:start' )
start ( )
finish ( { error : true } )
expect ( error . value ) . toBeTruthy ( )
start ( )
expect ( error . value ) . toBeFalsy ( )
finish ( )
} )
} )
2023-09-30 09:58:55 +00:00
describe . skipIf ( process . env . TEST_MANIFEST === 'manifest-off' ) ( 'app manifests' , ( ) = > {
2023-09-19 21:31:18 +00:00
it ( 'getAppManifest' , async ( ) = > {
const manifest = await getAppManifest ( )
2024-06-07 15:57:37 +00:00
// @ts-expect-error timestamp is not optional
2023-09-19 21:31:18 +00:00
delete manifest . timestamp
expect ( manifest ) . toMatchInlineSnapshot ( `
{
2023-10-07 06:30:10 +00:00
"id" : "override" ,
2023-09-19 21:31:18 +00:00
"matcher" : {
"dynamic" : { } ,
"static" : {
"/" : null ,
"/pre" : null ,
2023-10-16 22:28:42 +00:00
"/pre/test" : {
"redirect" : true ,
} ,
2023-09-19 21:31:18 +00:00
} ,
"wildcard" : {
"/pre" : {
"prerender" : true ,
} ,
} ,
} ,
"prerendered" : [
"/specific-prerendered" ,
] ,
}
` )
} )
it ( 'getRouteRules' , async ( ) = > {
2023-10-16 22:28:42 +00:00
expect ( await getRouteRules ( '/' ) ) . toMatchInlineSnapshot ( '{}' )
expect ( await getRouteRules ( '/pre' ) ) . toMatchInlineSnapshot ( `
{
"prerender" : true ,
}
` )
expect ( await getRouteRules ( '/pre/test' ) ) . toMatchInlineSnapshot ( `
{
"prerender" : true ,
"redirect" : true ,
}
` )
2023-09-19 21:31:18 +00:00
} )
it ( 'isPrerendered' , async ( ) = > {
expect ( await isPrerendered ( '/specific-prerendered' ) ) . toBeTruthy ( )
2024-03-08 23:03:01 +00:00
expect ( await isPrerendered ( '/prerendered/test' ) ) . toBeFalsy ( )
2023-09-19 21:31:18 +00:00
expect ( await isPrerendered ( '/test' ) ) . toBeFalsy ( )
2023-10-16 22:28:42 +00:00
expect ( await isPrerendered ( '/pre/test' ) ) . toBeFalsy ( )
expect ( await isPrerendered ( '/pre/thing' ) ) . toBeTruthy ( )
2023-09-19 21:31:18 +00:00
} )
} )
2023-11-14 12:44:39 +00:00
2024-11-02 22:25:05 +00:00
describe ( 'useRuntimeHook' , ( ) = > {
it ( 'types work' , ( ) = > {
// @ts-expect-error should not allow unknown hooks
useRuntimeHook ( 'test' , ( ) = > { } )
useRuntimeHook ( 'app:beforeMount' , ( _app ) = > {
// @ts-expect-error argument should be typed
_app = 'test'
} )
} )
it ( 'should call hooks' , async ( ) = > {
const nuxtApp = useNuxtApp ( )
let called = 1
const wrapper = await mountSuspended ( defineNuxtComponent ( {
setup ( ) {
useRuntimeHook ( 'test-hook' as any , ( ) = > {
called ++
} )
} ,
render : ( ) = > h ( 'div' , 'hi there' ) ,
} ) )
expect ( called ) . toBe ( 1 )
await nuxtApp . callHook ( 'test-hook' as any )
expect ( called ) . toBe ( 2 )
wrapper . unmount ( )
await nuxtApp . callHook ( 'test-hook' as any )
expect ( called ) . toBe ( 2 )
} )
} )
2023-11-14 12:44:39 +00:00
describe ( 'routing utilities: `navigateTo`' , ( ) = > {
it ( 'navigateTo should disallow navigation to external URLs by default' , ( ) = > {
2024-03-09 06:48:15 +00:00
expect ( ( ) = > navigateTo ( 'https://test.com' ) ) . toThrowErrorMatchingInlineSnapshot ( '[Error: Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.]' )
2023-11-14 12:44:39 +00:00
expect ( ( ) = > navigateTo ( 'https://test.com' , { external : true } ) ) . not . toThrow ( )
} )
it ( 'navigateTo should disallow navigation to data/script URLs' , ( ) = > {
const urls = [
[ 'data:alert("hi")' , 'data' ] ,
2024-04-05 18:08:32 +00:00
[ '\0data:alert("hi")' , 'data' ] ,
2023-11-14 12:44:39 +00:00
]
for ( const [ url , protocol ] of urls ) {
expect ( ( ) = > navigateTo ( url , { external : true } ) ) . toThrowError ( ` Cannot navigate to a URL with ' ${ protocol } :' protocol. ` )
}
} )
} )
2024-06-26 09:58:45 +00:00
describe ( 'routing utilities: `resolveRouteObject`' , ( ) = > {
it ( 'resolveRouteObject should correctly resolve a route object' , ( ) = > {
expect ( resolveRouteObject ( { path : '/test' } ) ) . toMatchInlineSnapshot ( ` "/test" ` )
expect ( resolveRouteObject ( { path : '/test' , hash : '#thing' , query : { foo : 'bar' } } ) ) . toMatchInlineSnapshot ( ` "/test?foo=bar#thing" ` )
} )
} )
describe ( 'routing utilities: `encodeURL`' , ( ) = > {
const encode = ( url : string ) = > {
const isExternal = hasProtocol ( url , { acceptRelative : true } )
return encodeURL ( url , isExternal )
}
it ( 'encodeURL should correctly encode a URL' , ( ) = > {
expect ( encode ( 'https://test.com' ) ) . toMatchInlineSnapshot ( ` "https://test.com/" ` )
expect ( encode ( '//test.com' ) ) . toMatchInlineSnapshot ( ` "//test.com/" ` )
expect ( encode ( 'mailto:daniel@cœur.com' ) ) . toMatchInlineSnapshot ( ` "mailto:daniel@c%C5%93ur.com" ` )
const encoded = encode ( '/cœur?redirected=' + encodeURIComponent ( 'https://google.com' ) )
expect ( new URL ( '/cœur' , 'http://localhost' ) . pathname ) . toMatchInlineSnapshot ( ` "/c%C5%93ur" ` )
expect ( encoded ) . toMatchInlineSnapshot ( ` "/c%C5%93ur?redirected=https%3A%2F%2Fgoogle.com" ` )
expect ( useRouter ( ) . resolve ( encoded ) . query . redirected ) . toMatchInlineSnapshot ( ` "https://google.com" ` )
} )
} )
2023-11-14 12:44:39 +00:00
describe ( 'routing utilities: `useRoute`' , ( ) = > {
2024-03-09 06:48:15 +00:00
it ( 'should show provide a mock route' , ( ) = > {
2023-11-14 12:44:39 +00:00
expect ( useRoute ( ) ) . toMatchObject ( {
fullPath : '/' ,
hash : '' ,
href : '/' ,
matched : [ ] ,
meta : { } ,
name : undefined ,
params : { } ,
path : '/' ,
query : { } ,
2024-04-05 18:08:32 +00:00
redirectedFrom : undefined ,
2023-11-14 12:44:39 +00:00
} )
} )
} )
describe ( 'routing utilities: `abortNavigation`' , ( ) = > {
it ( 'should throw an error if one is provided' , ( ) = > {
const error = useError ( )
2024-03-09 06:48:15 +00:00
expect ( ( ) = > abortNavigation ( { message : 'Page not found' } ) ) . toThrowErrorMatchingInlineSnapshot ( '[Error: Page not found]' )
2024-06-19 17:04:40 +00:00
expect ( error . value ) . toBe ( undefined )
2023-11-14 12:44:39 +00:00
} )
it ( 'should block navigation if no error is provided' , ( ) = > {
expect ( abortNavigation ( ) ) . toMatchInlineSnapshot ( 'false' )
} )
} )
describe ( 'routing utilities: `setPageLayout`' , ( ) = > {
2024-03-16 18:06:29 +00:00
it ( 'should set layout on page metadata if run outside middleware' , ( ) = > {
2023-11-14 12:44:39 +00:00
const route = useRoute ( )
expect ( route . meta . layout ) . toBeUndefined ( )
setPageLayout ( 'custom' )
expect ( route . meta . layout ) . toEqual ( 'custom' )
route . meta . layout = undefined
} )
2024-03-09 06:48:15 +00:00
it ( 'should not set layout directly if run within middleware' , ( ) = > {
2023-11-14 12:44:39 +00:00
const route = useRoute ( )
const nuxtApp = useNuxtApp ( )
nuxtApp . _processingMiddleware = true
setPageLayout ( 'custom' )
expect ( route . meta . layout ) . toBeUndefined ( )
nuxtApp . _processingMiddleware = false
} )
} )
describe ( 'defineNuxtComponent' , ( ) = > {
it ( 'should produce a Vue component' , async ( ) = > {
const wrapper = await mountSuspended ( defineNuxtComponent ( {
2024-04-05 18:08:32 +00:00
render : ( ) = > h ( 'div' , 'hi there' ) ,
2023-11-14 12:44:39 +00:00
} ) )
expect ( wrapper . html ( ) ) . toMatchInlineSnapshot ( '"<div>hi there</div>"' )
} )
it . todo ( 'should support Options API asyncData' )
it . todo ( 'should support Options API head' )
} )
2023-12-19 11:00:11 +00:00
2024-03-08 17:03:31 +00:00
describe ( 'useCookie' , ( ) = > {
it ( 'should watch custom cookie refs' , ( ) = > {
const user = useCookie ( 'userInfo' , {
default : ( ) = > ( { score : - 1 } ) ,
2024-04-05 18:08:32 +00:00
maxAge : 60 * 60 ,
2024-03-08 17:03:31 +00:00
} )
const computedVal = computed ( ( ) = > user . value . score )
expect ( computedVal . value ) . toBe ( - 1 )
user . value . score ++
expect ( computedVal . value ) . toBe ( 0 )
} )
2024-08-08 09:36:11 +00:00
it ( 'cookie decode function should be invoked once' , ( ) = > {
// Pre-set cookies
document . cookie = 'foo=Foo'
document . cookie = 'bar=%7B%22s2%22%3A0%7D'
document . cookie = 'baz=%7B%22s2%22%3A0%7D'
let barCallCount = 0
const bazCookie = useCookie < { s2 : number } > ( 'baz' , {
default : ( ) = > ( { s2 : - 1 } ) ,
decode ( value ) {
barCallCount ++
return destr ( decodeURIComponent ( value ) )
} ,
} )
bazCookie . value . s2 ++
expect ( bazCookie . value . s2 ) . toEqual ( 1 )
expect ( barCallCount ) . toBe ( 1 )
let quxCallCount = 0
const quxCookie = useCookie < { s3 : number } > ( 'qux' , {
default : ( ) = > ( { s3 : - 1 } ) ,
filter : key = > key === 'bar' || key === 'baz' ,
decode ( value ) {
quxCallCount ++
return destr ( decodeURIComponent ( value ) )
} ,
} )
quxCookie . value . s3 ++
expect ( quxCookie . value . s3 ) . toBe ( 0 )
expect ( quxCallCount ) . toBe ( 2 )
} )
2024-03-08 17:03:31 +00:00
it ( 'should not watch custom cookie refs when shallow' , ( ) = > {
for ( const value of [ 'shallow' , false ] as const ) {
const user = useCookie ( 'shallowUserInfo' , {
default : ( ) = > ( { score : - 1 } ) ,
maxAge : 60 * 60 ,
2024-04-05 18:08:32 +00:00
watch : value ,
2024-03-08 17:03:31 +00:00
} )
const computedVal = computed ( ( ) = > user . value . score )
expect ( computedVal . value ) . toBe ( - 1 )
user . value . score ++
expect ( computedVal . value ) . toBe ( - 1 )
}
} )
} )
2023-12-19 11:00:11 +00:00
describe ( 'callOnce' , ( ) = > {
it ( 'should only call composable once' , async ( ) = > {
const fn = vi . fn ( )
const execute = ( ) = > callOnce ( fn )
await execute ( )
await execute ( )
expect ( fn ) . toHaveBeenCalledTimes ( 1 )
} )
it ( 'should only call composable once when called in parallel' , async ( ) = > {
const fn = vi . fn ( ) . mockImplementation ( ( ) = > new Promise ( resolve = > setTimeout ( resolve , 1 ) ) )
const execute = ( ) = > callOnce ( fn )
await Promise . all ( [ execute ( ) , execute ( ) , execute ( ) ] )
expect ( fn ) . toHaveBeenCalledTimes ( 1 )
2024-02-05 20:25:11 +00:00
2024-03-08 17:03:31 +00:00
const fnSync = vi . fn ( ) . mockImplementation ( ( ) = > { } )
2024-01-25 14:29:50 +00:00
const executeSync = ( ) = > callOnce ( fnSync )
await Promise . all ( [ executeSync ( ) , executeSync ( ) , executeSync ( ) ] )
expect ( fnSync ) . toHaveBeenCalledTimes ( 1 )
2023-12-19 11:00:11 +00:00
} )
it ( 'should use key to dedupe' , async ( ) = > {
const fn = vi . fn ( )
const execute = ( key? : string ) = > callOnce ( key , fn )
await execute ( 'first' )
await execute ( 'first' )
await execute ( 'second' )
expect ( fn ) . toHaveBeenCalledTimes ( 2 )
} )
} )
2024-04-17 15:58:13 +00:00
describe ( 'route announcer' , ( ) = > {
it ( 'should create a route announcer with default politeness' , ( ) = > {
const announcer = useRouteAnnouncer ( )
expect ( announcer . politeness . value ) . toBe ( 'polite' )
} )
it ( 'should create a route announcer with provided politeness' , ( ) = > {
const announcer = useRouteAnnouncer ( { politeness : 'assertive' } )
expect ( announcer . politeness . value ) . toBe ( 'assertive' )
} )
it ( 'should set message and politeness' , ( ) = > {
const announcer = useRouteAnnouncer ( )
announcer . set ( 'Test message with politeness' , 'assertive' )
expect ( announcer . message . value ) . toBe ( 'Test message with politeness' )
expect ( announcer . politeness . value ) . toBe ( 'assertive' )
} )
it ( 'should set message with polite politeness' , ( ) = > {
const announcer = useRouteAnnouncer ( )
announcer . polite ( 'Test message polite' )
expect ( announcer . message . value ) . toBe ( 'Test message polite' )
expect ( announcer . politeness . value ) . toBe ( 'polite' )
} )
it ( 'should set message with assertive politeness' , ( ) = > {
const announcer = useRouteAnnouncer ( )
announcer . assertive ( 'Test message assertive' )
expect ( announcer . message . value ) . toBe ( 'Test message assertive' )
expect ( announcer . politeness . value ) . toBe ( 'assertive' )
} )
} )