mirror of
https://github.com/nuxt/nuxt.git
synced 2025-03-21 00:35:55 +00:00
test: migrate hmr test to use playwright runner (#31241)
This commit is contained in:
parent
b27294e205
commit
e78a456c43
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -302,7 +302,7 @@ jobs:
|
||||
path: packages
|
||||
|
||||
- name: Test (fixtures)
|
||||
run: pnpm test:fixtures
|
||||
run: pnpm test:fixtures && pnpm test:e2e
|
||||
env:
|
||||
TEST_ENV: ${{ matrix.env }}
|
||||
TEST_BUILDER: ${{ matrix.builder }}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -78,3 +78,5 @@ fixtures-temp
|
||||
.pnpm-store
|
||||
eslint-typegen.d.ts
|
||||
.eslintcache
|
||||
test-results/
|
||||
playwright-report
|
||||
|
@ -33,6 +33,11 @@
|
||||
"test:types": "pnpm --filter './test/fixtures/**' test:types",
|
||||
"test:unit": "vitest run packages/",
|
||||
"test:attw": "pnpm --filter './packages/**' test:attw",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:dev": "TEST_ENV=dev playwright test",
|
||||
"test:e2e:webpack": "TEST_BUILDER=webpack playwright test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs --languages html"
|
||||
},
|
||||
@ -77,6 +82,7 @@
|
||||
"@nuxt/rspack-builder": "workspace:*",
|
||||
"@nuxt/test-utils": "3.17.1",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@playwright/test": "1.51.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/babel__core": "7.20.5",
|
||||
"@types/babel__helper-plugin-utils": "7.10.3",
|
||||
@ -91,6 +97,7 @@
|
||||
"changelogen": "0.6.1",
|
||||
"consola": "3.4.0",
|
||||
"cssnano": "7.0.6",
|
||||
"defu": "^6.1.4",
|
||||
"destr": "2.0.3",
|
||||
"devalue": "5.1.1",
|
||||
"eslint": "9.21.0",
|
||||
@ -112,7 +119,7 @@
|
||||
"ofetch": "1.4.1",
|
||||
"pathe": "2.0.3",
|
||||
"pkg-pr-new": "0.0.40",
|
||||
"playwright-core": "1.50.1",
|
||||
"playwright-core": "1.51.0",
|
||||
"rollup": "4.34.9",
|
||||
"semver": "7.7.1",
|
||||
"sherif": "1.4.0",
|
||||
|
34
playwright.config.ts
Normal file
34
playwright.config.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
import type { ConfigOptions } from '@nuxt/test-utils/playwright'
|
||||
import { isCI, isWindows } from 'std-env'
|
||||
|
||||
/**
|
||||
* Playwright configuration for Nuxt e2e tests
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
export default defineConfig<ConfigOptions>({
|
||||
testDir: './test/e2e',
|
||||
testMatch: '**/*.test.ts',
|
||||
timeout: (isWindows ? 360 : 120) * 1000,
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
workers: isCI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
projects: [
|
||||
{
|
||||
name: 'setup fixtures',
|
||||
testMatch: /global\.setup\.ts/,
|
||||
teardown: 'cleanup fixtures',
|
||||
},
|
||||
{
|
||||
name: 'cleanup fixtures',
|
||||
testMatch: /global\.teardown\.ts/,
|
||||
},
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
dependencies: ['setup fixtures'],
|
||||
},
|
||||
],
|
||||
})
|
471
pnpm-lock.yaml
471
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -58,7 +58,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"205k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"204k"`)
|
||||
|
||||
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1397k"`)
|
||||
@ -97,7 +97,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output-inline/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"556k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"555k"`)
|
||||
|
||||
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"90.9k"`)
|
||||
|
19
test/e2e/global.setup.ts
Normal file
19
test/e2e/global.setup.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { cp, rm } from 'node:fs/promises'
|
||||
import { test as setup } from '@playwright/test'
|
||||
|
||||
const fixtureDir = fileURLToPath(new URL('../fixtures-temp/hmr', import.meta.url))
|
||||
const sourceDir = fileURLToPath(new URL('../fixtures/hmr', import.meta.url))
|
||||
|
||||
setup('create temporary hmr fixture directory', async () => {
|
||||
if (existsSync(fixtureDir)) {
|
||||
await rm(fixtureDir, { force: true, recursive: true })
|
||||
}
|
||||
await cp(sourceDir, fixtureDir, {
|
||||
recursive: true,
|
||||
filter: (src) => {
|
||||
return !src.includes('.cache') && !src.endsWith('.sock') && !src.includes('.output') && !src.includes('.nuxt-')
|
||||
},
|
||||
})
|
||||
})
|
12
test/e2e/global.teardown.ts
Normal file
12
test/e2e/global.teardown.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { test as teardown } from '@playwright/test'
|
||||
|
||||
const fixtureDir = fileURLToPath(new URL('../fixtures-temp/hmr', import.meta.url))
|
||||
|
||||
teardown('remove temporary hmr fixture directory', async () => {
|
||||
if (existsSync(fixtureDir)) {
|
||||
await rm(fixtureDir, { force: true, recursive: true })
|
||||
}
|
||||
})
|
177
test/e2e/hmr.test.ts
Normal file
177
test/e2e/hmr.test.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { isWindows } from 'std-env'
|
||||
import { join } from 'pathe'
|
||||
import { expect, test } from './test-utils'
|
||||
|
||||
const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack'
|
||||
|
||||
const fixtureDir = fileURLToPath(new URL('../fixtures-temp/hmr', import.meta.url))
|
||||
const sourceDir = fileURLToPath(new URL('../fixtures/hmr', import.meta.url))
|
||||
|
||||
test.use({
|
||||
nuxt: {
|
||||
rootDir: fixtureDir,
|
||||
dev: true,
|
||||
setupTimeout: (isWindows ? 360 : 120) * 1000,
|
||||
env: { TEST: '1' },
|
||||
nuxtConfig: {
|
||||
test: true,
|
||||
builder: isWebpack ? 'webpack' : 'vite',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (process.env.TEST_ENV === 'built' || isWindows) {
|
||||
test.skip('Skipped: HMR tests are skipped on Windows or in built mode', () => {})
|
||||
} else {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
// Load the fixture file
|
||||
const indexVue = readFileSync(join(sourceDir, 'pages/index.vue'), 'utf8')
|
||||
|
||||
test('basic HMR functionality', async ({ page, goto }) => {
|
||||
// Navigate to the page
|
||||
writeFileSync(join(fixtureDir, 'pages/index.vue'), indexVue)
|
||||
await goto('/')
|
||||
|
||||
// Check initial state
|
||||
await expect(page).toHaveTitle('HMR fixture')
|
||||
await expect(page.locator('[data-testid="count"]')).toHaveText('1')
|
||||
|
||||
// Test reactivity
|
||||
await page.locator('button').click()
|
||||
await expect(page.locator('[data-testid="count"]')).toHaveText('2')
|
||||
|
||||
// Modify the file and check for HMR updates
|
||||
let newContents = indexVue
|
||||
.replace('<Title>HMR fixture</Title>', '<Title>HMR fixture HMR</Title>')
|
||||
.replace('<h1>Home page</h1>', '<h1>Home page - but not as you knew it</h1>')
|
||||
newContents += '<style scoped>\nh1 { color: red }\n</style>'
|
||||
|
||||
writeFileSync(join(fixtureDir, 'pages/index.vue'), newContents)
|
||||
|
||||
// Wait for the title to be updated via HMR
|
||||
await expect(page).toHaveTitle('HMR fixture HMR')
|
||||
|
||||
// Check content HMR
|
||||
const h1 = page.locator('h1')
|
||||
await expect(h1).toHaveText('Home page - but not as you knew it')
|
||||
|
||||
// Check style HMR
|
||||
const h1Color = await h1.evaluate(el => window.getComputedStyle(el).getPropertyValue('color'))
|
||||
expect(h1Color).toBe('rgb(255, 0, 0)')
|
||||
|
||||
expect(page).toHaveNoErrorsOrWarnings()
|
||||
})
|
||||
|
||||
test('detecting new routes', async ({ fetch }) => {
|
||||
// Try accessing a non-existent route
|
||||
await rm(join(fixtureDir, 'pages/some-404.vue'), { force: true })
|
||||
const res = await fetch('/some-404')
|
||||
expect(res.status).toBe(404)
|
||||
|
||||
// Create a new page file
|
||||
writeFileSync(join(fixtureDir, 'pages/some-404.vue'), indexVue)
|
||||
|
||||
// Wait for the new route to be available
|
||||
await expect(() => fetch('/some-404').then(r => r.status).catch(() => false)).toBeWithPolling(200)
|
||||
})
|
||||
|
||||
test('hot reloading route rules', async ({ fetch }) => {
|
||||
// Check the initial header
|
||||
const file = readFileSync(join(sourceDir, 'pages/route-rules.vue'), 'utf8')
|
||||
writeFileSync(join(fixtureDir, 'pages/route-rules.vue'), file)
|
||||
|
||||
await expect(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null)).toBeWithPolling('added in routeRules')
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// Modify the route rules
|
||||
writeFileSync(join(fixtureDir, 'pages/route-rules.vue'), file.replace('added in routeRules', 'edited in dev'))
|
||||
|
||||
// Wait for the route rule to be hot reloaded
|
||||
await expect(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null)).toBeWithPolling('edited in dev')
|
||||
})
|
||||
|
||||
test('HMR for island components', async ({ page, goto }) => {
|
||||
// Navigate to the page with the island components
|
||||
await goto('/server-component')
|
||||
|
||||
const componentPath = join(fixtureDir, 'components/islands/HmrComponent.vue')
|
||||
const componentContents = readFileSync(componentPath, 'utf8')
|
||||
|
||||
// Test initial state of the component
|
||||
await expect(page.getByTestId('hmr-id')).toHaveText('0')
|
||||
|
||||
// Function to update the component and check for changes
|
||||
const triggerHmr = (number: string) => writeFileSync(componentPath, componentContents.replace('ref(0)', `ref(${number})`))
|
||||
|
||||
// First edit
|
||||
triggerHmr('1')
|
||||
await expect(page.getByTestId('hmr-id')).toHaveText('1', { timeout: 10000 })
|
||||
|
||||
// Second edit to make sure HMR is working consistently
|
||||
triggerHmr('2')
|
||||
await expect(page.getByTestId('hmr-id')).toHaveText('2', { timeout: 10000 })
|
||||
|
||||
expect(page).toHaveNoErrorsOrWarnings()
|
||||
})
|
||||
|
||||
// Skip if using webpack since this test only works with Vite
|
||||
if (!isWebpack) {
|
||||
test('HMR for page meta', async ({ page, goto }) => {
|
||||
const pageContents = readFileSync(join(sourceDir, 'pages/page-meta.vue'), 'utf8')
|
||||
writeFileSync(join(fixtureDir, 'pages/page-meta.vue'), pageContents)
|
||||
|
||||
await goto('/page-meta')
|
||||
|
||||
// Check initial meta state
|
||||
await expect(page.getByTestId('meta')).toHaveText(JSON.stringify({ some: 'stuff' }, null, 2))
|
||||
|
||||
// Update the meta
|
||||
writeFileSync(join(fixtureDir, 'pages/page-meta.vue'), pageContents.replace(`some: 'stuff'`, `some: 'other stuff'`))
|
||||
|
||||
// Check if meta updates
|
||||
await expect(page.getByTestId('meta')).toHaveText(JSON.stringify({ some: 'other stuff' }, null, 2))
|
||||
|
||||
// Verify no errors in console
|
||||
expect(page).toHaveNoErrorsOrWarnings()
|
||||
})
|
||||
|
||||
test('HMR for routes', async ({ page, goto }) => {
|
||||
await goto('/routes')
|
||||
|
||||
// Create a new route that doesn't exist yet
|
||||
writeFileSync(
|
||||
join(fixtureDir, 'pages/routes/non-existent.vue'),
|
||||
`<template><div data-testid="contents">A new route!</div></template>`,
|
||||
)
|
||||
|
||||
// Track console logs
|
||||
const consoleLogs: Array<{ type: string, text: string }> = []
|
||||
page.on('console', (msg) => {
|
||||
consoleLogs.push({
|
||||
type: msg.type(),
|
||||
text: msg.text(),
|
||||
})
|
||||
})
|
||||
|
||||
// Wait for HMR to process the new route
|
||||
await expect(() => consoleLogs.some(log => log.text.includes('hmr'))).toBeWithPolling(true)
|
||||
|
||||
// Navigate to the new route
|
||||
await page.locator('a[href="/routes/non-existent"]').click()
|
||||
|
||||
// Verify the new route content is rendered
|
||||
await expect(page.getByTestId('contents')).toHaveText('A new route!')
|
||||
|
||||
// Filter expected warnings about route not existing before the update
|
||||
const filteredLogs = consoleLogs.filter(log => (log.type === 'warning' || log.type === 'error') && !log.text.includes('No match found for location with path "/routes/non-existent"'))
|
||||
|
||||
// Verify no unexpected errors
|
||||
expect(filteredLogs).toStrictEqual([])
|
||||
})
|
||||
}
|
||||
}
|
117
test/e2e/test-utils.ts
Normal file
117
test/e2e/test-utils.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { waitForHydration } from '@nuxt/test-utils/e2e'
|
||||
import { test as base, expect as baseExpect } from '@nuxt/test-utils/playwright'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { fetch } from 'ofetch'
|
||||
import { joinURL } from 'ufo'
|
||||
|
||||
const test = base.extend<{ fetch: (path: string) => Promise<Response> }>({
|
||||
fetch: ({ request, _nuxtHooks }, use) => {
|
||||
use(async (path) => {
|
||||
let res: Response | undefined
|
||||
do {
|
||||
res = await fetch(joinURL(_nuxtHooks.ctx.url!, path), {
|
||||
headers: { 'accept': 'text/html' },
|
||||
signal: AbortSignal.timeout(1000),
|
||||
}).catch(() => undefined)
|
||||
} while (!res || res?.status === 503 || res?.status === 500)
|
||||
|
||||
if (!res) {
|
||||
await request.get(path, { headers: { 'accept': 'text/html' } })
|
||||
}
|
||||
|
||||
return res
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
test.use({
|
||||
page: ({ page }, use) => {
|
||||
const consoleLogs: Array<{ type: string, text: string }> = []
|
||||
page.on('console', (msg) => {
|
||||
consoleLogs.push({
|
||||
type: msg.type(),
|
||||
text: msg.text(),
|
||||
})
|
||||
})
|
||||
// @ts-expect-error untyped
|
||||
page._consoleLogs = consoleLogs
|
||||
return use(page)
|
||||
},
|
||||
goto: ({ page }, use) => {
|
||||
use(async (path, options) => {
|
||||
const result = await page.goto(path, options as any)
|
||||
await waitForHydration(page, path, 'hydration')
|
||||
return result
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const expect = baseExpect.extend({
|
||||
// Utility function to wait for a condition to be true
|
||||
async toBeWithPolling <T = true> (
|
||||
getter: () => Promise<T> | T,
|
||||
expected: T | ((val: T) => boolean) = true as T,
|
||||
options: { timeout?: number, interval?: number, message?: string } = {},
|
||||
) {
|
||||
const { timeout = 8000, interval = 300 } = options
|
||||
const startTime = Date.now()
|
||||
let lastValue: T | undefined
|
||||
let lastError: Error | undefined
|
||||
|
||||
// Create a matcher function
|
||||
const matcher = typeof expected === 'function'
|
||||
? expected as ((val: T) => boolean)
|
||||
: (val: T) => val === expected
|
||||
|
||||
let pass = false
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
lastValue = await getter()
|
||||
if (matcher(lastValue)) {
|
||||
pass = true
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err as Error
|
||||
}
|
||||
|
||||
// Wait before next attempt
|
||||
await new Promise(resolve => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
const message = options.message || `Timed out after ${timeout}ms waiting for condition to be met.`
|
||||
|
||||
// if (lastError) {
|
||||
// throw new Error(`${errorMessage}\nLast error: ${lastError.message}`)
|
||||
// }
|
||||
|
||||
// throw new Error(`${errorMessage}\nExpected: ${expected}\nReceived: ${lastValue!}`)
|
||||
return {
|
||||
message: () => pass ? '' : lastError ? `${message}\nLast error: ${lastError.message}` : `${message}\nExpected: ${expected}\nReceived: ${lastValue!}`,
|
||||
pass,
|
||||
name: 'toBeWithPolling',
|
||||
expected,
|
||||
actual: lastValue,
|
||||
}
|
||||
},
|
||||
toHaveNoErrorsOrWarnings (page: Page) {
|
||||
// @ts-expect-error untyped
|
||||
const consoleLogs: Array<{ text: string, type: string }> = page._consoleLogs
|
||||
const errorLogs = consoleLogs.filter(log =>
|
||||
log.type === 'error' || (log.type === 'warning' && !log.text.includes('webpack/hot/dev-server')))
|
||||
|
||||
const pass = errorLogs.length === 0
|
||||
const message = pass ? '' : `Found error logs: ${errorLogs.map(log => log.text).join('\n')}`
|
||||
|
||||
return {
|
||||
message: () => message,
|
||||
pass,
|
||||
name: 'toHaveNoErrorsOrWarnings',
|
||||
expected: [],
|
||||
actual: errorLogs,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export { test, expect }
|
176
test/hmr.test.ts
176
test/hmr.test.ts
@ -1,176 +0,0 @@
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { isWindows } from 'std-env'
|
||||
import { join } from 'pathe'
|
||||
import { $fetch as _$fetch, fetch, setup } from '@nuxt/test-utils/e2e'
|
||||
|
||||
import { expectNoErrorsOrWarnings, expectWithPolling, renderPage } from './utils'
|
||||
|
||||
// TODO: update @nuxt/test-utils
|
||||
const $fetch = _$fetch as import('nitro/types').$Fetch<unknown, import('nitro/types').NitroFetchRequest>
|
||||
|
||||
const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack'
|
||||
|
||||
// TODO: fix HMR on Windows
|
||||
if (process.env.TEST_ENV !== 'built' && !isWindows) {
|
||||
const fixturePath = fileURLToPath(new URL('./fixtures-temp/hmr', import.meta.url))
|
||||
await setup({
|
||||
rootDir: fixturePath,
|
||||
dev: true,
|
||||
server: true,
|
||||
browser: true,
|
||||
setupTimeout: (isWindows ? 360 : 120) * 1000,
|
||||
nuxtConfig: {
|
||||
buildDir: join(fixturePath, '.nuxt', 'test', Math.random().toString(36).slice(2, 8)),
|
||||
builder: isWebpack ? 'webpack' : 'vite',
|
||||
},
|
||||
})
|
||||
|
||||
const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8')
|
||||
|
||||
describe('hmr', { sequential: true }, () => {
|
||||
beforeAll(async () => {
|
||||
await expectWithPolling(() => $fetch<string>('/').then(r => r.includes('Home page')).catch(() => null), true)
|
||||
})
|
||||
|
||||
it('should work', async () => {
|
||||
const { page, pageErrors, consoleLogs } = await renderPage('/')
|
||||
|
||||
expect(await page.title()).toBe('HMR fixture')
|
||||
expect(await page.getByTestId('count').textContent()).toBe('1')
|
||||
|
||||
// reactive
|
||||
await page.getByRole('button').click()
|
||||
expect(await page.getByTestId('count').textContent()).toBe('2')
|
||||
|
||||
// modify file
|
||||
let newContents = indexVue
|
||||
.replace('<Title>HMR fixture</Title>', '<Title>HMR fixture HMR</Title>')
|
||||
.replace('<h1>Home page</h1>', '<h1>Home page - but not as you knew it</h1>')
|
||||
newContents += '<style scoped>\nh1 { color: red }\n</style>'
|
||||
await fsp.writeFile(join(fixturePath, 'pages/index.vue'), newContents)
|
||||
|
||||
await expectWithPolling(() => page.title(), 'HMR fixture HMR')
|
||||
|
||||
// content HMR
|
||||
const h1 = page.getByRole('heading')
|
||||
expect(await h1!.textContent()).toBe('Home page - but not as you knew it')
|
||||
|
||||
// style HMR
|
||||
const h1Color = await h1.evaluate(el => window.getComputedStyle(el).getPropertyValue('color'))
|
||||
expect(h1Color).toMatchInlineSnapshot('"rgb(255, 0, 0)"')
|
||||
|
||||
// ensure no errors
|
||||
expectNoErrorsOrWarnings(consoleLogs)
|
||||
expect(pageErrors).toEqual([])
|
||||
|
||||
await page.close()
|
||||
})
|
||||
|
||||
it('should detect new routes', { timeout: 60000 }, async () => {
|
||||
const res = await fetch('/some-404')
|
||||
expect(res.status).toBe(404)
|
||||
|
||||
// write new page route
|
||||
await fsp.writeFile(join(fixturePath, 'pages/some-404.vue'), indexVue)
|
||||
await expectWithPolling(() => $fetch<string>('/some-404').then(r => r.includes('Home page')).catch(() => null), true)
|
||||
})
|
||||
|
||||
it('should hot reload route rules', { timeout: 60000 }, async () => {
|
||||
await expectWithPolling(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null), 'added in routeRules')
|
||||
|
||||
// write new page route
|
||||
const file = await fsp.readFile(join(fixturePath, 'pages/route-rules.vue'), 'utf8')
|
||||
await fsp.writeFile(join(fixturePath, 'pages/route-rules.vue'), file.replace('added in routeRules', 'edited in dev'))
|
||||
|
||||
await expectWithPolling(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null), 'edited in dev')
|
||||
})
|
||||
|
||||
it('should HMR islands', async () => {
|
||||
const { page, pageErrors, consoleLogs } = await renderPage('/server-component')
|
||||
|
||||
const componentPath = join(fixturePath, 'components/islands/HmrComponent.vue')
|
||||
const componentContents = await fsp.readFile(componentPath, 'utf8')
|
||||
const triggerHmr = (number: string) => fsp.writeFile(componentPath, componentContents.replace('ref(0)', `ref(${number})`))
|
||||
|
||||
// initial state
|
||||
await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '0')
|
||||
|
||||
// first edit
|
||||
await triggerHmr('1')
|
||||
await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '1')
|
||||
|
||||
// just in-case
|
||||
await triggerHmr('2')
|
||||
await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '2')
|
||||
|
||||
// ensure no errors
|
||||
expectNoErrorsOrWarnings(consoleLogs)
|
||||
expect(pageErrors).toEqual([])
|
||||
|
||||
await page.close()
|
||||
})
|
||||
|
||||
it.skipIf(isWebpack)('should HMR page meta', async () => {
|
||||
const { page, pageErrors, consoleLogs } = await renderPage('/page-meta')
|
||||
|
||||
const pagePath = join(fixturePath, 'pages/page-meta.vue')
|
||||
const pageContents = await fsp.readFile(pagePath, 'utf8')
|
||||
|
||||
expect(JSON.parse(await page.getByTestId('meta').textContent() || '{}')).toStrictEqual({ some: 'stuff' })
|
||||
const initialConsoleLogs = structuredClone(consoleLogs)
|
||||
|
||||
await fsp.writeFile(pagePath, pageContents.replace(`some: 'stuff'`, `some: 'other stuff'`))
|
||||
|
||||
await expectWithPolling(async () => await page.getByTestId('meta').textContent() || '{}', JSON.stringify({ some: 'other stuff' }, null, 2))
|
||||
expect(consoleLogs).toStrictEqual([
|
||||
...initialConsoleLogs,
|
||||
{
|
||||
'text': '[vite] hot updated: /pages/page-meta.vue',
|
||||
'type': 'debug',
|
||||
},
|
||||
{
|
||||
'text': '[vite] hot updated: /pages/page-meta.vue?macro=true',
|
||||
'type': 'debug',
|
||||
},
|
||||
{
|
||||
'text': `[vite] hot updated: /@id/virtual:nuxt:${encodeURIComponent(join(fixturePath, '.nuxt/routes.mjs'))}`,
|
||||
'type': 'debug',
|
||||
},
|
||||
])
|
||||
|
||||
// ensure no errors
|
||||
expectNoErrorsOrWarnings(consoleLogs)
|
||||
expect(pageErrors).toEqual([])
|
||||
|
||||
await page.close()
|
||||
})
|
||||
|
||||
it.skipIf(isWebpack)('should HMR routes', { timeout: 60_000 }, async () => {
|
||||
const { page, pageErrors, consoleLogs } = await renderPage('/routes', { retries: 5 })
|
||||
|
||||
await fsp.writeFile(join(fixturePath, 'pages/routes/non-existent.vue'), `<template><div data-testid="contents">A new route!</div></template>`)
|
||||
|
||||
await expectWithPolling(() => consoleLogs.some(log => log.text.includes('hmr')), true)
|
||||
|
||||
await page.getByRole('link').click()
|
||||
await expectWithPolling(() => page.getByTestId('contents').textContent(), 'A new route!')
|
||||
|
||||
for (const log of consoleLogs) {
|
||||
if (log.text.includes('No match found for location with path "/routes/non-existent"')) {
|
||||
// we expect this warning before the routes are updated
|
||||
log.type = 'debug'
|
||||
}
|
||||
}
|
||||
|
||||
// ensure no errors
|
||||
expectNoErrorsOrWarnings(consoleLogs)
|
||||
expect(pageErrors).toEqual([])
|
||||
|
||||
await page.close()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
describe.skip('hmr', () => {})
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { cp, rm } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { dirname, join } from 'pathe'
|
||||
|
||||
const dir = dirname(fileURLToPath(import.meta.url))
|
||||
const fixtureDir = join(dir, 'fixtures')
|
||||
const tempDir = join(dir, 'fixtures-temp')
|
||||
|
||||
export async function setup () {
|
||||
if (existsSync(tempDir)) {
|
||||
await rm(tempDir, { force: true, recursive: true })
|
||||
}
|
||||
await cp(fixtureDir, tempDir, {
|
||||
recursive: true,
|
||||
filter: src => !src.includes('.cache'),
|
||||
})
|
||||
}
|
||||
|
||||
export async function teardown () {
|
||||
if (existsSync(tempDir)) {
|
||||
await rm(tempDir, { force: true, recursive: true })
|
||||
}
|
||||
}
|
@ -15,14 +15,13 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globalSetup: './test/setup.ts',
|
||||
setupFiles: ['./test/setup-env.ts'],
|
||||
coverage: {
|
||||
exclude: [...coverageConfigDefaults.exclude, 'packages/nuxt/src/app', 'playground', '**/test/', 'scripts', 'vitest.nuxt.config.ts'],
|
||||
},
|
||||
testTimeout: isWindows ? 60000 : 10000,
|
||||
// Excluded plugin because it should throw an error when accidentally loaded via Nuxt
|
||||
exclude: [...configDefaults.exclude, 'nuxt/**', '**/test.ts', '**/this-should-not-load.spec.js'],
|
||||
exclude: [...configDefaults.exclude, 'test/e2e/**', 'e2e/**', 'nuxt/**', '**/test.ts', '**/this-should-not-load.spec.js'],
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: process.env.TEST_ENV === 'dev' ? 1 : undefined,
|
||||
|
Loading…
Reference in New Issue
Block a user