test: migrate runtime compiler test to playwright (+ add test cases) (#31405)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Daniel Roe 2025-03-17 16:59:22 +00:00
parent db4daa02a3
commit 7fd75f54ad
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
15 changed files with 1228 additions and 241 deletions

View File

@ -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()
}
})
})

74
test/e2e/suspense.test.ts Normal file
View File

@ -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()
})
})

View File

@ -0,0 +1,176 @@
<template>
<div>
<header>
<h1 data-testid="page-title">
Nuxt Runtime Compiler Tests
</h1>
<nav>
<ul>
<li>
<NuxtLink
to="/"
active-class="active"
>
Overview
</NuxtLink>
</li>
<li>
<NuxtLink
to="/basic-component"
active-class="active"
>
Basic Component
</NuxtLink>
</li>
<li>
<NuxtLink
to="/component-in-setup"
active-class="active"
>
Computed Template
</NuxtLink>
</li>
<li>
<NuxtLink
to="/typescript-component"
active-class="active"
>
TypeScript Component
</NuxtLink>
</li>
<li>
<NuxtLink
to="/api-template"
active-class="active"
>
API Template
</NuxtLink>
</li>
<li>
<NuxtLink
to="/full-dynamic"
active-class="active"
>
Full Dynamic Component
</NuxtLink>
</li>
</ul>
</nav>
</header>
<main>
<slot />
</main>
<footer>
<p>
<small>These tests verify Vue's runtime compiler works correctly in Nuxt</small>
</p>
</footer>
</div>
</template>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
margin: 0;
padding: 0;
color: #222;
background: #fafafa;
}
header {
background-color: #00DC82;
color: #003543;
padding: 1rem;
margin-bottom: 1.5rem;
}
h1 {
margin-top: 0;
margin-bottom: 1rem;
}
nav ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
flex-wrap: wrap;
gap: 0.5rem;
}
nav li a {
display: block;
padding: 0.5rem 1rem;
text-decoration: none;
color: #003543;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 4px;
transition: all 0.2s ease;
}
nav li a:hover {
background-color: rgba(255, 255, 255, 0.4);
}
nav li a.active {
background-color: #003543;
color: white;
}
main {
max-width: 800px;
margin: 0 auto;
padding: 0 1rem 2rem;
}
.test-component {
margin-bottom: 2rem;
padding: 1.5rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.test-component h2 {
margin-top: 0;
color: #003543;
}
.test-description {
padding: 0.75rem;
background: #f0f9f6;
border-left: 4px solid #00DC82;
margin-bottom: 1.5rem;
}
.border {
border: 1px solid burlywood;
padding: 1rem;
background: #fffaf0;
border-radius: 4px;
}
button {
background: #00DC82;
color: #003543;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
button:hover {
background: #00c476;
}
footer {
text-align: center;
padding: 1rem;
color: #666;
border-top: 1px solid #eee;
margin-top: 2rem;
}
</style>

View File

@ -0,0 +1,131 @@
<script setup lang="ts">
/**
* Test case: API Template Component
*
* This demonstrates using the runtime compiler with a template
* fetched from an API endpoint.
*/
const { data, pending } = await useAsyncData('templateString', async () => {
const templateString = await $fetch('/api/template')
return { templateString }
})
</script>
<template>
<div>
<div class="test-component">
<h2>API Template Test</h2>
<div class="test-description">
<p>
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.
</p>
</div>
<div
v-if="pending"
class="loading"
>
Loading template from API...
</div>
<template v-else>
<h3>Component Output:</h3>
<div class="component-display">
<show-template
data-testid="show-template"
:template="data!.templateString"
name="John"
/>
</div>
<h3>API Response (Template):</h3>
<pre class="api-response"><code>{{ data!.templateString }}</code></pre>
<h3>Implementation:</h3>
<pre><code>
// 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 '&lt;div data-testid="template-content"&gt;Hello my name is : {\{ name }}, i am defined by ShowTemplate.vue and my template is retrieved from the API&lt;/div&gt;'
})
</code></pre>
</template>
</div>
</div>
</template>
<style scoped>
.component-display {
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 6px;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
}
.loading {
text-align: center;
padding: 2rem;
color: #6c757d;
font-style: italic;
}
.api-response {
background-color: #e9f5ff;
color: #0366d6;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9rem;
margin-bottom: 1.5rem;
white-space: pre-wrap;
word-break: break-word;
}
pre {
background-color: #2d2d2d;
color: #f8f8f2;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
}
code {
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
}
</style>

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
/**
* Test case: Basic Component
*
* This demonstrates using a basic component with a template string
* via defineNuxtComponent.
*/
import HelloWorld from '../components/Helloworld.vue'
</script>
<template>
<div>
<div class="test-component">
<h2>Basic Component Test</h2>
<div class="test-description">
<p>
This test demonstrates rendering a basic Vue component with a template string
using <code>defineNuxtComponent</code>. The template is defined directly in the
component file without requiring separate compilation.
</p>
</div>
<h3>Component Output:</h3>
<div class="component-display">
<HelloWorld data-testid="hello-world" />
</div>
<h3>Implementation:</h3>
<pre><code>
// Helloworld.vue
export default defineNuxtComponent({
template: '&lt;div&gt;hello, Helloworld.vue here ! &lt;/div&gt;',
})
</code></pre>
</div>
</div>
</template>
<style scoped>
.component-display {
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 6px;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
}
pre {
background-color: #2d2d2d;
color: #f8f8f2;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9rem;
line-height: 1.5;
}
code {
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
}
</style>

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
/**
* Test case: Component defined in setup with computed template
*
* This demonstrates defining a component with a computed template string
* that updates reactively when state changes.
*/
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>`,
}))
</script>
<template>
<div>
<div class="test-component">
<h2>Computed Template Test</h2>
<div class="test-description">
<p>
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.
</p>
</div>
<h3>Component Output:</h3>
<div class="component-display">
<ComponentDefinedInSetup data-testid="component-defined-in-setup" />
</div>
<div class="controls">
<button
data-testid="increment-count"
@click="count++"
>
Increment Count: {{ count }}
</button>
</div>
<h3>Implementation:</h3>
<pre><code>
const count = ref(0)
// Component with a computed template that updates when count changes
const ComponentDefinedInSetup = computed(() => defineComponent({
template: `
&lt;div class="border"&gt;
&lt;div&gt;hello i am defined in the setup of app.vue&lt;/div&gt;
&lt;div&gt;This component template is in a computed refreshed on count&lt;/div&gt;
count: &lt;span data-testid="computed-count"&gt;${count.value}&lt;/span&gt;.
I don't recommend doing this for performance reasons; prefer passing props for mutable data.
&lt;/div&gt;`,
}))
</code></pre>
</div>
</div>
</template>
<style scoped>
.component-display {
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 6px;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
}
.controls {
margin-bottom: 1.5rem;
}
pre {
background-color: #2d2d2d;
color: #f8f8f2;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
}
code {
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
}
</style>

View File

@ -0,0 +1,193 @@
<script setup lang="ts">
/**
* Test case: Full Dynamic Component
*
* This demonstrates using the runtime compiler with both template and script
* fetched from an API endpoint, creating a fully dynamic component.
*/
const { data, pending } = 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,
})
</script>
<template>
<div>
<div class="test-component">
<h2>Full Dynamic Component Test</h2>
<div class="test-description">
<p>
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.
</p>
</div>
<div
v-if="pending"
class="loading"
>
Loading component definition from API...
</div>
<template v-else>
<h3>Component Output:</h3>
<div class="component-display">
<Interactive
data-testid="interactive"
lastname="Doe"
firstname="John"
/>
</div>
<h3>API Response (Component Definition):</h3>
<pre class="api-response"><code>{{ JSON.stringify(data!.interactiveComponent, null, 2) }}</code></pre>
<div class="test-instructions">
<h4>Interactive Test</h4>
<p>
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.
</p>
</div>
<h3>Implementation:</h3>
<pre><code>
// 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: '&lt;div&gt;my name is {\{ fullName }}, &lt;button data-testid="inc-interactive-count" @click="count++"&gt;click here&lt;/button&gt; count: &lt;span data-testid="interactive-count"&gt;{\{ count }}&lt;/span&gt;. I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api &lt;/div&gt;',
}
})
</code></pre>
<div class="note">
<h4>Security Note</h4>
<p>
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.
</p>
</div>
</template>
</div>
</div>
</template>
<style scoped>
.component-display {
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 6px;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
}
.loading {
text-align: center;
padding: 2rem;
color: #6c757d;
font-style: italic;
}
.api-response {
background-color: #e9f5ff;
color: #0366d6;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9rem;
margin-bottom: 1.5rem;
}
.test-instructions {
background-color: #f0f9ff;
border-left: 4px solid #0ea5e9;
padding: 0.75rem;
margin-bottom: 1.5rem;
}
.test-instructions h4 {
margin-top: 0;
color: #0369a1;
margin-bottom: 0.5rem;
}
.test-instructions p {
margin: 0;
}
pre {
background-color: #2d2d2d;
color: #f8f8f2;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
}
code {
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
}
.note {
background-color: #fff1f2;
border-left: 4px solid #f43f5e;
padding: 0.75rem;
margin-top: 1.5rem;
}
.note h4 {
margin-top: 0;
color: #be123c;
margin-bottom: 0.5rem;
}
.note p {
margin: 0;
}
</style>

View File

@ -1,80 +1,87 @@
<script setup lang="ts">
import type { Component } from 'vue'
import Helloworld from '../components/Helloworld.vue'
const count = ref(0)
const compTemplate = computed(() => `
<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 class="count">${count.value}</span>.
I dont recommend you to do this for performance issue, prefer passing props for mutable data.
</div>`,
)
const ComponentDefinedInSetup = computed(() => h({
template: compTemplate.value,
}) as Component)
const { data, pending } = await useAsyncData('templates', async () => {
const [interactiveComponent, templateString] = await Promise.all([
$fetch('/api/full-component'),
$fetch('/api/template'),
])
return {
interactiveComponent,
templateString,
}
}, {})
const Interactive = h({
template: data.value?.interactiveComponent.template,
setup (props) {
return new Function(
'ref',
'computed',
'props',
data.value?.interactiveComponent.setup ?? '',
)(ref, computed, props)
},
props: data.value?.interactiveComponent.props,
}) as Component
/**
* Overview page for the runtime compiler tests
* Explains the purpose of each test case
*/
</script>
<template>
<!-- Edit this file to play around with Nuxt but never commit changes! -->
<div>
<Helloworld id="hello-world" />
<ComponentDefinedInSetup id="component-defined-in-setup" />
<button
id="increment-count"
@click="count++"
>
{{ count }}
</button>
<template v-if="!pending">
<Name
id="name"
template="<div>I am the Name.ts component</div>"
/>
<show-template
id="show-template"
:template="data?.templateString ?? ''"
name="John"
/>
<Interactive
id="interactive"
lastname="Doe"
firstname="John"
/>
</template>
<div class="test-component">
<h2>Welcome to the Nuxt Runtime Compiler Test Suite</h2>
<div class="test-description">
<p>This test suite verifies that Vue's Runtime Compiler works correctly within Nuxt, testing various ways of using runtime-compiled components.</p>
</div>
<h3>Test Cases</h3>
<ul class="test-case-list">
<li>
<strong>Basic Component</strong>
<p>Tests a basic Vue component with a template string using defineNuxtComponent.</p>
</li>
<li>
<strong>Computed Template</strong>
<p>Tests a component defined in setup with a computed template string that updates reactively.</p>
</li>
<li>
<strong>TypeScript Component</strong>
<p>Tests a TypeScript component using runtime compiler with a render function.</p>
</li>
<li>
<strong>API Template</strong>
<p>Tests a component that loads its template from an API endpoint.</p>
</li>
<li>
<strong>Full Dynamic Component</strong>
<p>Tests a component with both template and setup script loaded from an API endpoint.</p>
</li>
</ul>
<div class="note">
<h4>For Developers</h4>
<p>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.</p>
</div>
</div>
</div>
</template>
<style>
.border {
border: 1px solid burlywood;
<style scoped>
.test-case-list {
margin: 1.5rem 0;
padding-left: 1.5rem;
}
.test-case-list li {
margin-bottom: 1rem;
}
.test-case-list strong {
color: #00947e;
}
.test-case-list p {
margin: 0.25rem 0 0;
}
.note {
background-color: #fffde7;
border-left: 4px solid #ffd54f;
padding: 0.75rem;
margin-top: 2rem;
}
.note h4 {
margin-top: 0;
color: #f57c00;
}
.note p {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,100 @@
<script setup lang="ts">
/**
* Test case: TypeScript Component
*
* This demonstrates using the runtime compiler with a TypeScript component
* that uses a render function approach.
*/
</script>
<template>
<div>
<div class="test-component">
<h2>TypeScript Component Test</h2>
<div class="test-description">
<p>
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.
</p>
</div>
<h3>Component Output:</h3>
<div class="component-display">
<Name
data-testid="name"
template="<div>I am the Name.ts component</div>"
/>
</div>
<h3>Implementation:</h3>
<pre><code>
// 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,
})
},
})
</code></pre>
<div class="note">
<h4>Note</h4>
<p>
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.
</p>
</div>
</div>
</div>
</template>
<style scoped>
.component-display {
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 6px;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
}
pre {
background-color: #2d2d2d;
color: #f8f8f2;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9rem;
line-height: 1.5;
}
code {
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
}
.note {
background-color: #fffde7;
border-left: 4px solid #ffd54f;
padding: 0.75rem;
margin-top: 1.5rem;
}
.note h4 {
margin-top: 0;
color: #f57c00;
margin-bottom: 0.5rem;
}
.note p {
margin: 0;
}
</style>

View File

@ -7,12 +7,12 @@ export default defineEventHandler(() => {
props: ['lastname', 'firstname'],
// don't forget to sanitize
setup: `
const fullName = computed(() => props.lastname + ' ' + props.firstname);
const fullName = computed(() => props.lastname + ' ' + props.firstname);
const count = ref(0);
const count = ref(0);
return {fullName, count}
`,
template: '<div>my name is {{ fullName }}, <button id="inc-interactive-count" @click="count++">click here</button> count: <span id="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>',
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>',
}
})

View File

@ -1,3 +1,66 @@
<template>
<NuxtPage />
<div>
<NuxtPage />
</div>
</template>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
margin: 0;
padding: 0;
color: #003543;
background: #fafafa;
}
.test-component {
margin-bottom: 2rem;
padding: 1.5rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.test-component h2 {
margin-top: 0;
color: #003543;
}
.test-description {
padding: 0.75rem;
background: #f0f9f6;
border-left: 4px solid #00DC82;
margin-bottom: 1.5rem;
}
.component-display {
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 6px;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
}
button, .test-button {
background: #00DC82;
color: #003543;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
text-decoration: none;
display: inline-block;
}
button:hover, .test-button:hover {
background: #00c476;
}
.test-actions {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
}
</style>

View File

@ -1,29 +1,61 @@
<script setup lang="ts">
/**
* Test case: Suspense Multiple Navigation
*
* This demonstrates Nuxt's suspense functionality when
* rapidly navigating between pages.
*/
async function trigger () {
document.querySelector<HTMLButtonElement>('[data-testid="btn-a"]')!.click()
await new Promise(resolve => setTimeout(resolve, 10))
document.querySelector<HTMLButtonElement>('[data-testid="btn-b"]')!.click()
}
</script>
<template>
<div>
<div>Index Page</div>
<NuxtLink
id="btn-a"
to="/target?a"
>
Target A
</NuxtLink>
<NuxtLink
id="btn-b"
to="/target?b"
>
Target B
</NuxtLink>
<div class="test-component">
<h2 data-testid="index-title">
Suspense Multiple Navigation Test
</h2>
<div class="test-description">
<p>
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.
</p>
</div>
<h3>Navigation Actions:</h3>
<div class="component-display">
<div class="test-actions">
<NuxtLink
to="/target?id=a"
data-testid="btn-a"
class="test-button"
>
Target A
</NuxtLink>
<NuxtLink
to="/target?id=b"
data-testid="btn-b"
class="test-button"
>
Target B
</NuxtLink>
</div>
</div>
<button @click="trigger">
Trigger (for manual testing)
</button>
<div class="test-description">
<h4>Test Instructions</h4>
<p>
Click both buttons in rapid succession to verify that suspense handles
multiple navigations correctly. The final content should reflect the last
navigation (Target B).
</p>
</div>
</div>
</template>
<script setup>
async function trigger () {
document.getElementById('btn-a').click()
await new Promise(resolve => setTimeout(resolve, 10))
document.getElementById('btn-b').click()
}
</script>

View File

@ -1,11 +1,51 @@
<script setup lang="ts">
/**
* Target page for suspense navigation test
*/
const route = useRoute()
const id = computed(() => route.query.id || 'default')
// Simulate async data loading
await new Promise(resolve => setTimeout(resolve, 100))
</script>
<template>
<div>
<div id="content">
Hello {{ Object.keys($route.query).join(' ') }}
<div class="test-component">
<h2>Target Page</h2>
<div class="test-description">
<p>This page demonstrates async data loading with suspense.</p>
</div>
<h3>Component Output:</h3>
<div class="component-display">
<div
data-testid="content"
class="content-box"
>
Hello {{ id }}
</div>
<div class="test-actions">
<NuxtLink
to="/"
class="test-button"
>
Back to Home
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
await new Promise(resolve => setTimeout(resolve, 200))
</script>
<style scoped>
.content-box {
text-align: center;
font-size: 1.2rem;
margin-bottom: 1.5rem;
padding: 1rem;
background-color: #f0f9f6;
border-left: 4px solid #00DC82;
border-radius: 4px;
}
</style>

View File

@ -1,76 +0,0 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { $fetch, createPage, setup } from '@nuxt/test-utils/e2e'
import { isWindows } from 'std-env'
import { join } from 'pathe'
import { expectNoClientErrors } from './utils'
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))
await setup({
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,
},
})
describe('test basic config', () => {
it('expect render page without any error or logs', async () => {
await expectNoClientErrors('/')
})
it('test HelloWorld.vue', async () => {
const html = await $fetch('/')
const page = await createPage('/')
await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)
expect(html).toContain('<div id="hello-world">hello, Helloworld.vue here ! </div>')
expect(await page.locator('body').innerHTML()).toContain('<div id="hello-world">hello, Helloworld.vue here ! </div>')
await page.close()
})
it('test Name.ts', async () => {
const html = await $fetch('/')
const page = await createPage('/')
await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)
expect(html).toContain('<div id="name">I am the Name.ts component</div>')
expect(await page.locator('body').innerHTML()).toContain('<div id="name">I am the Name.ts component</div>')
await page.close()
})
it('test ShowTemplate.ts', async () => {
const html = await $fetch('/')
const page = await createPage('/')
await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)
expect(html).toContain('<div id="show-template">Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API</div>')
expect(await page.locator('body').innerHTML()).toContain('<div id="show-template">Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API</div>')
await page.close()
})
it('test Interactive component.ts', async () => {
const html = await $fetch('/')
const page = await createPage('/')
await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)
expect(html).toContain('I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api')
expect(await page.locator('#interactive').innerHTML()).toContain('I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api')
const button = page.locator('#inc-interactive-count')
await button.click()
const count = page.locator('#interactive-count')
expect(await count.innerHTML()).toBe('1')
await page.close()
})
})

View File

@ -1,61 +0,0 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { isWindows } from 'std-env'
import { setup } from '@nuxt/test-utils/e2e'
import { join } from 'pathe'
import { renderPage } from './utils'
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))
await setup({
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,
},
})
describe('suspense multiple nav', () => {
it('should not throw error', async () => {
const { page, consoleLogs, pageErrors } = await renderPage('/')
await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)
expect(await page.locator('#btn-a').textContent()).toMatchInlineSnapshot('" Target A "')
// Make sure it navigates to the correct page
await page.locator('#btn-a').click()
await page.waitForFunction(() => window.useNuxtApp?.()._route.path === '/target')
console.log(page.url())
expect(await page.locator('#content').textContent()).toContain('Hello a')
await page.goBack()
await page.waitForFunction(() => window.useNuxtApp?.()._route.path === '/')
// When back
expect(await page.locator('body').textContent()).toContain('Index Page')
// So we click two navigations quickly, before the first one is resolved
await Promise.all([
page.locator('#btn-a').click(),
page.locator('#btn-b').click(),
])
await page.waitForFunction(() => window.useNuxtApp?.()._route.path === '/target')
expect.soft(await page.locator('#content').textContent()).toContain('Hello b')
const consoleLogErrors = consoleLogs.filter(i => i.type === 'error')
const consoleLogWarnings = consoleLogs.filter(i => i.type === 'warning')
expect.soft(pageErrors).toEqual([])
expect.soft(consoleLogErrors).toEqual([])
expect.soft(consoleLogWarnings).toEqual([])
await page.close()
}, 60_000)
})