ci: run webpack/vite and dev/prod as matrices (#18905)

This commit is contained in:
Daniel Roe 2023-02-13 22:09:32 +00:00 committed by GitHub
parent aa409299a7
commit d036d3dec5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 224 additions and 272 deletions

View File

@ -12,15 +12,26 @@ on:
branches: branches:
- main - main
# 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"
# 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: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest]
node: [16]
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
@ -28,7 +39,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: 18
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -37,20 +48,18 @@ jobs:
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Test (types)
run: pnpm test:types
- name: Cache dist - name: Cache dist
uses: actions/cache@v3 uses: actions/upload-artifact@v3
with: with:
retention-days: 5
name: dist
path: packages/*/dist path: packages/*/dist
key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }}
lint: lint:
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest]
node: [16]
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
@ -58,7 +67,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: 18
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -67,13 +76,19 @@ jobs:
- name: Lint - name: Lint
run: pnpm lint run: pnpm lint
typecheck: test-fixtures:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest, windows-latest]
env: ['dev', 'built']
builder: ['vite', 'webpack']
node: [16] node: [16]
exclude:
- env: 'dev'
builder: 'webpack'
timeout-minutes: 10 timeout-minutes: 10
@ -88,147 +103,53 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
# Install playwright's binary under custom directory to cache
- 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
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
$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:
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
run: pnpm playwright install chromium
- name: Build (stub) - name: Build (stub)
run: pnpm build:stub run: pnpm build:stub
- name: Typecheck - name: Typecheck
run: pnpm typecheck run: pnpm typecheck
env:
test-fixtures: TEST_ENV: ${{ matrix.env }}
runs-on: ${{ matrix.os }} TEST_BUILDER: ${{ matrix.builder }}
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
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 (unit) - name: Test (unit)
run: pnpm test:unit run: pnpm test:unit
env:
TEST_ENV: ${{ matrix.env }}
TEST_BUILDER: ${{ matrix.builder }}
- name: Test (fixtures) - name: Test (fixtures)
run: pnpm test:fixtures run: pnpm test:fixtures
- name: Test (fixtures with dev)
run: pnpm test:fixtures:dev
env: env:
NODE_OPTIONS: --max-old-space-size=8192 TEST_ENV: ${{ matrix.env }}
TEST_BUILDER: ${{ matrix.builder }}
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
build-release: build-release:
if: | if: |
@ -240,8 +161,6 @@ jobs:
- lint - lint
- build - build
- test-fixtures - test-fixtures
- test-fixtures-webpack
- test-types
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -263,10 +182,10 @@ jobs:
run: pnpm install run: pnpm install
- name: Restore dist cache - name: Restore dist cache
uses: actions/cache@v3 uses: actions/download-artifact@v3
with: with:
path: packages/*/dist name: dist
key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} path: packages
- name: Release Edge - name: Release Edge
run: ./scripts/release-edge.sh run: ./scripts/release-edge.sh

1
.nuxtrc Normal file
View File

@ -0,0 +1 @@
telemetry.enabled=false

View File

@ -14,14 +14,14 @@
"lint": "eslint --ext .vue,.ts,.js,.mjs .", "lint": "eslint --ext .vue,.ts,.js,.mjs .",
"lint:docs": "markdownlint ./docs/content/1.docs && case-police 'docs/content/1.docs/**/*.md'", "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", "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", "nuxi": "JITI_ESM_RESOLVE=1 nuxi",
"nuxt": "NUXT_TELEMETRY_DISABLED=1 JITI_ESM_RESOLVE=1 nuxi", "nuxt": "JITI_ESM_RESOLVE=1 nuxi",
"play": "pnpm nuxi dev playground", "play": "pnpm nuxi dev playground",
"play:build": "pnpm nuxi build playground", "play:build": "pnpm nuxi build playground",
"play:preview": "pnpm nuxi preview 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": "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:dev": "TEST_ENV=dev pnpm test:fixtures",
"test:fixtures:webpack": "NUXT_TELEMETRY_DISABLED=1 TEST_WITH_WEBPACK=1 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: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", "test:unit": "JITI_ESM_RESOLVE=1 vitest run --dir packages",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"

View File

