test: refactor type tests into a separate fixture (#21007)

This commit is contained in:
Daniel Roe 2023-05-22 20:44:03 +01:00 committed by GitHub
parent 397c54c9db
commit fd30cc1e89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 483 additions and 63 deletions

View File

@ -21,7 +21,9 @@
"test:fixtures:payload": "TEST_PAYLOAD=js pnpm test:fixtures",
"test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures",
"test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures",
"test:types": "nuxi prepare test/fixtures/basic && cd test/fixtures/basic && npx vue-tsc --noEmit",
"test:types": "pnpm test:types:basic && pnpm test:types:minimal",
"test:types:basic": "nuxi prepare test/fixtures/basic-types && cd test/fixtures/basic-types && npx vue-tsc --noEmit",
"test:types:minimal": "nuxi prepare test/fixtures/minimal-types && cd test/fixtures/minimal-types && npx vue-tsc --noEmit",
"test:unit": "vitest run --dir packages",
"typecheck": "tsc --noEmit"
},

View File

@ -913,12 +913,37 @@ importers:
specifier: latest
version: 4.2.1(vue@3.3.4)
test/fixtures/basic-types:
dependencies:
nuxt:
specifier: workspace:*
version: link:../../../packages/nuxt
devDependencies:
ofetch:
specifier: latest
version: 1.0.1
unplugin:
specifier: latest
version: 1.3.1
vitest:
specifier: latest
version: 0.31.1(playwright@1.34.0)
vue-router:
specifier: latest
version: 4.2.1(vue@3.3.4)
test/fixtures/minimal:
dependencies:
nuxt:
specifier: workspace:*
version: link:../../../packages/nuxt
test/fixtures/minimal-types:
dependencies:
nuxt:
specifier: workspace:*
version: link:../../../packages/nuxt
test/fixtures/runtime-compiler:
dependencies:
nuxt:

1
test/fixtures/basic-types/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
!extends/node_modules

View File

@ -0,0 +1,6 @@
export default defineAppConfig({
userConfig: 123,
nested: {
val: 2
}
})

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
defineProps({
aProp: Number
})
defineSlots<{
fallback: { id: string }
}>()
defineExpose<{
_exposedValue: boolean
}>()
const _exposedValue = 42
const emit = defineEmits<{
'some-event': [id: string]
}>()
emit('some-event', '42')
// @ts-expect-error an invalid argument
emit('some-event', 42)
// @ts-expect-error an unknown event
emit('unknown-event', 42)
</script>
<template>
<div>
<!-- -->
</div>
</template>

View File

@ -0,0 +1,3 @@
export default {
fromLayer: true
}

View File

@ -0,0 +1,5 @@
declare module 'bing' {
interface BingInterface {
foo: 'bar'
}
}

View File

@ -0,0 +1,6 @@
<template>
<div>
<div>Extended layout from bar</div>
<NuxtPage />
</div>
</template>

View File

@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware((to) => {
to.meta.override = 'Injected by extended middleware from bar'
})

View File

@ -0,0 +1 @@
export default defineNuxtConfig({})

View File

@ -0,0 +1,14 @@
<script setup>
definePageMeta({
layout: 'override',
middleware: 'override'
})
</script>
<template>
<div>
<div>Extended page from bar</div>
<div>Middleware | override: {{ $route.meta.override }}</div>
<ExtendsOverride />
</div>
</template>

View File

@ -0,0 +1,6 @@
<template>
<div>
<div>Extended layout from foo</div>
<NuxtPage />
</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div>This layout should be overriden by bar</div>
</template>

View File

@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware((to) => {
to.meta.foo = 'Injected by extended middleware from foo'
})

View File

@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware((to) => {
to.meta.override = 'This middleware should be overriden by bar'
})

View File

@ -0,0 +1,3 @@
import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({})

View File

