test(nuxt): add additional unit tests for composables (#24289)

This commit is contained in:
Daniel Roe 2023-11-14 13:44:39 +01:00 committed by GitHub
parent 86693c1005
commit b301b639e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 250 additions and 19 deletions

View File

@ -238,6 +238,11 @@ jobs:
TEST_CONTEXT: ${{ matrix.context }}
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || runner.os == 'Windows' }}
- uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
with:
token: ${{ secrets.CODECOV_TOKEN }}
build-release:
permissions:
id-token: write

View File

@ -24,9 +24,9 @@
"test:fixtures": "nuxi prepare test/fixtures/basic && nuxi prepare test/fixtures/runtime-compiler && vitest run --dir test",
"test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures",
"test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures",
"test:runtime": "vitest -c vitest.nuxt.config.ts",
"test:runtime": "vitest -c vitest.nuxt.config.ts --coverage",
"test:types": "pnpm --filter './test/fixtures/**' test:types",
"test:unit": "vitest run --dir packages",
"test:unit": "vitest run --dir packages --coverage",
"typecheck": "tsc --noEmit"
},
"resolutions": {
@ -46,6 +46,7 @@
"@types/fs-extra": "11.0.4",
"@types/node": "20.9.0",
"@types/semver": "7.5.5",
"@vitest/coverage-v8": "^0.34.6",
"case-police": "0.6.1",
"changelogen": "0.5.5",
"consola": "3.2.3",

View File

@ -36,6 +36,9 @@ importers:
'@types/semver':
specifier: 7.5.5
version: 7.5.5
'@vitest/coverage-v8':
specifier: ^0.34.6
version: 0.34.6(vitest@0.33.0)
case-police:
specifier: 0.6.1
version: 0.6.1
@ -1210,6 +1213,10 @@ packages:
to-fast-properties: 2.0.0
dev: false
/@bcoe/v8-coverage@0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
/@cloudflare/kv-asset-handler@0.3.0:
resolution: {integrity: sha512-9CB/MKf/wdvbfkUdfrj+OkEwZ5b7rws0eogJ4293h+7b6KX5toPwym+VQKmILafNB9YiehqY0DlNrDcDhdWHSQ==}
dependencies:
@ -1649,6 +1656,11 @@ packages:
wrap-ansi: 8.1.0
wrap-ansi-cjs: /wrap-ansi@7.0.0
/@istanbuljs/schema@0.1.3:
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
dev: true
/@jest/schemas@29.6.3:
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -2415,7 +2427,6 @@ packages:
/@types/istanbul-lib-coverage@2.0.5:
resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==}
dev: false
/@types/istanbul-lib-report@3.0.2:
resolution: {integrity: sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==}
@ -2785,9 +2796,30 @@ packages:
vite: 4.5.0
vue: 3.3.8
dependencies:
vite: 4.5.0(@types/node@20.8.10)
vite: 4.5.0(@types/node@20.9.0)
vue: 3.3.8(typescript@5.2.2)
/@vitest/coverage-v8@0.34.6(vitest@0.33.0):
resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==}
peerDependencies:
vitest: '>=0.32.0 <1'
dependencies:
'@ampproject/remapping': 2.2.1
'@bcoe/v8-coverage': 0.2.3
istanbul-lib-coverage: 3.2.1
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 4.0.1
istanbul-reports: 3.1.6
magic-string: 0.30.5
picocolors: 1.0.0
std-env: 3.4.3
test-exclude: 6.0.0
v8-to-istanbul: 9.1.3
vitest: 0.33.0(happy-dom@12.10.3)
transitivePeerDependencies:
- supports-color
dev: true
/@vitest/expect@0.33.0:
resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==}
dependencies:
@ -3512,7 +3544,7 @@ packages:
resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==}
engines: {node: '>= 10.0.0'}
dependencies:
'@babel/types': 7.23.0
'@babel/types': 7.23.3
dev: false
/balanced-match@1.0.2:
@ -3918,8 +3950,8 @@ packages:
/constantinople@4.0.1:
resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
dependencies:
'@babel/parser': 7.23.0
'@babel/types': 7.23.0
'@babel/parser': 7.23.3
'@babel/types': 7.23.3
dev: false
/convert-gitmoji@0.1.3:
@ -5446,6 +5478,10 @@ packages:
resolution: {integrity: sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==}
dev: false
/html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
dev: true
/html-tags@3.3.1:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'}
@ -5857,6 +5893,39 @@ packages:
engines: {node: '>=16'}
dev: false
/istanbul-lib-coverage@3.2.1:
resolution: {integrity: sha512-opCrKqbthmq3SKZ10mFMQG9dk3fTa3quaOLD35kJa5ejwZHd9xAr+kLuziiZz2cG32s4lMZxNdmdcEQnTDP4+g==}
engines: {node: '>=8'}
dev: true
/istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
dependencies:
istanbul-lib-coverage: 3.2.1
make-dir: 4.0.0
supports-color: 7.2.0
dev: true
/istanbul-lib-source-maps@4.0.1:
resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
engines: {node: '>=10'}
dependencies:
debug: 4.3.4
istanbul-lib-coverage: 3.2.1
source-map: 0.6.1
transitivePeerDependencies:
- supports-color
dev: true
/istanbul-reports@3.1.6:
resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==}
engines: {node: '>=8'}
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
dev: true
/jackspeak@2.3.6:
resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
engines: {node: '>=14'}
@ -6248,6 +6317,13 @@ packages:
dependencies:
semver: 6.3.1
/make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
dependencies:
semver: 7.5.4
dev: true
/make-fetch-happen@11.1.1:
resolution: {integrity: sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@ -8596,6 +8672,15 @@ packages:
commander: 2.20.3
source-map-support: 0.5.21
/test-exclude@6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 7.2.3
minimatch: 3.1.2
dev: true
/text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -9080,6 +9165,15 @@ packages:
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
/v8-to-istanbul@9.1.3:
resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==}
engines: {node: '>=10.12.0'}
dependencies:
'@jridgewell/trace-mapping': 0.3.20
'@types/istanbul-lib-coverage': 2.0.5
convert-source-map: 2.0.0
dev: true
/validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies:
@ -9775,8 +9869,8 @@ packages:
resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==}
engines: {node: '>= 10.0.0'}
dependencies:
'@babel/parser': 7.23.0
'@babel/types': 7.23.0
'@babel/parser': 7.23.3
'@babel/types': 7.23.3
assert-never: 1.2.1
babel-walk: 3.0.0-canary-5
dev: false

