diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5b227224b4..d534abb877d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,15 +12,26 @@ on: branches: - main -jobs: - build: - runs-on: ${{ matrix.os }} +# https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml +env: + # 7 GiB by default on GitHub, setting to 6 GiB + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + NODE_OPTIONS: --max-old-space-size=6144 + # install playwright binary manually (because pnpm only runs install script once) + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" - strategy: - matrix: - os: [ubuntu-latest] - node: [16] +# Remove default permissions of GITHUB_TOKEN for security +# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: ${{ github.event_name != 'push' }} + +jobs: + build: + runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -28,7 +39,7 @@ jobs: - run: corepack enable - uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node }} + node-version: 18 cache: "pnpm" - name: Install dependencies @@ -37,20 +48,18 @@ jobs: - name: Build run: pnpm build + - name: Test (types) + run: pnpm test:types + - name: Cache dist - uses: actions/cache@v3 + uses: actions/upload-artifact@v3 with: + retention-days: 5 + name: dist path: packages/*/dist - key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} lint: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [16] - + runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -58,7 +67,7 @@ jobs: - run: corepack enable - uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node }} + node-version: 18 cache: "pnpm" - name: Install dependencies @@ -67,40 +76,19 @@ jobs: - name: Lint run: pnpm lint - typecheck: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [16] - - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - cache: "pnpm" - - - name: Install dependencies - run: pnpm install - - - name: Build (stub) - run: pnpm build:stub - - - name: Typecheck - run: pnpm typecheck - test-fixtures: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest] + env: ['dev', 'built'] + builder: ['vite', 'webpack'] node: [16] + exclude: + - env: 'dev' + builder: 'webpack' timeout-minutes: 10 @@ -115,22 +103,28 @@ jobs: - name: Install dependencies run: pnpm install - # https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml#L62 # Install playwright's binary under custom directory to cache - - name: Set Playwright path + - name: (non-windows) Set Playwright path and Get playwright version if: runner.os != 'Windows' - run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright-bin" >> $GITHUB_ENV - - name: Set Playwright path (windows) + run: | + echo "PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright-bin" >> $GITHUB_ENV + PLAYWRIGHT_VERSION="$(pnpm ls --depth 0 --json -w playwright | jq --raw-output '.[0].unsavedDependencies["playwright"].version')" + echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV + + - name: (windows) Set Playwright path and Get playwright version if: runner.os == 'Windows' - run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME\.cache\playwright-bin" >> $env:GITHUB_ENV + run: | + echo "PLAYWRIGHT_BROWSERS_PATH=$HOME\.cache\playwright-bin" >> $env:GITHUB_ENV + $env:PLAYWRIGHT_VERSION="$(pnpm ls --depth 0 --json -w playwright | jq --raw-output '.[0].unsavedDependencies[\"playwright\"].version')" + echo "PLAYWRIGHT_VERSION=$env:PLAYWRIGHT_VERSION" >> $env:GITHUB_ENV - name: Cache Playwright's binary uses: actions/cache@v3 with: - # Playwright removes unused browsers automatically - # So does not need to add playwright version to key - key: ${{ runner.os }}-playwright-bin-v1 + key: ${{ runner.os }}-playwright-bin-v1-${{ env.PLAYWRIGHT_VERSION }} path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + restore-keys: | + ${{ runner.os }}-playwright-bin-v1- - name: Install Playwright # does not need to explicitly set chromium after https://github.com/microsoft/playwright/issues/14862 is solved @@ -139,96 +133,23 @@ jobs: - name: Build (stub) run: pnpm build:stub + - name: Typecheck + run: pnpm typecheck + env: + TEST_ENV: ${{ matrix.env }} + TEST_BUILDER: ${{ matrix.builder }} + - name: Test (unit) run: pnpm test:unit + env: + TEST_ENV: ${{ matrix.env }} + TEST_BUILDER: ${{ matrix.builder }} - name: Test (fixtures) run: pnpm test:fixtures - - - name: Test (fixtures with dev) - run: pnpm test:fixtures:dev env: - NODE_OPTIONS: --max-old-space-size=8192 - - test-fixtures-webpack: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - node: [16] - - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - cache: "pnpm" - - - name: Install dependencies - run: pnpm install - - # https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml#L62 - # Install playwright's binary under custom directory to cache - - name: Set Playwright path (non-windows) - if: runner.os != 'Windows' - run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright-bin" >> $GITHUB_ENV - - name: Set Playwright path (windows) - if: runner.os == 'Windows' - run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME\.cache\playwright-bin" >> $env:GITHUB_ENV - - - name: Cache Playwright's binary - uses: actions/cache@v3 - with: - # Playwright removes unused browsers automatically - # So does not need to add playwright version to key - key: ${{ runner.os }}-playwright-bin-v1 - path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} - - - name: Install Playwright - # does not need to explicitly set chromium after https://github.com/microsoft/playwright/issues/14862 is solved - run: pnpm playwright install chromium - - - name: Build (stub) - run: pnpm build:stub - - - name: Test (fixtures) - run: pnpm test:fixtures:webpack - - test-types: - needs: - - build - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [16] - - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - cache: "pnpm" - - - name: Install dependencies - run: pnpm install - - - name: Restore dist cache - uses: actions/cache@v3 - with: - path: packages/*/dist - key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} - - - name: Test (types) - run: pnpm test:types + TEST_ENV: ${{ matrix.env }} + TEST_BUILDER: ${{ matrix.builder }} build-release: if: | @@ -240,8 +161,6 @@ jobs: - lint - build - test-fixtures - - test-fixtures-webpack - - test-types runs-on: ${{ matrix.os }} strategy: @@ -263,10 +182,10 @@ jobs: run: pnpm install - name: Restore dist cache - uses: actions/cache@v3 + uses: actions/download-artifact@v3 with: - path: packages/*/dist - key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} + name: dist + path: packages - name: Release Edge run: ./scripts/release-edge.sh diff --git a/.nuxtrc b/.nuxtrc new file mode 100644 index 00000000000..f54e61d4ffa --- /dev/null +++ b/.nuxtrc @@ -0,0 +1 @@ +telemetry.enabled=false diff --git a/package.json b/package.json index b47bd6038c8..a9a5ce021e6 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,14 @@ "lint": "eslint --ext .vue,.ts,.js,.mjs .", "lint:docs": "markdownlint ./docs/content/1.docs && case-police 'docs/content/1.docs/**/*.md'", "lint:docs:fix": "markdownlint ./docs/content/1.docs --fix && case-police 'docs/content/1.docs/**/*.md' --fix", - "nuxi": "NUXT_TELEMETRY_DISABLED=1 JITI_ESM_RESOLVE=1 nuxi", - "nuxt": "NUXT_TELEMETRY_DISABLED=1 JITI_ESM_RESOLVE=1 nuxi", + "nuxi": "JITI_ESM_RESOLVE=1 nuxi", + "nuxt": "JITI_ESM_RESOLVE=1 nuxi", "play": "pnpm nuxi dev playground", "play:build": "pnpm nuxi build playground", "play:preview": "pnpm nuxi preview playground", - "test:fixtures": "NUXT_TELEMETRY_DISABLED=1 pnpm nuxi prepare test/fixtures/basic && JITI_ESM_RESOLVE=1 vitest run --dir test", - "test:fixtures:dev": "NUXT_TELEMETRY_DISABLED=1 NUXT_TEST_DEV=true pnpm test:fixtures", - "test:fixtures:webpack": "NUXT_TELEMETRY_DISABLED=1 TEST_WITH_WEBPACK=1 pnpm test:fixtures", + "test:fixtures": "pnpm nuxi prepare test/fixtures/basic && JITI_ESM_RESOLVE=1 vitest run --dir test", + "test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures", + "test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures", "test:types": "pnpm nuxi prepare test/fixtures/basic && cd test/fixtures/basic && npx vue-tsc --noEmit", "test:unit": "JITI_ESM_RESOLVE=1 vitest run --dir packages", "typecheck": "tsc --noEmit" diff --git a/test/basic.test.ts b/test/basic.test.ts index a49028e5f38..775fb61f3ae 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -1,21 +1,27 @@ 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 { join, normalize } from 'pathe' +import { normalize } from 'pathe' // eslint-disable-next-line import/order 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' +import { expectNoClientErrors, expectWithPolling, renderPage, withLogs } from './utils' + +const isWebpack = process.env.TEST_BUILDER === 'webpack' -const fixturePath = join(fixturesDir, 'basic') await setup({ - rootDir: fixturePath, + rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), + dev: process.env.TEST_ENV === 'dev', server: true, browser: true, - setupTimeout: (isWindows ? 240 : 120) * 1000 + setupTimeout: (isWindows ? 240 : 120) * 1000, + nuxtConfig: { + builder: isWebpack ? 'webpack' : 'vite', + buildDir: process.env.NITRO_BUILD_DIR, + nitro: { output: { dir: process.env.NITRO_OUTPUT_DIR } } + } }) describe('server api', () => { @@ -543,7 +549,7 @@ describe('deferred app suspense resolve', () => { await page.waitForLoadState('networkidle') // Wait for all pending micro ticks to be cleared in case hydration haven't finished yet. - await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0))) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) const hydrationLogs = logs.filter(log => log.includes('isHydrating')) expect(hydrationLogs.length).toBe(3) @@ -571,7 +577,7 @@ describe('page key', () => { // Wait for all pending micro ticks to be cleared, // so we are not resolved too early when there are repeated page loading - await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0))) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) expect(logs.filter(l => l.includes('Child Setup')).length).toBe(1) }) @@ -590,7 +596,7 @@ describe('page key', () => { // Wait for all pending micro ticks to be cleared, // so we are not resolved too early when there are repeated page loading - await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0))) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) expect(logs.filter(l => l.includes('Child Setup')).length).toBe(2) }) @@ -611,7 +617,7 @@ describe('layout change not load page twice', () => { // Wait for all pending micro ticks to be cleared, // so we are not resolved too early when there are repeated page loading - await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0))) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) expect(logs.filter(l => l.includes('Layout2 Page Setup')).length).toBe(1) }) @@ -633,7 +639,7 @@ describe('automatically keyed composables', () => { }) }) -describe.skipIf(process.env.NUXT_TEST_DEV || process.env.TEST_WITH_WEBPACK)('inlining component styles', () => { +describe.skipIf(isDev() || isWebpack)('inlining component styles', () => { it('should inline styles', async () => { const html = await $fetch('/styles') for (const style of [ @@ -680,28 +686,29 @@ describe('prefetching', () => { }) }) -describe.runIf(process.env.NUXT_TEST_DEV)('detecting invalid root nodes', () => { - it('should detect invalid root nodes in pages', async () => { - for (const path of ['1', '2', '3', '4']) { - const { consoleLogs } = await renderPage(joinURL('/invalid-root', path)) - const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning').map(w => w.text).join('\n') - expect(consoleLogsWarns).toContain('does not have a single root node and will cause errors when navigating between routes') - } +describe.runIf(isDev())('detecting invalid root nodes', () => { + it.each(['1', '2', '3', '4'])('should detect invalid root nodes in pages (\'/invalid-root/%s\')', async (path) => { + const { consoleLogs, page } = await renderPage(joinURL('/invalid-root', path)) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) + await expectWithPolling( + () => consoleLogs + .map(w => w.text).join('\n') + .includes('does not have a single root node and will cause errors when navigating between routes'), + true + ) }) - it('should not complain if there is no transition', async () => { - for (const path of ['fine']) { - const { consoleLogs } = await renderPage(joinURL('/invalid-root', path)) - - const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning') + it.each(['fine'])('should not complain if there is no transition (%s)', async (path) => { + const { consoleLogs, page } = await renderPage(joinURL('/invalid-root', path)) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) - expect(consoleLogsWarns.length).toEqual(0) - } + const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning') + expect(consoleLogsWarns.length).toEqual(0) }) }) // TODO: dynamic paths in dev -describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { +describe.skipIf(isDev())('dynamic paths', () => { it('should work with no overrides', async () => { const html: string = await $fetch('/assets') for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { @@ -711,7 +718,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { }) // webpack injects CSS differently - it.skipIf(process.env.TEST_WITH_WEBPACK)('adds relative paths to CSS', async () => { + it.skipIf(isWebpack)('adds relative paths to CSS', async () => { const html: string = await $fetch('/assets') const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)).map(m => m[2] || m[3]) const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u)) @@ -740,7 +747,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { url.startsWith('/foo/_other/') || url === '/foo/public.svg' || // TODO: webpack does not yet support dynamic static paths - (process.env.TEST_WITH_WEBPACK && url === '/public.svg') + (isWebpack && url === '/public.svg') ).toBeTruthy() } }) @@ -757,7 +764,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { url.startsWith('./_nuxt/') || url === './public.svg' || // TODO: webpack does not yet support dynamic static paths - (process.env.TEST_WITH_WEBPACK && url === '/public.svg') + (isWebpack && url === '/public.svg') ).toBeTruthy() expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy() } @@ -785,7 +792,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { url.startsWith('https://example.com/_cdn/') || url === 'https://example.com/public.svg' || // TODO: webpack does not yet support dynamic static paths - (process.env.TEST_WITH_WEBPACK && url === '/public.svg') + (isWebpack && url === '/public.svg') ).toBeTruthy() } }) @@ -819,7 +826,7 @@ describe('component islands', () => { it('renders components with route', async () => { const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo') - if (process.env.NUXT_TEST_DEV) { + if (isDev()) { result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates')) } @@ -846,7 +853,7 @@ describe('component islands', () => { }) })) - if (process.env.NUXT_TEST_DEV) { + if (isDev()) { result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates')) const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url))) for (const link of result.head.link) { @@ -860,7 +867,8 @@ describe('component islands', () => { key: s.key.replace(/-[a-zA-Z0-9]+$/, '') })) - if (!(process.env.NUXT_TEST_DEV || process.env.TEST_WITH_WEBPACK)) { + // TODO: fix rendering of styles in webpack + if (!isDev() && !isWebpack) { expect(result.head).toMatchInlineSnapshot(` { "link": [], @@ -872,7 +880,7 @@ describe('component islands', () => { ], } `) - } else if (process.env.NUXT_TEST_DEV) { + } else if (isDev() && !isWebpack) { expect(result.head).toMatchInlineSnapshot(` { "link": [ @@ -908,7 +916,7 @@ describe('component islands', () => { }) }) -describe.runIf(process.env.NUXT_TEST_DEV && !process.env.TEST_WITH_WEBPACK)('vite plugins', () => { +describe.runIf(isDev() && !isWebpack)('vite plugins', () => { it('does not override vite plugins', async () => { expect(await $fetch('/vite-plugin-without-path')).toBe('vite-plugin without path') expect(await $fetch('/__nuxt-test')).toBe('vite-plugin with __nuxt prefix') @@ -918,7 +926,7 @@ describe.runIf(process.env.NUXT_TEST_DEV && !process.env.TEST_WITH_WEBPACK)('vit }) }) -describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', () => { +describe.skipIf(isDev() || isWindows)('payload rendering', () => { it('renders a payload', async () => { const payload = await $fetch('/random/a/_payload.js', { responseType: 'text' }) expect(payload).toMatch( @@ -937,7 +945,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', () await page.goto(url('/random/a')) await page.waitForLoadState('networkidle') - const importSuffix = process.env.NUXT_TEST_DEV && !process.env.TEST_WITH_WEBPACK ? '?import' : '' + const importSuffix = isDev() && !isWebpack ? '?import' : '' // We are manually prefetching other payloads expect(requests).toContain('/random/c/_payload.js') @@ -970,7 +978,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', () // We are not refetching payloads we've already prefetched // Note: we refetch on dev as urls differ between '' and '?import' - // expect(requests.filter(p => p.includes('_payload')).length).toBe(process.env.NUXT_TEST_DEV ? 1 : 0) + // expect(requests.filter(p => p.includes('_payload')).length).toBe(isDev() ? 1 : 0) }) }) @@ -995,64 +1003,3 @@ 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([]) - }, 60_000) - - it('should detect new routes', async () => { - const html = await $fetch('/some-404') - expect(html).toContain('404 at some-404') - - // write new page route - const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8') - await fsp.writeFile(join(fixturePath, 'pages/some-404.vue'), indexVue) - - await expectWithPolling( - () => $fetch('/some-404').then(r => r.includes('Hello Nuxt 3') ? 'ok' : 'fail'), - 'ok' - ) - }) - }) -} diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index d6e6fb85908..39a8ea02829 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -21,7 +21,7 @@ export default defineNuxtConfig({ } }, buildDir: process.env.NITRO_BUILD_DIR, - builder: process.env.TEST_WITH_WEBPACK ? 'webpack' : 'vite', + builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite', build: { transpile: [ (ctx) => { @@ -70,7 +70,7 @@ export default defineNuxtConfig({ } ], function (_, nuxt) { - if (process.env.TEST_WITH_WEBPACK) { return } + if (typeof nuxt.options.builder === 'string' && nuxt.options.builder.includes('webpack')) { return } nuxt.options.css.push('virtual.css') nuxt.options.build.transpile.push('virtual.css') diff --git a/test/hmr.test.ts b/test/hmr.test.ts new file mode 100644 index 00000000000..498e531180f --- /dev/null +++ b/test/hmr.test.ts @@ -0,0 +1,87 @@ +import { promises as fsp } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { isWindows } from 'std-env' +import { join } from 'pathe' +// eslint-disable-next-line import/order +import { setup, $fetch } from '@nuxt/test-utils' + +import { expectWithPolling, renderPage } from './utils' + +const isWebpack = process.env.TEST_BUILDER === 'webpack' + +// TODO: fix HMR on Windows +if (process.env.TEST_ENV !== 'built' && !isWindows) { + const fixturePath = fileURLToPath(new URL('./fixtures-temp/basic', import.meta.url)) + await setup({ + rootDir: fixturePath, + dev: true, + server: true, + browser: true, + setupTimeout: (isWindows ? 240 : 120) * 1000, + nuxtConfig: { + builder: isWebpack ? 'webpack' : 'vite', + buildDir: process.env.NITRO_BUILD_DIR, + nitro: { output: { dir: process.env.NITRO_OUTPUT_DIR } } + } + }) + + 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([]) + }, 60_000) + + it('should detect new routes', async () => { + const html = await $fetch('/some-404') + expect(html).toContain('404 at some-404') + + // write new page route + const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8') + await fsp.writeFile(join(fixturePath, 'pages/some-404.vue'), indexVue) + + await expectWithPolling( + () => $fetch('/some-404').then(r => r.includes('Hello Nuxt 3')), + true + ) + }) + }) +} else { + describe.skip('hmr', () => {}) +} diff --git a/test/utils.ts b/test/utils.ts index 5c566996c42..dfd19d29bc6 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,10 +1,7 @@ -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) { @@ -53,21 +50,22 @@ export async function expectNoClientErrors (path: string) { expect(consoleLogWarnings).toEqual([]) } +type EqualityVal = string | number | boolean | null | undefined | RegExp export async function expectWithPolling ( - get: () => Promise | string, - expected: string, + get: () => Promise | EqualityVal, + expected: EqualityVal, retries = process.env.CI ? 100 : 30, delay = process.env.CI ? 500 : 100 ) { - let result: string | undefined + let result: EqualityVal for (let i = retries; i >= 0; i--) { result = await get() - if (result === expected) { + if (result?.toString() === expected?.toString()) { break } await new Promise(resolve => setTimeout(resolve, delay)) } - expect(result).toEqual(expected) + expect(result?.toString(), `"${result?.toString()}" did not equal "${expected?.toString()}" in ${retries * delay}ms`).toEqual(expected?.toString()) } export async function withLogs (callback: (page: Page, logs: string[]) => Promise) { @@ -76,8 +74,8 @@ export async function withLogs (callback: (page: Page, logs: string[]) => Promis const logs: string[] = [] page.on('console', (msg) => { const text = msg.text() - if (done) { - throw new Error('Test finished prematurely') + if (done && !text.includes('[vite] server connection lost')) { + throw new Error(`Test finished prematurely before log: [${msg.type()}] ${text}`) } logs.push(text) }) diff --git a/vitest.config.ts b/vitest.config.ts index a1a6b95f44b..ac9e6998bfc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ deps: { inline: ['@vitejs/plugin-vue'] }, // Excluded plugin because it should throw an error when accidentally loaded via Nuxt 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 + maxThreads: process.env.TEST_ENV === 'dev' ? 1 : undefined, + minThreads: process.env.TEST_ENV === 'dev' ? 1 : undefined } })