@ -1,21 +1,27 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { promises as fsp } from 'node:fs'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { joinURL, withQuery } from 'ufo' import { joinURL, withQuery } from 'ufo'
import { isWindows } from 'std-env' import { isWindows } from 'std-env'
import { join, normalize } from 'pathe' import { normalize } from 'pathe'
// eslint-disable-next-line import/order // eslint-disable-next-line import/order
import { setup, fetch, $fetch, startServer, isDev, createPage, url } from '@nuxt/test-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 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({ await setup({
rootDir: fixturePath, rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
dev: process.env.TEST_ENV === 'dev',
server: true, server: true,
browser: 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', () => { describe('server api', () => {
@ -543,7 +549,7 @@ describe('deferred app suspense resolve', () => {
await page.waitForLoadState('networkidle') await page.waitForLoadState('networkidle')
// Wait for all pending micro ticks to be cleared in case hydration haven't finished yet. // 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')) const hydrationLogs = logs.filter(log => log.includes('isHydrating'))
expect(hydrationLogs.length).toBe(3) expect(hydrationLogs.length).toBe(3)
@ -571,7 +577,7 @@ describe('page key', () => {
// Wait for all pending micro ticks to be cleared, // Wait for all pending micro ticks to be cleared,
// so we are not resolved too early when there are repeated page loading // 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) 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, // Wait for all pending micro ticks to be cleared,
// so we are not resolved too early when there are repeated page loading // 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) 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, // Wait for all pending micro ticks to be cleared,
// so we are not resolved too early when there are repeated page loading // 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) 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 () => { it('should inline styles', async () => {
const html = await $fetch('/styles') const html = await $fetch('/styles')
for (const style of [ for (const style of [
@ -680,28 +686,29 @@ describe('prefetching', () => {
}) })
}) })
describe.runIf(process.env.NUXT_TEST_DEV)('detecting invalid root nodes', () => { describe.runIf(isDev())('detecting invalid root nodes', () => {
it('should detect invalid root nodes in pages', async () => { it.each(['1', '2', '3', '4'])('should detect invalid root nodes in pages (\'/invalid-root/%s\')', async (path) => {
for (const path of ['1', '2', '3', '4']) { const { consoleLogs, page } = await renderPage(joinURL('/invalid-root', path))
const { consoleLogs } = await renderPage(joinURL('/invalid-root', path)) await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning').map(w => w.text).join('\n') await expectWithPolling(
expect(consoleLogsWarns).toContain('does not have a single root node and will cause errors when navigating between routes') () => 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 () => { it.each(['fine'])('should not complain if there is no transition (%s)', async (path) => {
for (const path of ['fine']) { const { consoleLogs, page } = await renderPage(joinURL('/invalid-root', path))
const { consoleLogs } = await renderPage(joinURL('/invalid-root', path)) await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning') const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning')
expect(consoleLogsWarns.length).toEqual(0)
expect(consoleLogsWarns.length).toEqual(0)
}
}) })
}) })
// TODO: dynamic paths in dev // 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 () => { it('should work with no overrides', async () => {
const html: string = await $fetch('/assets') const html: string = await $fetch('/assets')
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { 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 // 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 html: string = await $fetch('/assets')
const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)).map(m => m[2] || m[3]) 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)) 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.startsWith('/foo/_other/') ||
url === '/foo/public.svg' || url === '/foo/public.svg' ||
// TODO: webpack does not yet support dynamic static paths // TODO: webpack does not yet support dynamic static paths
(process.env.TEST_WITH_WEBPACK && url === '/public.svg') (isWebpack && url === '/public.svg')
).toBeTruthy() ).toBeTruthy()
} }
}) })
@ -757,7 +764,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => {
url.startsWith('./_nuxt/') || url.startsWith('./_nuxt/') ||
url === './public.svg' || url === './public.svg' ||
// TODO: webpack does not yet support dynamic static paths // TODO: webpack does not yet support dynamic static paths
(process.env.TEST_WITH_WEBPACK && url === '/public.svg') (isWebpack && url === '/public.svg')
).toBeTruthy() ).toBeTruthy()
expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy() 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.startsWith('https://example.com/_cdn/') ||
url === 'https://example.com/public.svg' || url === 'https://example.com/public.svg' ||
// TODO: webpack does not yet support dynamic static paths // TODO: webpack does not yet support dynamic static paths
(process.env.TEST_WITH_WEBPACK && url === '/public.svg') (isWebpack && url === '/public.svg')
).toBeTruthy() ).toBeTruthy()
} }
}) })
@ -819,7 +826,7 @@ describe('component islands', () => {
it('renders components with route', async () => { it('renders components with route', async () => {
const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo') 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')) 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')) 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))) const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url)))
for (const link of result.head.link) { for (const link of result.head.link) {
@ -860,7 +867,8 @@ describe('component islands', () => {
key: s.key.replace(/-[a-zA-Z0-9]+$/, '') 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(` expect(result.head).toMatchInlineSnapshot(`
{ {
"link": [], "link": [],
@ -872,7 +880,7 @@ describe('component islands', () => {
], ],
} }
`) `)
} else if (process.env.NUXT_TEST_DEV) { } else if (isDev() && !isWebpack) {
expect(result.head).toMatchInlineSnapshot(` expect(result.head).toMatchInlineSnapshot(`
{ {
"link": [ "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 () => { it('does not override vite plugins', async () => {
expect(await $fetch('/vite-plugin-without-path')).toBe('vite-plugin without path') expect(await $fetch('/vite-plugin-without-path')).toBe('vite-plugin without path')
expect(await $fetch('/__nuxt-test')).toBe('vite-plugin with __nuxt prefix') 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 () => { it('renders a payload', async () => {
const payload = await $fetch('/random/a/_payload.js', { responseType: 'text' }) const payload = await $fetch('/random/a/_payload.js', { responseType: 'text' })
expect(payload).toMatch( 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.goto(url('/random/a'))
await page.waitForLoadState('networkidle') 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 // We are manually prefetching other payloads
expect(requests).toContain('/random/c/_payload.js') 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 // We are not refetching payloads we've already prefetched
// Note: we refetch on dev as urls differ between '' and '?import' // 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') 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('<Title>Basic fixture</Title>', '<Title>Basic fixture HMR</Title>')
.replace('<h1>Hello Nuxt 3!</h1>', '<h1>Hello Nuxt 3! HMR</h1>')
indexVue += '<style scoped>\nh1 { color: red }\n</style>'
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'
)
})
})
}

View File

@ -21,7 +21,7 @@ export default defineNuxtConfig({
} }
}, },
buildDir: process.env.NITRO_BUILD_DIR, buildDir: process.env.NITRO_BUILD_DIR,
builder: process.env.TEST_WITH_WEBPACK ? 'webpack' : 'vite', builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite',
build: { build: {
transpile: [ transpile: [
(ctx) => { (ctx) => {
@ -70,7 +70,7 @@ export default defineNuxtConfig({
} }
], ],
function (_, nuxt) { 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.css.push('virtual.css')
nuxt.options.build.transpile.push('virtual.css') nuxt.options.build.transpile.push('virtual.css')

87
test/hmr.test.ts Normal file
View File

@ -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('<Title>Basic fixture</Title>', '<Title>Basic fixture HMR</Title>')
.replace('<h1>Hello Nuxt 3!</h1>', '<h1>Hello Nuxt 3! HMR</h1>')
indexVue += '<style scoped>\nh1 { color: red }\n</style>'
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', () => {})
}

View File

@ -1,10 +1,7 @@
import { fileURLToPath } from 'node:url'
import { expect } from 'vitest' import { expect } from 'vitest'
import type { Page } from 'playwright' import type { Page } from 'playwright'
import { createPage, getBrowser, url, useTestContext } from '@nuxt/test-utils' 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 = '/') { export async function renderPage (path = '/') {
const ctx = useTestContext() const ctx = useTestContext()
if (!ctx.options.browser) { if (!ctx.options.browser) {
@ -53,21 +50,22 @@ export async function expectNoClientErrors (path: string) {
expect(consoleLogWarnings).toEqual([]) expect(consoleLogWarnings).toEqual([])
} }
type EqualityVal = string | number | boolean | null | undefined | RegExp
export async function expectWithPolling ( export async function expectWithPolling (
get: () => Promise<string> | string, get: () => Promise<EqualityVal> | EqualityVal,
expected: string, expected: EqualityVal,
retries = process.env.CI ? 100 : 30, retries = process.env.CI ? 100 : 30,
delay = process.env.CI ? 500 : 100 delay = process.env.CI ? 500 : 100
) { ) {
let result: string | undefined let result: EqualityVal
for (let i = retries; i >= 0; i--) { for (let i = retries; i >= 0; i--) {
result = await get() result = await get()
if (result === expected) { if (result?.toString() === expected?.toString()) {
break break
} }
await new Promise(resolve => setTimeout(resolve, delay)) 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<void>) { export async function withLogs (callback: (page: Page, logs: string[]) => Promise<void>) {
@ -76,8 +74,8 @@ export async function withLogs (callback: (page: Page, logs: string[]) => Promis
const logs: string[] = [] const logs: string[] = []
page.on('console', (msg) => { page.on('console', (msg) => {
const text = msg.text() const text = msg.text()
if (done) { if (done && !text.includes('[vite] server connection lost')) {
throw new Error('Test finished prematurely') throw new Error(`Test finished prematurely before log: [${msg.type()}] ${text}`)
} }
logs.push(text) logs.push(text)
}) })

View File

@ -19,7 +19,7 @@ export default defineConfig({
deps: { inline: ['@vitejs/plugin-vue'] }, deps: { inline: ['@vitejs/plugin-vue'] },
// Excluded plugin because it should throw an error when accidentally loaded via Nuxt // 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, maxThreads: process.env.TEST_ENV === 'dev' ? 1 : undefined,
minThreads: process.env.NUXT_TEST_DEV ? 1 : undefined minThreads: process.env.TEST_ENV === 'dev' ? 1 : undefined
} }
}) })