@ -0,0 +1,19 @@
<script setup>
import { test } from '~/alias/test'
definePageMeta({
middleware: 'foo'
})
const foo = useExtendsFoo()
</script>
<template>
<div>
<div>{{ test }}</div>
<div>Extended page from foo</div>
<div>Middleware | foo: {{ $route.meta.foo }}</div>
<div>Composable | useExtendsFoo: {{ foo }}</div>
<div>Plugin | foo: {{ $foo() }}</div>
<ExtendsFoo />
</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div>This page should be overriden by bar</div>
</template>

View File

@ -0,0 +1,7 @@
export default defineNuxtPlugin(() => {
return {
provide: {
foo: () => 'String generated from foo plugin!'
}
}
})

View File

@ -0,0 +1,3 @@
import { eventHandler } from 'h3'
export default eventHandler(() => 'foo')

View File

View File

@ -0,0 +1,6 @@
<template>
<div>
Custom Layout:
<slot />
</div>
</template>

View File

@ -0,0 +1,7 @@
export default defineNuxtRouteMiddleware((to) => {
if ('abort' in to.query) {
return abortNavigation({
statusCode: 401
})
}
})

View File

@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware((to) => {
to.meta.auth = 'Injected by injectAuth middleware'
})

View File

@ -0,0 +1,15 @@
import { addServerHandler, createResolver, defineNuxtModule } from 'nuxt/kit'
export default defineNuxtModule({
meta: {
name: 'auto-registered-module'
},
setup () {
const resolver = createResolver(import.meta.url)
addServerHandler({
handler: resolver.resolve('./runtime/handler'),
route: '/auto-registered-module'
})
}
})

View File

@ -0,0 +1 @@
export default defineEventHandler(() => 'handler added by auto-registered module')

View File

@ -0,0 +1,23 @@
import { addPlugin, createResolver, defineNuxtModule, useNuxt } from 'nuxt/kit'
export default defineNuxtModule({
defaults: {
enabled: true,
typeTest: (value: boolean) => typeof value === 'boolean'
},
meta: {
name: 'my-module',
configKey: 'sampleModule'
},
setup () {
const resolver = createResolver(import.meta.url)
addPlugin(resolver.resolve('./runtime/plugin'))
useNuxt().hook('app:resolve', (app) => {
app.middleware.push({
name: 'unctx-test',
path: resolver.resolve('./runtime/middleware')
})
})
}
})

View File

@ -0,0 +1,19 @@
import { createResolver, defineNuxtModule, useNuxt } from 'nuxt/kit'
export default defineNuxtModule({
meta: {
name: 'page-extend'
},
setup () {
const nuxt = useNuxt()
const resolver = createResolver(import.meta.url)
nuxt.hook('pages:extend', (pages) => {
pages.push({
name: 'page-extend',
path: '/page-extend',
file: resolver.resolve('./runtime/page.vue')
})
})
}
})

View File

@ -0,0 +1,4 @@
export default defineNuxtRouteMiddleware(async () => {
await new Promise(resolve => setTimeout(resolve, 1))
useNuxtApp()
})

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import { setResponseHeader } from 'h3'
definePageMeta({
value: 'added in pages:extend'
})
if (process.server) {
setResponseHeader(useRequestEvent(), 'x-extend', useRoute().meta.value as string)
}
</script>
<template>
<div>
added in pages:extend
</div>
</template>

View File

@ -0,0 +1,4 @@
export default defineNuxtPlugin(async () => {
await new Promise(resolve => setTimeout(resolve, 1))
useNuxtApp()
})

View File

@ -0,0 +1,7 @@
import { defineNuxtModule } from 'nuxt/kit'
export default defineNuxtModule({
meta: {
name: 'test'
}
})

View File

