From 72ba53efbc2384f802d654fffd92eaf36a81b507 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Thu, 6 Apr 2023 14:07:22 +0200 Subject: [PATCH] feat(test-utils): allow mounting single component for testing (#5723) --- .../nuxt/src/app/components/nuxt-root.vue | 5 ++++ .../app/components/test-component-wrapper.ts | 19 +++++++++++++++ packages/nuxt/src/core/templates.ts | 5 ++++ packages/test-utils/build.config.ts | 1 + packages/test-utils/experimental.d.ts | 1 + packages/test-utils/package.json | 7 +++++- packages/test-utils/src/experimental.ts | 23 +++++++++++++++++++ packages/test-utils/src/server.ts | 5 +++- pnpm-lock.yaml | 3 +++ test/basic.test.ts | 11 +++++++++ vitest.config.ts | 1 + 11 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 packages/nuxt/src/app/components/test-component-wrapper.ts create mode 100644 packages/test-utils/experimental.d.ts create mode 100644 packages/test-utils/src/experimental.ts diff --git a/packages/nuxt/src/app/components/nuxt-root.vue b/packages/nuxt/src/app/components/nuxt-root.vue index 49835190622..698899bce4e 100644 --- a/packages/nuxt/src/app/components/nuxt-root.vue +++ b/packages/nuxt/src/app/components/nuxt-root.vue @@ -2,6 +2,7 @@ + @@ -21,6 +22,10 @@ const IslandRenderer = 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()) diff --git a/packages/nuxt/src/app/components/test-component-wrapper.ts b/packages/nuxt/src/app/components/test-component-wrapper.ts new file mode 100644 index 00000000000..676b821e8be --- /dev/null +++ b/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', + + 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 }) + ]) + ] + } +}) diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index 33c031cd08c..0f75b2bc292 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -40,6 +40,11 @@ export const errorComponentTemplate: NuxtTemplate = { 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 = { filename: 'css.mjs', diff --git a/packages/test-utils/build.config.ts b/packages/test-utils/build.config.ts index 4b6640762b1..e0f61579a17 100644 --- a/packages/test-utils/build.config.ts +++ b/packages/test-utils/build.config.ts @@ -4,6 +4,7 @@ export default defineBuildConfig({ declaration: true, entries: [ 'src/index', + 'src/experimental', { input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' } ], externals: [ diff --git a/packages/test-utils/experimental.d.ts b/packages/test-utils/experimental.d.ts new file mode 100644 index 00000000000..c9d24adffad --- /dev/null +++ b/packages/test-utils/experimental.d.ts @@ -0,0 +1 @@ +export * from './dist/experimental' diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 603989324ed..d0ce8c87e4b 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -9,6 +9,10 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs" + }, + "./experimental": { + "types": "./dist/experimental.d.ts", + "import": "./dist/experimental.mjs" } }, "files": [ @@ -26,7 +30,8 @@ "get-port-please": "^3.0.1", "jiti": "^1.18.2", "ofetch": "^1.0.1", - "pathe": "^1.1.0" + "pathe": "^1.1.0", + "ufo": "^1.1.1" }, "devDependencies": { "playwright": "^1.32.2", diff --git a/packages/test-utils/src/experimental.ts b/packages/test-utils/src/experimental.ts new file mode 100644 index 00000000000..7d24def0a55 --- /dev/null +++ b/packages/test-utils/src/experimental.ts @@ -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) { + return $fetch(componentTestUrl(filepath, props)) +} + +export function componentTestUrl (filepath: string, props?: Record) { + const ctx = useTestContext() + filepath = resolve(ctx.options.rootDir, filepath) + const path = stringifyQuery({ + path: filepath, + props: JSON.stringify(props) + }) + return `/__nuxt_component_test__/?${path}` +} diff --git a/packages/test-utils/src/server.ts b/packages/test-utils/src/server.ts index 56261dd8982..77b580e2ae3 100644 --- a/packages/test-utils/src/server.ts +++ b/packages/test-utils/src/server.ts @@ -1,9 +1,9 @@ -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 { useTestContext } from './context' // @ts-ignore type cast @@ -75,5 +75,8 @@ export function url (path: string) { 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 } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a63d5dcdde..cdfee34c886 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -754,6 +754,9 @@ importers: pathe: specifier: ^1.1.0 version: 1.1.0 + ufo: + specifier: ^1.1.1 + version: 1.1.1 vue: specifier: ^3.2.47 version: 3.2.47 diff --git a/test/basic.test.ts b/test/basic.test.ts index 9e34bb31205..35f764b4371 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -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 { expectNoClientErrors, expectWithPolling, renderPage, withLogs } from './utils' +import { $fetchComponent } from '@nuxt/test-utils/experimental' const isWebpack = process.env.TEST_BUILDER === 'webpack' @@ -1277,3 +1278,13 @@ describe.skipIf(isWindows)('useAsyncData', () => { 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') + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index 6a054ad90c5..048dfb643bd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ resolve: { alias: { '#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') } },