diff --git a/examples/app-vitest-full/components/TestTeleport.vue b/examples/app-vitest-full/components/TestTeleport.vue
new file mode 100644
index 000000000..3e3de3d69
--- /dev/null
+++ b/examples/app-vitest-full/components/TestTeleport.vue
@@ -0,0 +1,9 @@
+
+
+
+
+ Teleport Title
+
+
+
+
diff --git a/examples/app-vitest-full/pages/other/index.vue b/examples/app-vitest-full/pages/other/index.vue
new file mode 100644
index 000000000..9bf43b5f3
--- /dev/null
+++ b/examples/app-vitest-full/pages/other/index.vue
@@ -0,0 +1,12 @@
+
+
+
+ -
+ Options Api
+
+ -
+ Teleports
+
+
+
+
diff --git a/examples/app-vitest-full/pages/other/teleports.vue b/examples/app-vitest-full/pages/other/teleports.vue
new file mode 100644
index 000000000..3b7763ca9
--- /dev/null
+++ b/examples/app-vitest-full/pages/other/teleports.vue
@@ -0,0 +1,8 @@
+
+
+
+ Teleport
+
+
+
+
diff --git a/examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts b/examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts
index 260fda2c7..fc6e92f90 100644
--- a/examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts
+++ b/examples/app-vitest-full/tests/nuxt/mount-suspended.spec.ts
@@ -30,7 +30,7 @@ import ComponentWithCssVar from '~/components/ComponentWithCssVar.vue'
import ComponentWithPluginProvidedValue from '~/components/ComponentWithPluginProvidedValue.vue'
import GenericStateComponent from '~/components/GenericStateComponent.vue'
-import { BoundAttrs } from '#components'
+import { BoundAttrs, TestTeleport } from '#components'
import DirectiveComponent from '~/components/DirectiveComponent.vue'
import CustomComponent from '~/components/CustomComponent.vue'
import WrapperElement from '~/components/WrapperElement.vue'
@@ -592,6 +592,16 @@ it('element should be changed', async () => {
expect(component.element.tagName).toBe('SPAN')
})
+it('teleport should work', async () => {
+ expect(document.getElementById('teleport-title')).toBeFalsy()
+
+ const wrapper = await mountSuspended(TestTeleport)
+ expect(document.getElementById('teleport-title')).toBeTruthy()
+
+ wrapper.unmount()
+ expect(document.getElementById('teleport-title')).toBeFalsy()
+})
+
const { useCounterMock } = vi.hoisted(() => {
return {
useCounterMock: vi.fn(() => {
diff --git a/examples/app-vitest-full/tests/nuxt/render-suspended.spec.ts b/examples/app-vitest-full/tests/nuxt/render-suspended.spec.ts
index 07007f461..0a1bc1f66 100644
--- a/examples/app-vitest-full/tests/nuxt/render-suspended.spec.ts
+++ b/examples/app-vitest-full/tests/nuxt/render-suspended.spec.ts
@@ -23,7 +23,7 @@ import CompostionApi from '~/components/TestComponentWithCompostionApi.vue'
import OptionsApiWithData from '~/components/TestComponentWithOptionsApiWithData.vue'
import OptionsApiWithSetup from '~/components/TestComponentWithOptionsApiWithSetup.vue'
-import { BoundAttrs, OptionsApiComputed, OptionsApiEmits, OptionsApiWatch, ScriptSetupEmits, ScriptSetupWatch } from '#components'
+import { BoundAttrs, OptionsApiComputed, OptionsApiEmits, OptionsApiWatch, ScriptSetupEmits, ScriptSetupWatch, TestTeleport } from '#components'
const formats = {
ExportDefaultComponent,
@@ -398,3 +398,13 @@ it('renders links correctly', async () => {
"
`)
})
+
+it('teleport should work', async () => {
+ expect(document.getElementById('teleport-title')).toBeFalsy()
+
+ const wrapper = await renderSuspended(TestTeleport)
+ expect(document.getElementById('teleport-title')).toBeTruthy()
+
+ wrapper.unmount()
+ expect(document.getElementById('teleport-title')).toBeFalsy()
+})
diff --git a/examples/app-vitest-workspace/app1/nuxt.config.ts b/examples/app-vitest-workspace/app1/nuxt.config.ts
index 5a2ebafa6..79e7eb6c8 100644
--- a/examples/app-vitest-workspace/app1/nuxt.config.ts
+++ b/examples/app-vitest-workspace/app1/nuxt.config.ts
@@ -1,5 +1,20 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
+ app: {
+ rootAttrs: {
+ class: 'nuxt-root-class',
+ style: '',
+ tabindex: 0,
+ spellcheck: true,
+ draggable: 'true', // as ``
+ },
+ teleportAttrs: {
+ class: 'teleport-class',
+ tabindex: -1,
+ spellcheck: false,
+ draggable: true, // as ``
+ },
+ },
compatibilityDate: '2024-04-03',
})
diff --git a/examples/app-vitest-workspace/app1/test/app.nuxt.spec.ts b/examples/app-vitest-workspace/app1/test/app.nuxt.spec.ts
index 8243b759c..318c338ad 100644
--- a/examples/app-vitest-workspace/app1/test/app.nuxt.spec.ts
+++ b/examples/app-vitest-workspace/app1/test/app.nuxt.spec.ts
@@ -9,4 +9,26 @@ describe('app', () => {
// @ts-expect-error injected global, not typed
expect(__NUXT_VITEST_RESOLVED__).toBe(true)
})
+
+ it('should exist the app root element', () => {
+ const element = document.getElementById('__nuxt')
+ expect(element).toBeTruthy()
+ expect(element?.tagName).toBe('DIV')
+ expect(element?.getAttribute('class')).toBe('nuxt-root-class')
+ expect(element?.getAttribute('style')).toBe('')
+ expect(element?.getAttribute('tabindex')).toBe('0')
+ expect(element?.getAttribute('spellcheck')).toBe('')
+ expect(element?.getAttribute('draggable')).toBe('true')
+ })
+
+ it('should exist the app teleport element', () => {
+ const element = document.getElementById('teleports')
+ expect(element).toBeTruthy()
+ expect(element?.tagName).toBe('DIV')
+ expect(element?.getAttribute('class')).toBe('teleport-class')
+ expect(element?.getAttribute('style')).toBe(null)
+ expect(element?.getAttribute('tabindex')).toBe('-1')
+ expect(element?.getAttribute('spellcheck')).toBe(null)
+ expect(element?.getAttribute('draggable')).toBe('')
+ })
})
diff --git a/examples/app-vitest-workspace/app2/nuxt.config.ts b/examples/app-vitest-workspace/app2/nuxt.config.ts
index c154ef59d..55fe9a6f8 100644
--- a/examples/app-vitest-workspace/app2/nuxt.config.ts
+++ b/examples/app-vitest-workspace/app2/nuxt.config.ts
@@ -3,5 +3,10 @@ export default defineNuxtConfig({
modules: ['@nuxtjs/color-mode', '@nuxt/ui'],
pages: false,
devtools: { enabled: true },
+ app: {
+ rootAttrs: {
+ id: undefined,
+ },
+ },
compatibilityDate: '2024-04-03',
})
diff --git a/examples/app-vitest-workspace/app2/test/nuxt/app.spec.ts b/examples/app-vitest-workspace/app2/test/nuxt/app.spec.ts
index 33b79310f..3be02e355 100644
--- a/examples/app-vitest-workspace/app2/test/nuxt/app.spec.ts
+++ b/examples/app-vitest-workspace/app2/test/nuxt/app.spec.ts
@@ -10,4 +10,10 @@ describe('app', () => {
it('useRuntimeConfig', () => {
expect(useRuntimeConfig().icon).toBeDefined()
})
+
+ it('should exist the app root element', () => {
+ const element = document.getElementById('nuxt-test')
+ expect(element).toBeTruthy()
+ expect(element?.tagName).toBe('DIV')
+ })
})
diff --git a/examples/app-vitest-workspace/app3/test/nuxt/app.spec.ts b/examples/app-vitest-workspace/app3/test/nuxt/app.spec.ts
new file mode 100644
index 000000000..9c9ea28aa
--- /dev/null
+++ b/examples/app-vitest-workspace/app3/test/nuxt/app.spec.ts
@@ -0,0 +1,15 @@
+import { describe, expect, it } from 'vitest'
+
+describe('app', () => {
+ it('should exist the app root element', () => {
+ const element = document.getElementById('__test_root')
+ expect(element).toBeTruthy()
+ expect(element?.tagName).toBe('MAIN')
+ })
+
+ it('should exist the app teleport element', () => {
+ const element = document.getElementById('__test_teleport')
+ expect(element).toBeTruthy()
+ expect(element?.tagName).toBe('P')
+ })
+})
diff --git a/examples/app-vitest-workspace/app3/vitest.config.ts b/examples/app-vitest-workspace/app3/vitest.config.ts
index e11c7d6d8..329ab52a2 100644
--- a/examples/app-vitest-workspace/app3/vitest.config.ts
+++ b/examples/app-vitest-workspace/app3/vitest.config.ts
@@ -7,6 +7,18 @@ export default defineVitestProject({
environmentOptions: {
nuxt: {
rootDir: fileURLToPath(new URL('.', import.meta.url)),
+ overrides: {
+ app: {
+ rootAttrs: {
+ id: '__test_root',
+ },
+ rootTag: 'main',
+ teleportAttrs: {
+ id: '__test_teleport',
+ },
+ teleportTag: 'p',
+ },
+ },
},
},
},
diff --git a/src/config.ts b/src/config.ts
index d7f5a011f..a7596cf04 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -150,19 +150,27 @@ export async function getVitestConfigFromNuxt(
},
test: {
environmentOptions: {
- nuxtRuntimeConfig: applyEnv(structuredClone(options.nuxt.options.runtimeConfig), {
- prefix: 'NUXT_',
- env: await setupDotenv(defu(loadNuxtOptions.dotenv, {
- cwd: rootDir,
- fileName: '.env.test',
- })),
- }),
- nuxtRouteRules: defu(
- {},
- options.nuxt.options.routeRules,
- options.nuxt.options.nitro?.routeRules,
- ),
- },
+ nuxtConfig: {
+ runtimeConfig: applyEnv(structuredClone(options.nuxt.options.runtimeConfig), {
+ prefix: 'NUXT_',
+ env: await setupDotenv(defu(loadNuxtOptions.dotenv, {
+ cwd: rootDir,
+ fileName: '.env.test',
+ })),
+ }),
+ routeRules: defu(
+ {},
+ options.nuxt.options.routeRules,
+ options.nuxt.options.nitro?.routeRules,
+ ),
+ app: {
+ rootAttrs: options.nuxt.options.app.rootAttrs,
+ rootTag: options.nuxt.options.app.rootTag,
+ teleportAttrs: options.nuxt.options.app.teleportAttrs,
+ teleportTag: options.nuxt.options.app.teleportTag,
+ },
+ },
+ } satisfies Omit,
server: {
deps: {
inline: [
@@ -225,14 +233,14 @@ export async function getVitestConfigFromNuxt(
test: {
environmentOptions: {
nuxt: {
- rootId: options.nuxt.options.app.rootId || undefined,
+ rootId: options.nuxt.options.app.rootAttrs?.id || undefined,
h3Version: h3Info?.version?.startsWith('2.') ? 2 : 1,
mock: {
intersectionObserver: true,
indexedDb: false,
},
},
- },
+ } satisfies NuxtEnvironmentResolvedOptions,
} satisfies VitestConfig,
},
) as ViteUserConfig & { test: VitestConfig }
@@ -405,6 +413,7 @@ export interface NuxtEnvironmentOptions {
/**
* The id of the root div to which the app should be mounted. You should also set `app.rootId` to the same value.
* @default 'nuxt-test'
+ * @deprecated Prefer `overrides.app.rootAttrs.id` instead
*/
rootId?: string
/**
@@ -423,6 +432,21 @@ export interface NuxtEnvironmentOptions {
}
}
+/**
+ * @internal
+ */
+export interface NuxtEnvironmentResolvedOptions {
+ nuxt: NuxtEnvironmentOptions
+ nuxtConfig?: {
+ app: Pick<
+ NonNullable,
+ 'rootAttrs' | 'rootTag' | 'teleportTag' | 'teleportAttrs'
+ >
+ runtimeConfig: NuxtConfig['runtimeConfig']
+ routeRules: NuxtConfig['routeRules']
+ }
+}
+
declare module 'vitest/node' {
interface EnvironmentOptions {
nuxt?: NuxtEnvironmentOptions
diff --git a/src/runtime/shared/environment.ts b/src/runtime/shared/environment.ts
index 838eeab3e..397450e37 100644
--- a/src/runtime/shared/environment.ts
+++ b/src/runtime/shared/environment.ts
@@ -3,19 +3,20 @@ import { joinURL } from 'ufo'
import { defineEventHandler } from './h3.ts'
import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from 'radix3'
import type { NuxtWindow } from '../../vitest-environment.ts'
-import type { NuxtEnvironmentOptions } from '../../config.ts'
+import type { NuxtEnvironmentResolvedOptions } from '../../config.ts'
import { createFetchForH3V1 } from './h3-v1.ts'
import { createFetchForH3V2 } from './h3-v2.ts'
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export async function setupWindow(win: NuxtWindow, environmentOptions: { nuxt: NuxtEnvironmentOptions, nuxtRuntimeConfig?: Record, nuxtRouteRules?: Record }) {
+export async function setupWindow(win: NuxtWindow, environmentOptions: NuxtEnvironmentResolvedOptions) {
+ const nuxtConfig = environmentOptions.nuxtConfig
+
win.__NUXT_VITEST_ENVIRONMENT__ = true
win.__NUXT__ = {
serverRendered: false,
config: {
public: {},
app: { baseURL: '/' },
- ...environmentOptions?.nuxtRuntimeConfig,
+ ...nuxtConfig?.runtimeConfig,
},
data: {},
state: {},
@@ -29,10 +30,11 @@ export async function setupWindow(win: NuxtWindow, environmentOptions: { nuxt: N
return consoleInfo(...args)
}
- const app = win.document.createElement('div')
- // this is a workaround for a happy-dom bug with ids beginning with _
- app.id = environmentOptions.nuxt.rootId || 'nuxt-test'
- win.document.body.appendChild(app)
+ createElementAndAppend(win, nuxtConfig?.app.rootTag || 'div', {
+ ...nuxtConfig?.app.rootAttrs,
+ id: environmentOptions.nuxt.rootId || 'nuxt-test',
+ })
+ createElementAndAppend(win, nuxtConfig?.app.teleportTag || 'div', nuxtConfig?.app.teleportAttrs)
if (!win.fetch || !('Request' in win)) {
await import('node-fetch-native/polyfill')
@@ -66,12 +68,12 @@ export async function setupWindow(win: NuxtWindow, environmentOptions: { nuxt: N
// App manifest support
const timestamp = Date.now()
const routeRulesMatcher = toRouteMatcher(
- createRadixRouter({ routes: environmentOptions.nuxtRouteRules || {} }),
+ createRadixRouter({ routes: nuxtConfig?.routeRules || {} }),
)
const matcher = exportMatcher(routeRulesMatcher)
const manifestOutputPath = joinURL(
- environmentOptions?.nuxtRuntimeConfig?.app?.baseURL || '/',
- environmentOptions?.nuxtRuntimeConfig?.app?.buildAssetsDir || '_nuxt',
+ nuxtConfig?.runtimeConfig?.app?.baseURL || '/',
+ nuxtConfig?.runtimeConfig?.app?.buildAssetsDir || '_nuxt',
'builds',
)
const manifestBaseRoutePath = joinURL('/_', manifestOutputPath)
@@ -103,3 +105,24 @@ export async function setupWindow(win: NuxtWindow, environmentOptions: { nuxt: N
console.info = consoleInfo
}
}
+
+function createElementAndAppend(
+ win: NuxtWindow,
+ tag: string,
+ attrs: NonNullable['app']['rootAttrs']
+ | NonNullable['app']['teleportAttrs']
+ | undefined,
+) {
+ if (attrs?.id && win.document.getElementById(attrs.id)) {
+ return
+ }
+
+ const element = win.document.createElement(tag)
+ for (const [key, value] of Object.entries(attrs ?? {})) {
+ if (value !== false && value != null) {
+ element.setAttribute(key, value === true ? '' : String(value))
+ }
+ }
+
+ win.document.body.appendChild(element)
+}
diff --git a/tsconfig.json b/tsconfig.json
index fae88a927..139610e7d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,7 +2,8 @@
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"moduleResolution": "Bundler",
- "allowImportingTsExtensions": true
+ "allowImportingTsExtensions": true,
+ "stripInternal": true,
},
"exclude": [
"config.d.ts",