Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

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

Merged
merged 11 commits into from Apr 6, 2023
6 changes: 6 additions & 0 deletions packages/nuxt/src/app/components/nuxt-root.vue
Expand Up @@ -2,6 +2,7 @@
<Suspense @resolve="onResolve">
<ErrorComponent v-if="error" :error="error" />
<IslandRendererer v-else-if="islandContext" :context="islandContext" />
<component v-else-if="SingleRenderer" :is="SingleRenderer" />
<AppComponent v-else />
</Suspense>
</template>
Expand All @@ -19,6 +20,11 @@ const IslandRendererer = process.server
const nuxtApp = useNuxtApp()
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
provide('_route', useRoute())

Expand Down
19 changes: 19 additions & 0 deletions packages/nuxt/src/app/components/test-component-wrapper.ts
@@ -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',
// eslint-disable-next-line require-await
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 })
])
]
}
})
5 changes: 5 additions & 0 deletions packages/nuxt/src/core/templates.ts
Expand Up @@ -40,6 +40,11 @@ export const errorComponentTemplate: NuxtTemplate<TemplateContext> = {
filename: 'error-component.mjs',
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> = {
filename: 'css.mjs',
Expand Down
3 changes: 2 additions & 1 deletion packages/test-utils/package.json
Expand Up @@ -21,7 +21,8 @@
"get-port-please": "^3.0.1",
"jiti": "^1.16.2",
"ofetch": "^1.0.0",
"pathe": "^1.1.0"
"pathe": "^1.1.0",
"ufo": "^1.0.1"
},
"devDependencies": {
"playwright": "^1.30.0",
Expand Down
20 changes: 19 additions & 1 deletion packages/test-utils/src/server.ts
@@ -1,9 +1,10 @@
import { resolve } from 'node:path'
import { execa } from 'execa'
import { getRandomPort, waitForPort } from 'get-port-please'
import type { FetchOptions } from 'ofetch'
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'

// @ts-ignore type cast
Expand Down Expand Up @@ -69,10 +70,27 @@ export function $fetch (path: string, options?: FetchOptions) {
return _$fetch(url(path), options)
}

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}`
}

export function url (path: string) {
const ctx = useTestContext()
if (!ctx.url) {
throw new Error('url is not available (is server option enabled?)')
}
if (path.startsWith(ctx.url)) {
return path
}
return ctx.url + path
}
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion test/basic.test.ts
Expand Up @@ -5,7 +5,7 @@ import { joinURL, withQuery } from 'ufo'
import { isWindows } from 'std-env'
import { join, normalize } from 'pathe'
// eslint-disable-next-line import/order
import { setup, fetch, $fetch, startServer, isDev, createPage, url } from '@nuxt/test-utils'
import { setup, fetch, $fetch, $fetchComponent, startServer, isDev, createPage, url } from '@nuxt/test-utils'

import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
import { expectNoClientErrors, fixturesDir, expectWithPolling, renderPage, withLogs } from './utils'
Expand Down Expand Up @@ -986,6 +986,16 @@ describe.skipIf(isWindows)('useAsyncData', () => {
})
})

describe.runIf(process.env.NUXT_TEST_DEV)('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')
})
})

// HMR should be at the last
// TODO: fix HMR on Windows
if (isDev() && !isWindows) {
Expand Down