mirror of
https://github.com/nuxt/nuxt.git
synced 2025-03-18 23:41:18 +00:00
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:
parent
db4daa02a3
commit
7fd75f54ad
149
test/e2e/runtime-compiler.test.ts
Normal file
149
test/e2e/runtime-compiler.test.ts
Normal 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
74
test/e2e/suspense.test.ts
Normal 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()
|
||||
})
|
||||
})
|
176
test/fixtures/runtime-compiler/layouts/default.vue
vendored
Normal file
176
test/fixtures/runtime-compiler/layouts/default.vue
vendored
Normal 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>
|
131
test/fixtures/runtime-compiler/pages/api-template.vue
vendored
Normal file
131
test/fixtures/runtime-compiler/pages/api-template.vue
vendored
Normal 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 '<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>'
|
||||
})
|
||||
</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>
|
63
test/fixtures/runtime-compiler/pages/basic-component.vue
vendored
Normal file
63
test/fixtures/runtime-compiler/pages/basic-component.vue
vendored
Normal 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: '<div>hello, Helloworld.vue here ! </div>',
|
||||
})
|
||||
</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>
|
96
test/fixtures/runtime-compiler/pages/component-in-setup.vue
vendored
Normal file
96
test/fixtures/runtime-compiler/pages/component-in-setup.vue
vendored
Normal 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: `
|
||||
<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>`,
|
||||
}))
|
||||
</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>
|
193
test/fixtures/runtime-compiler/pages/full-dynamic.vue
vendored
Normal file
193
test/fixtures/runtime-compiler/pages/full-dynamic.vue
vendored
Normal 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: '<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>',
|
||||
}
|
||||
})
|
||||
</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>
|
147
test/fixtures/runtime-compiler/pages/index.vue
vendored
147
test/fixtures/runtime-compiler/pages/index.vue
vendored
@ -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>
|
||||
|
100
test/fixtures/runtime-compiler/pages/typescript-component.vue
vendored
Normal file
100
test/fixtures/runtime-compiler/pages/typescript-component.vue
vendored
Normal 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>
|
@ -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>',
|
||||
}
|
||||
})
|
||||
|
65
test/fixtures/suspense/app.vue
vendored
65
test/fixtures/suspense/app.vue
vendored
@ -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>
|
||||
|
76
test/fixtures/suspense/pages/index.vue
vendored
76
test/fixtures/suspense/pages/index.vue
vendored
@ -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>
|
||||
|
52
test/fixtures/suspense/pages/target.vue
vendored
52
test/fixtures/suspense/pages/target.vue
vendored
@ -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>
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
Loading…
Reference in New Issue
Block a user