@ -0,0 +1,76 @@
export default defineNuxtConfig({
experimental: {
typedPages: true
},
typescript: {
strict: true,
tsConfig: {
compilerOptions: {
moduleResolution: process.env.MODULE_RESOLUTION
}
}
},
buildDir: process.env.NITRO_BUILD_DIR,
builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite',
theme: './extends/bar',
extends: [
'./extends/node_modules/foo'
],
runtimeConfig: {
baseURL: '',
baseAPIToken: '',
privateConfig: 'secret_key',
public: {
ids: [1, 2, 3],
needsFallback: undefined,
testConfig: 123
}
},
appConfig: {
fromNuxtConfig: true,
nested: {
val: 1
}
},
modules: [
'./modules/test',
[
'~/modules/example',
{
typeTest (val) {
// @ts-expect-error module type defines val as boolean
const b: string = val
return !!b
}
}
],
function (_options, nuxt) {
nuxt.hook('pages:extend', (pages) => {
pages.push({
name: 'internal-async-parent',
path: '/internal-async-parent'
})
})
}
],
telemetry: false, // for testing telemetry types - it is auto-disabled in tests
hooks: {
'schema:extend' (schemas) {
schemas.push({
appConfig: {
someThing: {
value: {
$default: 'default',
$schema: {
tsType: 'string | false'
}
}
}
}
})
},
'prepare:types' ({ tsConfig }) {
tsConfig.include = tsConfig.include!.filter(i => i !== '../../../../**/*')
}
}
})

16
test/fixtures/basic-types/package.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"private": true,
"name": "fixture-basic-types",
"scripts": {
"build": "nuxi build"
},
"dependencies": {
"nuxt": "workspace:*"
},
"devDependencies": {
"ofetch": "latest",
"unplugin": "latest",
"vitest": "latest",
"vue-router": "latest"
}
}

View File

@ -0,0 +1,5 @@
<template>
<div>
<!-- -->
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
<!-- -->
</div>
</template>

View File

@ -0,0 +1,7 @@
export default defineNuxtPlugin(() => {
return {
provide: {
pluginInjection: () => ''
}
}
})

View File

@ -0,0 +1 @@
export default defineEventHandler(() => 'Hello API')

View File

@ -0,0 +1,4 @@
export default defineEventHandler(() => ({
foo: 'bar',
baz: 'qux'
}))

View File

@ -0,0 +1,3 @@
export default defineEventHandler(() => ({
method: 'post' as const
}))

View File

@ -0,0 +1,4 @@
export default defineEventHandler(() => ({
type: 'a',
foo: 'bar'
}) as { type: 'a', foo: string } | { type: 'b', baz: string })