1
test/mocks/app-config.ts Normal file
View File

@ -0,0 +1 @@
export default {}

4
test/mocks/paths.ts Normal file
View File

@ -0,0 +1,4 @@
import { joinURL } from 'ufo'
export const baseURL = () => '/'
export const buildAssetsURL = (url: string) => joinURL('/_nuxt', url)

View File

@ -3,7 +3,7 @@
import { describe, expect, it, vi } from 'vitest'
import { defineEventHandler } from 'h3'
import { registerEndpoint } from 'nuxt-vitest/utils'
import { mountSuspended, registerEndpoint } from 'nuxt-vitest/utils'
import * as composables from '#app/composables'
@ -39,6 +39,33 @@ registerEndpoint('/_nuxt/builds/meta/override.json', defineEventHandler(() => ({
prerendered: ['/specific-prerendered']
})))
describe('app config', () => {
it('can be updated', () => {
const appConfig = useAppConfig()
expect(appConfig).toMatchInlineSnapshot(`
{
"nuxt": {
"buildId": "override",
},
}
`)
updateAppConfig({
new: 'value',
// @ts-expect-error property does not exist
nuxt: { nested: 42 }
})
expect(appConfig).toMatchInlineSnapshot(`
{
"new": "value",
"nuxt": {
"buildId": "override",
"nested": 42,
},
}
`)
})
})
describe('composables', () => {
it('are all tested', () => {
const testedComposables: string[] = [
@ -52,6 +79,7 @@ describe('composables', () => {
'showError',
'useError',
'getAppManifest',
'useHydration',
'getRouteRules',
'onNuxtReady',
'setResponseStatus',
@ -62,17 +90,19 @@ describe('composables', () => {
'useRequestHeaders',
'clearNuxtState',
'useState',
'useRequestURL'
'useRequestURL',
'useRoute',
'navigateTo',
'abortNavigation',
'setPageLayout',
'defineNuxtComponent',
]
const skippedComposables: string[] = [
'abortNavigation',
'addRouteMiddleware',
'defineNuxtComponent',
'defineNuxtRouteMiddleware',
'definePayloadReducer',
'definePayloadReviver',
'loadPayload',
'navigateTo',
'onBeforeRouteLeave',
'onBeforeRouteUpdate',
'prefetchComponents',
@ -80,14 +110,11 @@ describe('composables', () => {
'preloadPayload',
'preloadRouteComponents',
'reloadNuxtApp',
'setPageLayout',
'useCookie',
'useFetch',
'useHead',
'useHydration',
'useLazyFetch',
'useLazyAsyncData',
'useRoute',
'useRouter',
'useSeoMeta',
'useServerSeoMeta'
@ -262,6 +289,20 @@ describe('ssr composables', () => {
})
})
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"')
})
})
describe('useState', () => {
it('default', () => {
expect(useState(() => 'default').value).toBe('default')
@ -378,3 +419,77 @@ describe.skipIf(process.env.TEST_MANIFEST === 'manifest-off')('app manifests', (
expect(await isPrerendered('/pre/thing')).toBeTruthy()
})
})
describe('routing utilities: `navigateTo`', () => {
it('navigateTo should disallow navigation to external URLs by default', () => {
expect(() => navigateTo('https://test.com')).toThrowErrorMatchingInlineSnapshot('"Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`."')
expect(() => navigateTo('https://test.com', { external: true })).not.toThrow()
})
it('navigateTo should disallow navigation to data/script URLs', () => {
const urls = [
['data:alert("hi")', 'data'],
['\0data:alert("hi")', 'data'],
]
for (const [url, protocol] of urls) {
expect(() => navigateTo(url, { external: true })).toThrowError(`Cannot navigate to a URL with '${protocol}:' protocol.`)
}
})
})
describe('routing utilities: `useRoute`', () => {
it('should show provide a mock route', async () => {
expect(useRoute()).toMatchObject({
fullPath: '/',
hash: '',
href: '/',
matched: [],
meta: {},
name: undefined,
params: {},
path: '/',
query: {},
redirectedFrom: undefined,
})
})
})
describe('routing utilities: `abortNavigation`', () => {
it('should throw an error if one is provided', () => {
const error = useError()
expect(() => abortNavigation({ message: 'Page not found' })).toThrowErrorMatchingInlineSnapshot('"Page not found"')
expect(error.value).toBeFalsy()
})
it('should block navigation if no error is provided', () => {
expect(abortNavigation()).toMatchInlineSnapshot('false')
})
})
describe('routing utilities: `setPageLayout`', () => {
it('should set error on page metadata if run outside middleware', () => {
const route = useRoute()
expect(route.meta.layout).toBeUndefined()
setPageLayout('custom')
expect(route.meta.layout).toEqual('custom')
route.meta.layout = undefined
})
it('should not set layout directly if run within middleware', async () => {
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({
render: () => h('div', 'hi there')
}))
expect(wrapper.html()).toMatchInlineSnapshot('"<div>hi there</div>"')
})
it.todo('should support Options API asyncData')
it.todo('should support Options API head')
})

View File

@ -1,13 +1,15 @@
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import { configDefaults } from 'vitest/config'
import { configDefaults, coverageConfigDefaults } from 'vitest/config'
import { isWindows } from 'std-env'
export default defineConfig({
resolve: {
alias: {
'#build/nuxt.config.mjs': resolve('./test/mocks/nuxt-config'),
'#app': resolve('./packages/nuxt/dist/app/index')
'#build/paths.mjs': resolve('./test/mocks/paths'),
'#build/app.config.mjs': resolve('./test/mocks/app-config'),
'#app': resolve('./packages/nuxt/dist/app')
}
},
define: {
@ -17,6 +19,10 @@ export default defineConfig({
globalSetup: './test/setup.ts',
setupFiles: ['./test/setup-env.ts'],
testTimeout: isWindows ? 60000 : 10000,
coverage: {
// TODO: remove when we upgrade to vitest 0.34.0: https://github.com/vitest-dev/vitest/pull/3794
exclude: [...coverageConfigDefaults.exclude, '**/virtual:nuxt:**'],
},
// Excluded plugin because it should throw an error when accidentally loaded via Nuxt
exclude: [...configDefaults.exclude, '**/test/nuxt/**', '**/test.ts', '**/this-should-not-load.spec.js'],
maxThreads: process.env.TEST_ENV === 'dev' ? 1 : undefined,

View File

@ -1,4 +1,5 @@
import { defineVitestConfig } from 'nuxt-vitest/config'
import { coverageConfigDefaults } from 'vitest/config'
export default defineVitestConfig({
// TODO: investigate
@ -8,6 +9,10 @@ export default defineVitestConfig({
test: {
dir: './test/nuxt',
environment: 'nuxt',
coverage: {
// TODO: remove when we upgrade to vitest 0.34.0: https://github.com/vitest-dev/vitest/pull/3794
exclude: [...coverageConfigDefaults.exclude, '**/virtual:nuxt:**'],
},
environmentOptions: {
nuxt: {
overrides: {