Target Page
+ +This page demonstrates async data loading with suspense.
+diff --git a/test/e2e/runtime-compiler.test.ts b/test/e2e/runtime-compiler.test.ts
new file mode 100644
index 0000000000..c347ddec07
--- /dev/null
+++ b/test/e2e/runtime-compiler.test.ts
@@ -0,0 +1,149 @@
+import { fileURLToPath } from 'node:url'
+import { isWindows } from 'std-env'
+import { join } from 'pathe'
+import { expect, test } from './test-utils'
+
+/**
+ * This test suite verifies that Vue's runtime compiler works correctly within Nuxt,
+ * testing various ways of using runtime-compiled components across multiple pages.
+ */
+
+const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack'
+const isDev = process.env.TEST_ENV === 'dev'
+
+const fixtureDir = fileURLToPath(new URL('../fixtures/runtime-compiler', import.meta.url))
+
+// Run tests in parallel in production mode, but serially in dev mode
+// to avoid interference between HMR and test execution
+test.describe.configure({ mode: isDev ? 'serial' : 'parallel' })
+
+test.use({
+ nuxt: {
+ rootDir: fixtureDir,
+ dev: isDev,
+ server: true,
+ browser: true,
+ setupTimeout: (isWindows ? 360 : 120) * 1000,
+ nuxtConfig: {
+ builder: isWebpack ? 'webpack' : 'vite',
+ buildDir: isDev ? join(fixtureDir, '.nuxt', 'test', Math.random().toString(36).slice(2, 8)) : undefined,
+ },
+ },
+})
+
+test.describe('Runtime compiler functionality', () => {
+ /**
+ * Tests that the overview page loads without errors
+ */
+ test('should render the overview page without errors', async ({ page, goto }) => {
+ await goto('/')
+ await expect(page.getByTestId('page-title')).toHaveText('Nuxt Runtime Compiler Tests')
+ expect(page).toHaveNoErrorsOrWarnings()
+ })
+
+ /**
+ * Tests the basic component with template string
+ */
+ test('should render HelloWorld.vue with template string via runtime compiler', async ({ page, goto }) => {
+ await goto('/basic-component')
+
+ await expect(page.getByTestId('hello-world')).toHaveText('hello, Helloworld.vue here !')
+ expect(page).toHaveNoErrorsOrWarnings()
+ })
+
+ /**
+ * Tests the component with computed template
+ */
+ test('should render and update ComponentDefinedInSetup with reactive template', async ({ page, goto }) => {
+ await goto('/component-in-setup')
+
+ // Check initial render
+ await expect(page.getByTestId('component-defined-in-setup')).toContainText('hello i am defined in the setup of app.vue')
+ await expect(page.getByTestId('computed-count')).toHaveText('0')
+
+ // Update counter
+ await page.getByTestId('increment-count').click()
+
+ // Check updated template
+ await expect(page.getByTestId('computed-count')).toHaveText('1')
+
+ // Multiple updates
+ await page.getByTestId('increment-count').click()
+ await expect(page.getByTestId('computed-count')).toHaveText('2')
+
+ expect(page).toHaveNoErrorsOrWarnings()
+ })
+
+ /**
+ * Tests the TypeScript component with render function
+ */
+ test('should render Name.ts component using render function', async ({ page, goto }) => {
+ await goto('/typescript-component')
+
+ await expect(page.getByTestId('name')).toHaveText('I am the Name.ts component')
+ expect(page).toHaveNoErrorsOrWarnings()
+ })
+
+ /**
+ * Tests a component with template from API
+ */
+ test('should render ShowTemplate component with template from API', async ({ page, goto }) => {
+ await goto('/api-template')
+
+ const expectedText = 'Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API'
+ await expect(page.getByTestId('show-template')).toHaveText(expectedText)
+ expect(page).toHaveNoErrorsOrWarnings()
+ })
+
+ /**
+ * Tests a fully dynamic component with both template and script from API
+ */
+ test('should render and update Interactive component with template and script from API', async ({ page, goto }) => {
+ await goto('/full-dynamic')
+
+ // Check initial render
+ await expect(page.getByTestId('interactive')).toContainText('I am defined by Interactive in the setup of App.vue')
+ await expect(page.getByTestId('interactive')).toContainText('my name is Doe John')
+
+ // Test reactivity
+ const button = page.getByTestId('inc-interactive-count')
+ await button.click()
+ await expect(page.getByTestId('interactive-count')).toHaveText('1')
+
+ // Test continued reactivity
+ await button.click()
+ await expect(page.getByTestId('interactive-count')).toHaveText('2')
+
+ expect(page).toHaveNoErrorsOrWarnings()
+ })
+
+ /**
+ * Tests navigation between pages and verifies all components are reachable
+ */
+ test('should allow navigation between all test cases', async ({ page, goto }) => {
+ await goto('/')
+
+ // Navigate to each page and verify
+ const pages = [
+ { path: '/basic-component', text: 'Basic Component Test' },
+ { path: '/component-in-setup', text: 'Computed Template Test' },
+ { path: '/typescript-component', text: 'TypeScript Component Test' },
+ { path: '/api-template', text: 'API Template Test' },
+ { path: '/full-dynamic', text: 'Full Dynamic Component Test' },
+ ]
+
+ for (const { path, text } of pages) {
+ // Click navigation link
+ await page.getByRole('link', { name: new RegExp(text.split(' ')[0]!, 'i') }).click()
+
+ // Verify page title
+ await expect(page.locator('h2')).toContainText(text)
+
+ // Check URL
+ expect(page.url()).toContain(path)
+
+ // Verify no errors
+ expect(page).toHaveNoErrorsOrWarnings()
+ }
+ })
+})
diff --git a/test/e2e/suspense.test.ts b/test/e2e/suspense.test.ts
new file mode 100644
index 0000000000..f90e74770a
--- /dev/null
+++ b/test/e2e/suspense.test.ts
@@ -0,0 +1,74 @@
+import { fileURLToPath } from 'node:url'
+import { isWindows } from 'std-env'
+import { join } from 'pathe'
+import { expect, test } from './test-utils'
+
+/**
+ * This test suite verifies that Nuxt's suspense integration works correctly,
+ * testing navigation between pages with suspense boundaries.
+ */
+
+const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack'
+const isDev = process.env.TEST_ENV === 'dev'
+
+const fixtureDir = fileURLToPath(new URL('../fixtures/suspense', import.meta.url))
+
+// Run tests in parallel in production mode, but serially in dev mode
+test.describe.configure({ mode: isDev ? 'serial' : 'parallel' })
+
+test.use({
+ nuxt: {
+ rootDir: fixtureDir,
+ dev: isDev,
+ server: true,
+ browser: true,
+ setupTimeout: (isWindows ? 360 : 120) * 1000,
+ nuxtConfig: {
+ builder: isWebpack ? 'webpack' : 'vite',
+ buildDir: isDev ? join(fixtureDir, '.nuxt', 'test', Math.random().toString(36).slice(2, 8)) : undefined,
+ },
+ },
+})
+
+test.describe('Suspense multiple navigation', () => {
+ test('should not throw error during multiple rapid navigation', async ({ page, goto }) => {
+ // Navigate to the index page
+ await goto('/')
+
+ // Verify initial state
+ await expect(page.getByTestId('btn-a')).toHaveText(' Target A ')
+
+ // Navigate to target page using button A
+ await page.getByTestId('btn-a').click()
+ await page.waitForFunction(() => window.useNuxtApp?.()._route.path === '/target')
+
+ // Verify content after navigation
+ await expect(page.getByTestId('content')).toContainText('Hello a')
+
+ // Go back to index page
+ await page.goBack()
+ await page.waitForFunction(() => window.useNuxtApp?.()._route.path === '/')
+
+ // Verify back at index page
+ await expect(page.getByTestId('index-title')).toBeVisible()
+
+ // Test multiple rapid navigation (clicking both buttons before first navigation completes)
+ await Promise.all([
+ page.getByTestId('btn-a').click(),
+ page.getByTestId('btn-b').click(),
+ page.getByTestId('btn-a').click(),
+ page.getByTestId('btn-b').click(),
+ page.getByTestId('btn-a').click(),
+ page.getByTestId('btn-b').click(),
+ page.getByTestId('btn-a').click(),
+ page.getByTestId('btn-b').click(),
+ ])
+
+ // Verify we reached the target page with the correct content (from the second navigation)
+ await page.waitForFunction(() => window.useNuxtApp?.()._route.path === '/target')
+ await expect(page.getByTestId('content')).toContainText('Hello b')
+
+ // Verify no errors or warnings occurred
+ expect(page).toHaveNoErrorsOrWarnings()
+ })
+})
diff --git a/test/fixtures/runtime-compiler/layouts/default.vue b/test/fixtures/runtime-compiler/layouts/default.vue
new file mode 100644
index 0000000000..8351932f1b
--- /dev/null
+++ b/test/fixtures/runtime-compiler/layouts/default.vue
@@ -0,0 +1,176 @@
+
+
+ This test demonstrates using the runtime compiler with a template string
+ that is fetched from an API endpoint. This is useful when templates need to be
+ dynamically loaded or managed outside the application.
+
+ This test demonstrates rendering a basic Vue component with a template string
+ using
+ This test demonstrates creating a component in setup with a computed template
+ that reacts to state changes. When the count changes, the component template
+ is regenerated with the new value.
+
+ This test demonstrates creating a fully dynamic component where both
+ template and script logic are fetched from an API endpoint. This approach
+ enables completely runtime-defined components with reactive behavior.
+
+ Click the "click here" button in the component above to test reactivity.
+ The counter should increment, demonstrating that the dynamic script is
+ properly executed and reactive.
+
+ When using this approach in production, always sanitize templates and scripts
+ from external sources. Using new Function() with untrusted content can lead to
+ security vulnerabilities including code injection.
+ This test suite verifies that Vue's Runtime Compiler works correctly within Nuxt, testing various ways of using runtime-compiled components. Tests a basic Vue component with a template string using defineNuxtComponent. Tests a component defined in setup with a computed template string that updates reactively. Tests a TypeScript component using runtime compiler with a render function. Tests a component that loads its template from an API endpoint. Tests a component with both template and setup script loaded from an API endpoint. Each test case demonstrates a different use case of the runtime compiler. Feel free to use this as a reference for implementing your own runtime-compiled components.
+ This test demonstrates using the runtime compiler with a TypeScript component
+ that defines a template as a prop and renders it using a render function.
+
+ This approach is useful when you need to create components programmatically
+ in TypeScript with strong type checking and don't want to use string templates directly.
+
+ Nuxt Runtime Compiler Tests
+
+
+ API Template Test
+
+ Component Output:
+ API Response (Template):
+
+
+ {{ data!.templateString }}
Implementation:
+
+
+
+// ShowTemplate.vue
+export default defineNuxtComponent({
+ props: {
+ template: {
+ required: true,
+ type: String,
+ },
+ name: {
+ type: String,
+ default: () => '(missing name prop)',
+ },
+ },
+ setup (props) {
+ const showIt = h({
+ template: props.template,
+ props: {
+ name: {
+ type: String,
+ default: () => '(missing name prop)',
+ },
+ },
+ })
+ return {
+ showIt,
+ }
+ },
+})
+
+// API Endpoint (server/api/template.get.ts)
+export default defineEventHandler(() => {
+ return '<div data-testid="template-content">Hello my name is : {\{ name }}, i am defined by ShowTemplate.vue and my template is retrieved from the API</div>'
+})
+
Basic Component Test
+
+ defineNuxtComponent
. The template is defined directly in the
+ component file without requiring separate compilation.
+ Component Output:
+ Implementation:
+
+
+// Helloworld.vue
+export default defineNuxtComponent({
+ template: '<div>hello, Helloworld.vue here ! </div>',
+})
+
Computed Template Test
+
+ Component Output:
+ Implementation:
+
+
+const count = ref(0)
+
+// Component with a computed template that updates when count changes
+const ComponentDefinedInSetup = computed(() => defineComponent({
+ template: `
+<div class="border">
+ <div>hello i am defined in the setup of app.vue</div>
+ <div>This component template is in a computed refreshed on count</div>
+ count: <span data-testid="computed-count">${count.value}</span>.
+ I don't recommend doing this for performance reasons; prefer passing props for mutable data.
+</div>`,
+}))
+
Full Dynamic Component Test
+
+ Component Output:
+ API Response (Component Definition):
+
+
+ {{ JSON.stringify(data!.interactiveComponent, null, 2) }}
Interactive Test
+ Implementation:
+
+
+
+// In your page/component
+const { data } = await useAsyncData('interactiveComponent', async () => {
+ const interactiveComponent = await $fetch('/api/full-component')
+ return { interactiveComponent }
+})
+
+const Interactive = defineComponent({
+ props: data.value?.interactiveComponent.props,
+ setup(props) {
+ return new Function(
+ 'ref',
+ 'computed',
+ 'props',
+ data.value?.interactiveComponent.setup ?? '',
+ )(ref, computed, props)
+ },
+ template: data.value?.interactiveComponent.template,
+})
+
+// API Endpoint (server/api/full-component.get.ts)
+export default defineEventHandler(() => {
+ return {
+ props: ['lastname', 'firstname'],
+ setup: `
+ const fullName = computed(() => props.lastname + ' ' + props.firstname);
+ const count = ref(0);
+ return {fullName, count}
+ `,
+ template: '<div>my name is {\{ fullName }}, <button data-testid="inc-interactive-count" @click="count++">click here</button> count: <span data-testid="interactive-count">{\{ count }}</span>. I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api </div>',
+ }
+})
+
Security Note
+ Welcome to the Nuxt Runtime Compiler Test Suite
+
+ Test Cases
+
+
+
+
+ For Developers
+ TypeScript Component Test
+
+ Component Output:
+ Implementation:
+
+
+
+// Name.ts
+export default defineNuxtComponent({
+ props: ['template', 'name'],
+ /**
+ * Most of the time, Vue compiler needs at least a VNode, use h() to render the component
+ */
+ render () {
+ return h({
+ props: ['name'],
+ template: this.template,
+ }, {
+ name: this.name,
+ })
+ },
+})
+
Note
+
+ This test verifies that Nuxt's suspense functionality works correctly when rapidly navigating + between pages. It demonstrates that multiple rapid navigations don't cause errors or unexpected behavior. +
++ Click both buttons in rapid succession to verify that suspense handles + multiple navigations correctly. The final content should reflect the last + navigation (Target B). +
+This page demonstrates async data loading with suspense.
+