From 757641a7ea935d5714e0a3d3f5decf1804332c7e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 17 Mar 2025 16:59:22 +0000 Subject: [PATCH] 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> --- test/basic.test.ts | 5 +- test/e2e/runtime-compiler.test.ts | 149 ++++++++++++++ test/e2e/suspense.test.ts | 74 +++++++ .../runtime-compiler/layouts/default.vue | 176 ++++++++++++++++ .../runtime-compiler/pages/api-template.vue | 131 ++++++++++++ .../pages/basic-component.vue | 63 ++++++ .../pages/component-in-setup.vue | 96 +++++++++ .../runtime-compiler/pages/full-dynamic.vue | 193 ++++++++++++++++++ .../fixtures/runtime-compiler/pages/index.vue | 147 ++++++------- .../pages/typescript-component.vue | 100 +++++++++ .../server/api/full-component.get.ts | 10 +- test/fixtures/suspense/app.vue | 65 +++++- test/fixtures/suspense/pages/index.vue | 76 +++++-- test/fixtures/suspense/pages/target.vue | 52 ++++- test/runtime-compiler.test.ts | 76 ------- test/suspense.test.ts | 61 ------ 16 files changed, 1229 insertions(+), 245 deletions(-) create mode 100644 test/e2e/runtime-compiler.test.ts create mode 100644 test/e2e/suspense.test.ts create mode 100644 test/fixtures/runtime-compiler/layouts/default.vue create mode 100644 test/fixtures/runtime-compiler/pages/api-template.vue create mode 100644 test/fixtures/runtime-compiler/pages/basic-component.vue create mode 100644 test/fixtures/runtime-compiler/pages/component-in-setup.vue create mode 100644 test/fixtures/runtime-compiler/pages/full-dynamic.vue create mode 100644 test/fixtures/runtime-compiler/pages/typescript-component.vue delete mode 100644 test/runtime-compiler.test.ts delete mode 100644 test/suspense.test.ts diff --git a/test/basic.test.ts b/test/basic.test.ts index c90965d4bf..46987eaa5f 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -4,16 +4,13 @@ import { describe, expect, it } from 'vitest' import { joinURL, withQuery } from 'ufo' import { isCI, isWindows } from 'std-env' import { join, normalize } from 'pathe' -import { $fetch as _$fetch, createPage, fetch, isDev, setup, startServer, url, useTestContext } from '@nuxt/test-utils/e2e' +import { $fetch, createPage, fetch, isDev, setup, startServer, url, useTestContext } from '@nuxt/test-utils/e2e' import { $fetchComponent } from '@nuxt/test-utils/experimental' import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage } from './utils' import type { NuxtIslandResponse } from '#app' -// TODO: update @nuxt/test-utils -const $fetch = _$fetch as import('nitro/types').$Fetch - const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack' const isTestingAppManifest = process.env.TEST_MANIFEST !== 'manifest-off' 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 @@ + + + diff --git a/test/fixtures/runtime-compiler/pages/api-template.vue b/test/fixtures/runtime-compiler/pages/api-template.vue new file mode 100644 index 0000000000..ac257cf901 --- /dev/null +++ b/test/fixtures/runtime-compiler/pages/api-template.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/test/fixtures/runtime-compiler/pages/basic-component.vue b/test/fixtures/runtime-compiler/pages/basic-component.vue new file mode 100644 index 0000000000..e57534f4c3 --- /dev/null +++ b/test/fixtures/runtime-compiler/pages/basic-component.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/test/fixtures/runtime-compiler/pages/component-in-setup.vue b/test/fixtures/runtime-compiler/pages/component-in-setup.vue new file mode 100644 index 0000000000..6366b4153c --- /dev/null +++ b/test/fixtures/runtime-compiler/pages/component-in-setup.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/test/fixtures/runtime-compiler/pages/full-dynamic.vue b/test/fixtures/runtime-compiler/pages/full-dynamic.vue new file mode 100644 index 0000000000..aa068006e1 --- /dev/null +++ b/test/fixtures/runtime-compiler/pages/full-dynamic.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/test/fixtures/runtime-compiler/pages/index.vue b/test/fixtures/runtime-compiler/pages/index.vue index 0d9e1f5bfb..2e8840d000 100644 --- a/test/fixtures/runtime-compiler/pages/index.vue +++ b/test/fixtures/runtime-compiler/pages/index.vue @@ -1,80 +1,87 @@ - diff --git a/test/fixtures/runtime-compiler/pages/typescript-component.vue b/test/fixtures/runtime-compiler/pages/typescript-component.vue new file mode 100644 index 0000000000..9519659f52 --- /dev/null +++ b/test/fixtures/runtime-compiler/pages/typescript-component.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/test/fixtures/runtime-compiler/server/api/full-component.get.ts b/test/fixtures/runtime-compiler/server/api/full-component.get.ts index 1585f4989f..ef25b08af8 100644 --- a/test/fixtures/runtime-compiler/server/api/full-component.get.ts +++ b/test/fixtures/runtime-compiler/server/api/full-component.get.ts @@ -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: '
my name is {{ fullName }}, count: {{count}}. I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api
', + return {fullName, count} + `, + template: '
my name is {{ fullName }}, count: {{count}}. I am defined by Interactive in the setup of App.vue. My full component definition is retrieved from the api
', } }) diff --git a/test/fixtures/suspense/app.vue b/test/fixtures/suspense/app.vue index 8f62b8bf92..eb9d4306b3 100644 --- a/test/fixtures/suspense/app.vue +++ b/test/fixtures/suspense/app.vue @@ -1,3 +1,66 @@ + + diff --git a/test/fixtures/suspense/pages/index.vue b/test/fixtures/suspense/pages/index.vue index bc377b646d..131d6c5ae4 100644 --- a/test/fixtures/suspense/pages/index.vue +++ b/test/fixtures/suspense/pages/index.vue @@ -1,29 +1,61 @@ + + - - diff --git a/test/fixtures/suspense/pages/target.vue b/test/fixtures/suspense/pages/target.vue index 4a0e8fa21b..bf27a0ad50 100644 --- a/test/fixtures/suspense/pages/target.vue +++ b/test/fixtures/suspense/pages/target.vue @@ -1,11 +1,51 @@ + + - + diff --git a/test/runtime-compiler.test.ts b/test/runtime-compiler.test.ts deleted file mode 100644 index cc7f1c2116..0000000000 --- a/test/runtime-compiler.test.ts +++ /dev/null @@ -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('
hello, Helloworld.vue here !
') - expect(await page.locator('body').innerHTML()).toContain('
hello, Helloworld.vue here !
') - 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('
I am the Name.ts component
') - expect(await page.locator('body').innerHTML()).toContain('
I am the Name.ts component
') - - 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('
Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API
') - expect(await page.locator('body').innerHTML()).toContain('
Hello my name is : John, i am defined by ShowTemplate.vue and my template is retrieved from the API
') - - 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() - }) -}) diff --git a/test/suspense.test.ts b/test/suspense.test.ts deleted file mode 100644 index c1834953f7..0000000000 --- a/test/suspense.test.ts +++ /dev/null @@ -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) -})