View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -7,7 +7,7 @@ import type { AppConfig, RuntimeValue } from 'nuxt/schema'
import { defineNuxtConfig } from 'nuxt/config'
import { callWithNuxt, isVue3 } from '#app'
import type { NavigateToOptions } from '#app/composables/router'
import { NuxtLink, NuxtPage } from '#components'
import { NuxtLink, NuxtPage, WithTypes } from '#components'
import { useRouter } from '#imports'
interface TestResponse { message: string }
@ -15,6 +15,10 @@ interface TestResponse { message: string }
describe('API routes', () => {
it('generates types for routes', () => {
expectTypeOf($fetch('/api/hello')).toEqualTypeOf<Promise<string>>()
// registered in extends
expectTypeOf($fetch('/api/foo')).toEqualTypeOf<Promise<string>>()
// registered in module
expectTypeOf($fetch('/auto-registered-module')).toEqualTypeOf<Promise<string>>()
expectTypeOf($fetch('/api/hey')).toEqualTypeOf<Promise<{ foo: string, baz: string }>>()
expectTypeOf($fetch('/api/hey', { method: 'get' })).toEqualTypeOf<Promise<{ foo: string, baz: string }>>()
expectTypeOf($fetch('/api/hey', { method: 'post' })).toEqualTypeOf<Promise<{ method: 'post' }>>()
@ -90,11 +94,14 @@ describe('aliases', () => {
describe('middleware', () => {
it('recognizes named middleware', () => {
definePageMeta({ middleware: 'inject-auth' })
definePageMeta({ middleware: 'named' })
// provided by layer
definePageMeta({ middleware: 'override' })
definePageMeta({ middleware: 'foo' })
// @ts-expect-error ignore global middleware
definePageMeta({ middleware: 'redirect' })
definePageMeta({ middleware: 'global' })
// @ts-expect-error Invalid middleware
definePageMeta({ middleware: 'invalid-middleware' })
definePageMeta({ middleware: 'nonexistent' })
})
it('handles adding middleware', () => {
addRouteMiddleware('example', (to, from) => {
@ -117,14 +124,14 @@ describe('typed router integration', () => {
// @ts-expect-error this named route does not exist
router.push({ name: 'some-thing' })
// this one does
router.push({ name: 'fixed-keyed-child-parent' })
router.push({ name: 'page' })
// @ts-expect-error this is an invalid param
router.push({ name: 'random-id', params: { bob: 23 } })
router.push({ name: 'random-id', params: { id: 4 } })
router.push({ name: 'param-id', params: { bob: 23 } })
router.push({ name: 'param-id', params: { id: 4 } })
})
it('allows typing useRoute', () => {
const route = useRoute('random-id')
const route = useRoute('param-id')
// @ts-expect-error this param does not exist
const _invalid = route.params.something
// this param does
@ -135,36 +142,40 @@ describe('typed router integration', () => {
// @ts-expect-error this named route does not exist
navigateTo({ name: 'some-thing' })
// this one does
navigateTo({ name: 'fixed-keyed-child-parent' })
navigateTo({ name: 'page' })
// @ts-expect-error this is an invalid param
navigateTo({ name: 'random-id', params: { bob: 23 } })
navigateTo({ name: 'random-id', params: { id: 4 } })
navigateTo({ name: 'param-id', params: { bob: 23 } })
navigateTo({ name: 'param-id', params: { id: 4 } })
})
it('allows typing middleware', () => {
defineNuxtRouteMiddleware((to) => {
expectTypeOf(to.name).not.toBeAny()
// @ts-expect-error this route does not exist
expectTypeOf(to.name === 'bob').toMatchTypeOf<boolean>()
expectTypeOf(to.name === 'assets').toMatchTypeOf<boolean>()
expectTypeOf(to.name === 'bob').toEqualTypeOf<boolean>()
expectTypeOf(to.name === 'page').toEqualTypeOf<boolean>()
})
})
it('respects pages:extend augmentation', () => {
// added via pages:extend
expectTypeOf(useRoute().name === 'internal-async-parent').toMatchTypeOf<boolean>()
expectTypeOf(useRoute().name === 'internal-async-parent').toEqualTypeOf<boolean>()
// @ts-expect-error this route does not exist
expectTypeOf(useRoute().name === 'invalid').toMatchTypeOf<boolean>()
expectTypeOf(useRoute().name === 'invalid').toEqualTypeOf<boolean>()
})
it('respects pages added via layer', () => {
expectTypeOf(useRoute().name === 'override').toEqualTypeOf<boolean>()
})
it('allows typing NuxtLink', () => {
// @ts-expect-error this named route does not exist
h(NuxtLink, { to: { name: 'some-thing' } })
// this one does
h(NuxtLink, { to: { name: 'fixed-keyed-child-parent' } })
h(NuxtLink, { to: { name: 'page' } })
// @ts-expect-error this is an invalid param
h(NuxtLink, { to: { name: 'random-id', params: { bob: 23 } } })
h(NuxtLink, { to: { name: 'random-id', params: { id: 4 } } })
h(NuxtLink, { to: { name: 'param-id', params: { bob: 23 } } })
h(NuxtLink, { to: { name: 'param-id', params: { id: 4 } } })
})
})
@ -172,6 +183,7 @@ describe('layouts', () => {
it('recognizes named layouts', () => {
definePageMeta({ layout: 'custom' })
definePageMeta({ layout: 'pascal-case' })
definePageMeta({ layout: 'override' })
// @ts-expect-error Invalid layout
definePageMeta({ layout: 'invalid-layout' })
})
@ -189,7 +201,8 @@ describe('modules', () => {
describe('nuxtApp', () => {
it('types injections provided by plugins', () => {
expectTypeOf(useNuxtApp().$asyncPlugin).toEqualTypeOf<() => string>()
expectTypeOf(useNuxtApp().$pluginInjection).toEqualTypeOf<() => ''>()
expectTypeOf(useNuxtApp().$foo).toEqualTypeOf<() => 'String generated from foo plugin!'>()
expectTypeOf(useNuxtApp().$router).toEqualTypeOf<Router>()
})
it('marks unknown injections as unknown', () => {
@ -264,6 +277,13 @@ describe('components', () => {
it('includes types for NuxtPage', () => {
expectTypeOf(NuxtPage).not.toBeAny()
})
it('includes types for other components', () => {
h(WithTypes)
// @ts-expect-error wrong prop type for this component
h(WithTypes, { aProp: '40' })
// TODO: assert typed slots, exposed, generics, etc.
})
})
describe('composables', () => {

View File

@ -1,10 +1,6 @@
import { addPlugin, createResolver, defineNuxtModule, useNuxt } from 'nuxt/kit'
export default defineNuxtModule({
defaults: {
enabled: true,
typeTest: (value: boolean) => typeof value === 'boolean'
},
meta: {
name: 'my-module',
configKey: 'sampleModule'

View File

@ -11,14 +11,6 @@ declare module 'nitropack' {
}
export default defineNuxtConfig({
typescript: {
strict: true,
tsConfig: {
compilerOptions: {
moduleResolution: process.env.MODULE_RESOLUTION
}
}
},
app: {
pageTransition: true,
layoutTransition: true,
@ -70,27 +62,14 @@ export default defineNuxtConfig({
]
},
runtimeConfig: {
baseURL: '',
baseAPIToken: '',
privateConfig: 'secret_key',
public: {
ids: [1, 2, 3],
needsFallback: undefined,
testConfig: 123
}
},
modules: [
'./modules/test',
[
'~/modules/example',
{
typeTest (val) {
// @ts-expect-error module type defines val as boolean
const b: string = val
return !!b
}
}
],
'~/modules/example',
function (_, nuxt) {
if (typeof nuxt.options.builder === 'string' && nuxt.options.builder.includes('webpack')) { return }
@ -143,23 +122,6 @@ export default defineNuxtConfig({
},
telemetry: false, // for testing telemetry types - it is auto-disabled in tests
hooks: {
'schema:extend' (schemas) {
schemas.push({
appConfig: {
someThing: {
value: {
$default: 'default',
$schema: {
tsType: 'string | false'
}
}
}
}
})
},
'prepare:types' ({ tsConfig }) {
tsConfig.include = tsConfig.include!.filter(i => i !== '../../../../**/*')
},
'modules:done' () {
addComponent({
name: 'CustomComponent',

3
test/fixtures/minimal-types/app.vue vendored Normal file
View File

@ -0,0 +1,3 @@
<template>
<div>Hello World!</div>
</template>

View File

@ -0,0 +1 @@
export default defineNuxtConfig({})

View File

@ -0,0 +1,10 @@
{
"private": true,
"name": "fixture-minimal-types",
"scripts": {
"build": "nuxi build"
},
"dependencies": {
"nuxt": "workspace:*"
}
}

View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

48
test/fixtures/minimal-types/types.ts vendored Normal file
View File

@ -0,0 +1,48 @@
import { describe, expectTypeOf, it } from 'vitest'
describe('routing utilities', () => {
it('allows using route composables', () => {
const router = useRouter()
router.push('/test')
expectTypeOf(useRouter()).not.toBeAny()
expectTypeOf(useRoute()).not.toBeAny()
navigateTo('/thing')
})
})
describe('auto-imports', () => {
it('defineNuxtConfig', () => {
defineNuxtConfig({
modules: [],
// @ts-expect-error Should show error on unknown properties
unknownProp: ''
})
})
it('core composables', () => {
ref()
useHead({
script: [],
// @ts-expect-error Should show error on unknown properties
unknown: []
})
})
})
describe('config typings', () => {
it('runtimeConfig', () => {
expectTypeOf(useRuntimeConfig()).toMatchTypeOf<{
app: {
baseURL: string
buildAssetsDir: string
cdnURL: string
}
public: Record<string, any>
}>()
})
it('appConfig', () => {
expectTypeOf(useAppConfig()).toEqualTypeOf<{ [key: string]: unknown }>()
})
})