diff --git a/.eslintrc b/.eslintrc
index 228718ee50..ccc3ae8e4f 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -18,6 +18,7 @@
"vue/one-component-per-file": "off",
"vue/require-default-prop": "off",
"vue/no-multiple-template-root": "off",
+ "vue/no-v-model-argument": "off",
"jsdoc/require-jsdoc": "off",
"jsdoc/require-param": "off",
"jsdoc/require-returns": "off",
diff --git a/.gitignore b/.gitignore
index 892bc2a522..1efebd2cb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,3 +61,5 @@ Temporary Items
.build-*
.env
.netlify
+
+fixtures-temp
diff --git a/test/basic.test.ts b/test/basic.test.ts
index d73e82aa7e..7c4aadc14a 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -1,15 +1,18 @@
import { fileURLToPath } from 'node:url'
+import { promises as fsp } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { joinURL, withQuery } from 'ufo'
import { isWindows } from 'std-env'
-import { normalize } from 'pathe'
+import { join, normalize } from 'pathe'
// eslint-disable-next-line import/order
-import { setup, fetch, $fetch, startServer, createPage, url } from '@nuxt/test-utils'
-import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
-import { expectNoClientErrors, renderPage, withLogs } from './utils'
+import { setup, fetch, $fetch, startServer, isDev, createPage, url } from '@nuxt/test-utils'
+import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
+import { expectNoClientErrors, fixturesDir, expectWithPolling, renderPage, withLogs } from './utils'
+
+const fixturePath = join(fixturesDir, 'basic')
await setup({
- rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
+ rootDir: fixturePath,
server: true,
browser: true,
setupTimeout: (isWindows ? 240 : 120) * 1000
@@ -981,3 +984,50 @@ describe.skipIf(isWindows)('useAsyncData', () => {
await expectNoClientErrors('/useAsyncData/promise-all')
})
})
+
+// HMR should be at the last
+// TODO: fix HMR on Windows
+if (isDev() && !isWindows) {
+ describe('hmr', () => {
+ it('should work', async () => {
+ const { page, pageErrors, consoleLogs } = await renderPage('/')
+
+ expect(await page.title()).toBe('Basic fixture')
+ expect((await page.$('.sugar-counter').then(r => r!.textContent()))!.trim())
+ .toEqual('Sugar Counter 12 x 2 = 24 Inc')
+
+ // reactive
+ await page.$('.sugar-counter button').then(r => r!.click())
+ expect((await page.$('.sugar-counter').then(r => r!.textContent()))!.trim())
+ .toEqual('Sugar Counter 13 x 2 = 26 Inc')
+
+ // modify file
+ let indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8')
+ indexVue = indexVue
+ .replace('
Basic fixture', 'Basic fixture HMR')
+ .replace('Hello Nuxt 3!
', 'Hello Nuxt 3! HMR
')
+ indexVue += ''
+ await fsp.writeFile(join(fixturePath, 'pages/index.vue'), indexVue)
+
+ await expectWithPolling(
+ () => page.title(),
+ 'Basic fixture HMR'
+ )
+
+ // content HMR
+ const h1 = await page.$('h1')
+ expect(await h1!.textContent()).toBe('Hello Nuxt 3! HMR')
+
+ // style HMR
+ const h1Color = await h1!.evaluate(el => window.getComputedStyle(el).getPropertyValue('color'))
+ expect(h1Color).toMatchInlineSnapshot('"rgb(255, 0, 0)"')
+
+ // ensure no errors
+ const consoleLogErrors = consoleLogs.filter(i => i.type === 'error')
+ const consoleLogWarnings = consoleLogs.filter(i => i.type === 'warn')
+ expect(pageErrors).toEqual([])
+ expect(consoleLogErrors).toEqual([])
+ expect(consoleLogWarnings).toEqual([])
+ }, isWindows ? 60_000 : 30_000)
+ })
+}
diff --git a/test/fixtures/basic/components/Nested/SugarCounter.vue b/test/fixtures/basic/components/Nested/SugarCounter.vue
index 059cf8f5f2..f74fd8182f 100644
--- a/test/fixtures/basic/components/Nested/SugarCounter.vue
+++ b/test/fixtures/basic/components/Nested/SugarCounter.vue
@@ -1,6 +1,6 @@
-
+
Sugar Counter {{ count }} x {{ multiplier }} = {{ doubled }}
+
diff --git a/test/fixtures/basic/pages/index.vue b/test/fixtures/basic/pages/index.vue
index 03d5ebea81..c9b9a78772 100644
--- a/test/fixtures/basic/pages/index.vue
+++ b/test/fixtures/basic/pages/index.vue
@@ -14,7 +14,7 @@
Link
-
+
diff --git a/test/setup.ts b/test/setup.ts
new file mode 100644
index 0000000000..a444d934b2
--- /dev/null
+++ b/test/setup.ts
@@ -0,0 +1,20 @@
+import { fileURLToPath } from 'node:url'
+import { dirname, join } from 'node:path'
+import fs from 'fs-extra'
+
+const dir = dirname(fileURLToPath(import.meta.url))
+const fixtureDir = join(dir, 'fixtures')
+const tempDir = join(dir, 'fixtures-temp')
+
+export async function setup () {
+ if (fs.existsSync(tempDir)) {
+ await fs.remove(tempDir)
+ }
+ await fs.copy(fixtureDir, tempDir)
+}
+
+export async function teardown () {
+ if (fs.existsSync(tempDir)) {
+ await fs.remove(tempDir)
+ }
+}
diff --git a/test/utils.ts b/test/utils.ts
index 46bb8c9020..5c566996c4 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -1,7 +1,10 @@
+import { fileURLToPath } from 'node:url'
import { expect } from 'vitest'
import type { Page } from 'playwright'
import { createPage, getBrowser, url, useTestContext } from '@nuxt/test-utils'
+export const fixturesDir = fileURLToPath(new URL(process.env.NUXT_TEST_DEV ? './fixtures-temp' : './fixtures', import.meta.url))
+
export async function renderPage (path = '/') {
const ctx = useTestContext()
if (!ctx.options.browser) {
@@ -50,6 +53,23 @@ export async function expectNoClientErrors (path: string) {
expect(consoleLogWarnings).toEqual([])
}
+export async function expectWithPolling (
+ get: () => Promise
| string,
+ expected: string,
+ retries = process.env.CI ? 100 : 30,
+ delay = process.env.CI ? 500 : 100
+) {
+ let result: string | undefined
+ for (let i = retries; i >= 0; i--) {
+ result = await get()
+ if (result === expected) {
+ break
+ }
+ await new Promise(resolve => setTimeout(resolve, delay))
+ }
+ expect(result).toEqual(expected)
+}
+
export async function withLogs (callback: (page: Page, logs: string[]) => Promise) {
let done = false
const page = await createPage()
diff --git a/vitest.config.ts b/vitest.config.ts
index fa1bc0964a..9addb79bee 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -14,8 +14,11 @@ export default defineConfig({
tsconfigRaw: '{}'
},
test: {
+ globalSetup: 'test/setup.ts',
testTimeout: isWindows ? 60000 : 10000,
// Excluded plugin because it should throw an error when accidentally loaded via Nuxt
- exclude: [...configDefaults.exclude, '**/this-should-not-load.spec.js']
+ exclude: [...configDefaults.exclude, '**/this-should-not-load.spec.js'],
+ maxThreads: process.env.NUXT_TEST_DEV ? 1 : undefined,
+ minThreads: process.env.NUXT_TEST_DEV ? 1 : undefined
}
})