From c91505a3f6b24d06f707391e7fd9395fa065e312 Mon Sep 17 00:00:00 2001 From: Francesco Agnoletto Date: Mon, 17 Mar 2025 23:48:38 +0100 Subject: [PATCH] test: migrate spa preloader tests to playwright (#31273) --- test/e2e/spa-preloader-body.test.ts | 95 +++++++++++++++++++ test/e2e/spa-preloader-within.test.ts | 95 +++++++++++++++++++ test/fixtures/spa-loader/app.vue | 8 +- .../spa-loader/plugins/delay.client.ts | 3 - .../spa-preloader-outside-disabled.test.ts | 54 ----------- .../spa-preloader-outside-enabled.test.ts | 65 ------------- 6 files changed, 193 insertions(+), 127 deletions(-) create mode 100644 test/e2e/spa-preloader-body.test.ts create mode 100644 test/e2e/spa-preloader-within.test.ts delete mode 100644 test/fixtures/spa-loader/plugins/delay.client.ts delete mode 100644 test/spa-loader/spa-preloader-outside-disabled.test.ts delete mode 100644 test/spa-loader/spa-preloader-outside-enabled.test.ts diff --git a/test/e2e/spa-preloader-body.test.ts b/test/e2e/spa-preloader-body.test.ts new file mode 100644 index 0000000000..4125e92281 --- /dev/null +++ b/test/e2e/spa-preloader-body.test.ts @@ -0,0 +1,95 @@ +import { fileURLToPath } from 'node:url' +import { isWindows } from 'std-env' +import { join } from 'pathe' +import type { Page } from 'playwright-core' +import { waitForHydration } from '@nuxt/test-utils' +import { expect, test } from './test-utils' + +/** + * This test suite verifies that the SPA loading template is correctly rendered + * outside the app tag when spaLoadingTemplateLocation is set to 'body'. + */ + +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/spa-loader', import.meta.url)) + +// Skip tests in dev mode +test.skip(isDev, 'These tests are only relevant in production mode') + +const loaderHTML = '
loading...
' + +test.use({ + nuxt: { + rootDir: fixtureDir, + server: true, + browser: true, + setupTimeout: (isWindows ? 360 : 120) * 1000, + nuxtConfig: { + buildDir: isDev ? join(fixtureDir, '.nuxt', 'test', Math.random().toString(36).slice(2, 8)) : undefined, + builder: isWebpack ? 'webpack' : 'vite', + spaLoadingTemplate: true, + experimental: { + spaLoadingTemplateLocation: 'body', + }, + }, + }, +}) + +test.describe('spaLoadingTemplateLocation flag is set to `body`', () => { + test('should render loader alongside appTag', async ({ request }) => { + const response = await request.get('/spa') + const html = await response.text() + + expect(html).toContain(loaderHTML) + }) + + test('should render spa-loader', async ({ page, fetch }) => { + expect(await fetch('/spa').then(r => r.text())).toContain(loaderHTML) + + // Navigate to the SPA page + await page.goto('/spa') + + // Verify the loader is visible first and content is hidden + expect(await getState(page)).toEqual({ + loader: true, + content: false, + }) + + page.dispatchEvent('html', 'finishHydration') + await waitForHydration(page, '/spa', 'hydration') + + expect(await getState(page)).toEqual({ + loader: false, + content: true, + }) + }) + + test('should render content without spa-loader for SSR pages', async ({ page, fetch }) => { + expect(await fetch('/ssr').then(r => r.text())).not.toContain(loaderHTML) + + // Navigate to SSR page + await page.goto('/ssr') + + // Verify the loader is hidden and content is visible for SSR pages + expect(await getState(page)).toEqual({ + loader: false, + content: true, + }) + }) +}) + +// isVisible is preferred here as we want to snapshot the state of the page at a specific moment, since waiting would make this test flake. +// https://github.com/nuxt/nuxt/pull/31273#issuecomment-2731002417 +async function getState (page: Page) { + const [loader, content] = await Promise.all([ + page.getByTestId('loader').isVisible(), + page.getByTestId('content').isVisible(), + ]) + const state = { + loader, + content, + } + return state +} diff --git a/test/e2e/spa-preloader-within.test.ts b/test/e2e/spa-preloader-within.test.ts new file mode 100644 index 0000000000..fae040bf90 --- /dev/null +++ b/test/e2e/spa-preloader-within.test.ts @@ -0,0 +1,95 @@ +import { fileURLToPath } from 'node:url' +import { isWindows } from 'std-env' +import { join } from 'pathe' +import type { Page } from 'playwright-core' +import { waitForHydration } from '@nuxt/test-utils' +import { expect, test } from './test-utils' + +/** + * This test suite verifies that the SPA loading template is correctly rendered + * inside the app tag when spaLoadingTemplateLocation is set to 'within'. + */ + +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/spa-loader', import.meta.url)) + +// Skip tests in dev mode +test.skip(isDev, 'These tests are only relevant in production mode') + +const loaderHTML = '
loading...
' + +test.use({ + nuxt: { + rootDir: fixtureDir, + dev: isDev, + server: true, + browser: true, + setupTimeout: (isWindows ? 360 : 120) * 1000, + nuxtConfig: { + buildDir: isDev ? join(fixtureDir, '.nuxt', 'test', Math.random().toString(36).slice(2, 8)) : undefined, + builder: isWebpack ? 'webpack' : 'vite', + spaLoadingTemplate: true, + experimental: { + spaLoadingTemplateLocation: 'within', + }, + }, + }, +}) + +test.describe('spaLoadingTemplateLocation flag is set to `within`', () => { + test('should render loader inside appTag', async ({ request }) => { + const response = await request.get('/spa') + const html = await response.text() + + expect(html).toContain(loaderHTML) + }) + + test('spa-loader does not appear while the app is mounting', async ({ page }) => { + // Navigate to the SPA page + await page.goto('/spa') + + // wait for intervening (less optimal!) current behaviour + await expect(page.getByTestId('loader')).toBeHidden() + await expect(page.getByTestId('content')).toBeHidden() + + expect(await getState(page)).toEqual({ + loader: false, + content: false, + }) + + page.dispatchEvent('html', 'finishHydration') + await waitForHydration(page, '/spa', 'hydration') + + // Wait for content to become visible after hydration + await expect(page.getByTestId('content')).toBeVisible() + }) + + test('should render content without spa-loader for SSR pages', async ({ page, fetch }) => { + expect(await fetch('/ssr').then(r => r.text())).not.toContain(loaderHTML) + + // Navigate to SSR page + await page.goto('/ssr') + + // Verify the loader is hidden and content is visible for SSR pages + expect(await getState(page)).toEqual({ + loader: false, + content: true, + }) + }) +}) + +// isVisible is preferred here as we want to snapshot the state of the page at a specific moment, since waiting that would make this test flake. +// https://github.com/nuxt/nuxt/pull/31273#issuecomment-2731002417 +async function getState (page: Page) { + const [loader, content] = await Promise.all([ + page.getByTestId('loader').isVisible(), + page.getByTestId('content').isVisible(), + ]) + const state = { + loader, + content, + } + return state +} diff --git a/test/fixtures/spa-loader/app.vue b/test/fixtures/spa-loader/app.vue index 574b3e6fe0..2196bff703 100644 --- a/test/fixtures/spa-loader/app.vue +++ b/test/fixtures/spa-loader/app.vue @@ -1,6 +1,8 @@ @@ -9,7 +11,3 @@ if (import.meta.client) { app content - - diff --git a/test/fixtures/spa-loader/plugins/delay.client.ts b/test/fixtures/spa-loader/plugins/delay.client.ts deleted file mode 100644 index 49529b291c..0000000000 --- a/test/fixtures/spa-loader/plugins/delay.client.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default defineNuxtPlugin(async () => { - await new Promise(resolve => setTimeout(resolve, 50)) -}) diff --git a/test/spa-loader/spa-preloader-outside-disabled.test.ts b/test/spa-loader/spa-preloader-outside-disabled.test.ts deleted file mode 100644 index d733699e48..0000000000 --- a/test/spa-loader/spa-preloader-outside-disabled.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' -import { isWindows } from 'std-env' -import { $fetch, createPage, setup, url } from '@nuxt/test-utils/e2e' -import { join } from 'pathe' - -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/spa-loader', import.meta.url)) - -if (!isDev) { - await setup({ - rootDir: fixtureDir, - dev: isDev, - server: true, - browser: true, - setupTimeout: (isWindows ? 360 : 120) * 1000, - nuxtConfig: { - buildDir: isDev ? join(fixtureDir, '.nuxt', 'test', Math.random().toString(36).slice(2, 8)) : undefined, - builder: isWebpack ? 'webpack' : 'vite', - spaLoadingTemplate: true, - experimental: { - spaLoadingTemplateLocation: 'within', - }, - }, - }) -} - -describe.skipIf(isDev)('spaLoadingTemplateLocation flag is set to `within`', () => { - it('should render loader inside appTag', async () => { - const html = await $fetch('/spa') - expect(html).toContain(`
loading...
`) - }) - - it('spa-loader does not appear while the app is mounting', async () => { - const page = await createPage() - await page.goto(url('/spa')) - - await page.getByTestId('loader').waitFor({ state: 'visible' }) - expect(await page.getByTestId('content').isHidden()).toBeTruthy() - - await page.waitForFunction(() => window.useNuxtApp?.() && window.useNuxtApp?.().isHydrating) - - expect(await page.getByTestId('content').isHidden()).toBeTruthy() - - await page.getByTestId('content').waitFor({ state: 'visible' }) - - await page.close() - }, 60_000) -}) diff --git a/test/spa-loader/spa-preloader-outside-enabled.test.ts b/test/spa-loader/spa-preloader-outside-enabled.test.ts deleted file mode 100644 index f381e09332..0000000000 --- a/test/spa-loader/spa-preloader-outside-enabled.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' -import { isWindows } from 'std-env' -import { createPage, setup, url } from '@nuxt/test-utils/e2e' -import type { Page } from 'playwright-core' -import { join } from 'pathe' - -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/spa-loader', import.meta.url)) - -if (!isDev) { - await setup({ - rootDir: fixtureDir, - server: true, - browser: true, - setupTimeout: (isWindows ? 360 : 120) * 1000, - nuxtConfig: { - buildDir: isDev ? join(fixtureDir, '.nuxt', 'test', Math.random().toString(36).slice(2, 8)) : undefined, - builder: isWebpack ? 'webpack' : 'vite', - spaLoadingTemplate: true, - experimental: { - spaLoadingTemplateLocation: 'body', - }, - }, - }) -} - -describe.skipIf(isDev)('spaLoadingTemplateLocation flag is set to `body`', () => { - it('should render spa-loader', async () => { - const page = await createPage() - await page.goto(url('/spa'), { waitUntil: 'domcontentloaded' }) - - await page.getByTestId('loader').waitFor({ state: 'visible' }) - expect(await page.getByTestId('content').isHidden()).toBeTruthy() - - await page.getByTestId('content').waitFor({ state: 'visible' }) - expect(await page.getByTestId('loader').isHidden()).toBeTruthy() - - await page.close() - }, 60_000) - - it('should render content without spa-loader', async () => { - const page = await createPage() - await page.goto(url('/ssr'), { waitUntil: 'domcontentloaded' }) - - const [loaderIsHidden, contentIsHidden] = await getState(page) - - expect(loaderIsHidden).toBeTruthy() - expect(contentIsHidden).toBeFalsy() - - await page.close() - }, 60_000) -}) - -function getState (page: Page) { - const loader = page.getByTestId('loader') - const content = page.getByTestId('content') - - return Promise.all([ - loader.isHidden(), - content.isHidden(), - ]) -}