feat(test-utils): allow mounting single component for testing (#5723)

This commit is contained in:
Anthony Fu 2023-04-06 14:07:22 +02:00 committed by GitHub
parent 3fc9a75070
commit 72ba53efbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 79 additions and 2 deletions

View File

@ -2,6 +2,7 @@
<Suspense @resolve="onResolve"> <Suspense @resolve="onResolve">
<ErrorComponent v-if="error" :error="error" /> <ErrorComponent v-if="error" :error="error" />
<IslandRenderer v-else-if="islandContext" :context="islandContext" /> <IslandRenderer v-else-if="islandContext" :context="islandContext" />
<component :is="SingleRenderer" v-else-if="SingleRenderer" />
<AppComponent v-else /> <AppComponent v-else />
</Suspense> </Suspense>
</template> </template>
@ -21,6 +22,10 @@ const IslandRenderer = process.server
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const onResolve = nuxtApp.deferHydration() const onResolve = nuxtApp.deferHydration()
const url = process.server ? nuxtApp.ssrContext.url : window.location.pathname
const SingleRenderer = process.dev && process.server && url.startsWith('/__nuxt_component_test__/') && defineAsyncComponent(() => import('#build/test-component-wrapper.mjs')
.then(r => r.default(process.server ? url : window.location.href)))
// Inject default route (outside of pages) as active route // Inject default route (outside of pages) as active route
provide('_route', useRoute()) provide('_route', useRoute())

View File

@ -0,0 +1,19 @@
import { parseURL } from 'ufo'
import { defineComponent, h } from 'vue'
import { parseQuery } from 'vue-router'
export default (url:string) => defineComponent({
name: 'NuxtTestComponentWrapper',
async setup (props, { attrs }) {
const query = parseQuery(parseURL(url).search)
const urlProps = query.props ? JSON.parse(query.props as string) : {}
const comp = await import(/* @vite-ignore */ query.path as string).then(r => r.default)
return () => [
h('div', 'Component Test Wrapper for ' + query.path),
h('div', { id: 'nuxt-component-root' }, [
h(comp, { ...attrs, ...props, ...urlProps })
])
]
}
})

View File

@ -40,6 +40,11 @@ export const errorComponentTemplate: NuxtTemplate<TemplateContext> = {
filename: 'error-component.mjs', filename: 'error-component.mjs',
getContents: ctx => genExport(ctx.app.errorComponent!, ['default']) getContents: ctx => genExport(ctx.app.errorComponent!, ['default'])
} }
// TODO: Use an alias
export const testComponentWrapperTemplate = {
filename: 'test-component-wrapper.mjs',
getContents: (ctx: TemplateContext) => genExport(resolve(ctx.nuxt.options.appDir, 'components/test-component-wrapper'), ['default'])
}
export const cssTemplate: NuxtTemplate<TemplateContext> = { export const cssTemplate: NuxtTemplate<TemplateContext> = {
filename: 'css.mjs', filename: 'css.mjs',

View File

@ -4,6 +4,7 @@ export default defineBuildConfig({
declaration: true, declaration: true,
entries: [ entries: [
'src/index', 'src/index',
'src/experimental',
{ input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' } { input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' }
], ],
externals: [ externals: [

1
packages/test-utils/experimental.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from './dist/experimental'

View File

@ -9,6 +9,10 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.mjs" "import": "./dist/index.mjs"
},
"./experimental": {
"types": "./dist/experimental.d.ts",
"import": "./dist/experimental.mjs"
} }
}, },
"files": [ "files": [
@ -26,7 +30,8 @@
"get-port-please": "^3.0.1", "get-port-please": "^3.0.1",
"jiti": "^1.18.2", "jiti": "^1.18.2",
"ofetch": "^1.0.1", "ofetch": "^1.0.1",
"pathe": "^1.1.0" "pathe": "^1.1.0",
"ufo": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"playwright": "^1.32.2", "playwright": "^1.32.2",

View File

@ -0,0 +1,23 @@
import { fetch as _fetch, $fetch as _$fetch } from 'ofetch'
import * as _kit from '@nuxt/kit'
import { resolve } from 'pathe'
import { stringifyQuery } from 'ufo'
import { useTestContext } from './context'
import { $fetch } from './server'
/**
* This is a function to render a component directly with the Nuxt server.
*/
export function $fetchComponent (filepath: string, props?: Record<string, any>) {
return $fetch(componentTestUrl(filepath, props))
}
export function componentTestUrl (filepath: string, props?: Record<string, any>) {
const ctx = useTestContext()
filepath = resolve(ctx.options.rootDir, filepath)
const path = stringifyQuery({
path: filepath,
props: JSON.stringify(props)
})
return `/__nuxt_component_test__/?${path}`
}

View File

@ -1,9 +1,9 @@
import { resolve } from 'node:path'
import { execa } from 'execa' import { execa } from 'execa'
import { getRandomPort, waitForPort } from 'get-port-please' import { getRandomPort, waitForPort } from 'get-port-please'
import type { FetchOptions } from 'ofetch' import type { FetchOptions } from 'ofetch'
import { fetch as _fetch, $fetch as _$fetch } from 'ofetch' import { fetch as _fetch, $fetch as _$fetch } from 'ofetch'
import * as _kit from '@nuxt/kit' import * as _kit from '@nuxt/kit'
import { resolve } from 'pathe'
import { useTestContext } from './context' import { useTestContext } from './context'
// @ts-ignore type cast // @ts-ignore type cast
@ -75,5 +75,8 @@ export function url (path: string) {
if (!ctx.url) { if (!ctx.url) {
throw new Error('url is not available (is server option enabled?)') throw new Error('url is not available (is server option enabled?)')
} }
if (path.startsWith(ctx.url)) {
return path
}
return ctx.url + path return ctx.url + path
} }

View File

@ -754,6 +754,9 @@ importers:
pathe: pathe:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0 version: 1.1.0
ufo:
specifier: ^1.1.1
version: 1.1.1
vue: vue:
specifier: ^3.2.47 specifier: ^3.2.47
version: 3.2.47 version: 3.2.47

View File

@ -7,6 +7,7 @@ import { setup, fetch, $fetch, startServer, isDev, createPage, url } from '@nuxt
import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer' import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
import { expectNoClientErrors, expectWithPolling, renderPage, withLogs } from './utils' import { expectNoClientErrors, expectWithPolling, renderPage, withLogs } from './utils'
import { $fetchComponent } from '@nuxt/test-utils/experimental'
const isWebpack = process.env.TEST_BUILDER === 'webpack' const isWebpack = process.env.TEST_BUILDER === 'webpack'
@ -1277,3 +1278,13 @@ describe.skipIf(isWindows)('useAsyncData', () => {
await expectNoClientErrors('/useAsyncData/promise-all') await expectNoClientErrors('/useAsyncData/promise-all')
}) })
}) })
describe.runIf(isDev())('component testing', () => {
it('should work', async () => {
const comp1 = await $fetchComponent('components/SugarCounter.vue', { multiplier: 2 })
expect(comp1).toContain('12 x 2 = 24')
const comp2 = await $fetchComponent('components/SugarCounter.vue', { multiplier: 4 })
expect(comp2).toContain('12 x 4 = 48')
})
})

View File

@ -7,6 +7,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'#app': resolve('./packages/nuxt/dist/app/index'), '#app': resolve('./packages/nuxt/dist/app/index'),
'@nuxt/test-utils/experimental': resolve('./packages/test-utils/src/experimental.ts'),
'@nuxt/test-utils': resolve('./packages/test-utils/src/index.ts') '@nuxt/test-utils': resolve('./packages/test-utils/src/index.ts')
} }
}, },