From edc5b1193e7d5ef883f85b488e24cafe0f624551 Mon Sep 17 00:00:00 2001 From: Valeriia Radchenko Date: Mon, 9 Nov 2026 12:56:10 -0700 Subject: [PATCH] test: wait for HMR fixture readiness after navigation --- playground/hmr/__tests__/hmr.spec.ts | 42 ++++++++++++++++--- .../hmr-no-partial-accept.spec.ts | 7 ++-- playground/hmr/vite.config.ts | 27 ++++++++++++ playground/test-utils.ts | 11 ----- 4 files changed, 68 insertions(+), 19 deletions(-) diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index d4313e1d3e77ba..09e3f389bd0c3c 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -10,7 +10,6 @@ import { editFile, getBg, getColor, - gotoLatestBuild, hotUpdatedLog, isBuild, isBundledDev, @@ -524,8 +523,10 @@ if (!isBuild) { const CONNECTED = /connected/ const baseDir = 'accept-exports' - const openPage = (testDir: string) => - gotoLatestBuild(`${viteTestUrl}/${testDir}/`) + const openPage = async (testDir: string) => { + await page.goto(`${viteTestUrl}/${testDir}/`, { waitUntil: 'commit' }) + await page.waitForSelector('html[data-hmr-ready="true"]') + } describe('when all used exports are accepted', () => { const testDir = baseDir + '/main-accepted' @@ -1144,7 +1145,10 @@ if (!isBuild) { "export { value as childValue } from './child'", ), ) - }, 'file-delete-restore/child.js hot data after prune: undefined') + }, [ + 'file-delete-restore/child.js hot data after prune: undefined', + hotUpdatedLog(parentFile), + ]) await expect .poll(() => page.textContent('.file-delete-restore')) .toMatch('parent:child') @@ -1352,7 +1356,35 @@ if (!isBuild) { ) test('import.meta.hot?.accept', async () => { - await gotoLatestBuild(viteTestUrl) + if (isBundledDev) { + // Navigation can finish before the new HMR client registers. + let delayRegistration = true + await page.routeWebSocket('**', (socket) => { + const server = socket.connectToServer() + const queuedMessages: (string | Buffer)[] = [] + let queueMessages = false + socket.onMessage((message) => { + if ( + delayRegistration && + String(message).includes('vite:client-connected') + ) { + delayRegistration = false + queueMessages = true + setTimeout(() => { + for (const queued of queuedMessages) server.send(queued) + queueMessages = false + }, 500) + } + if (queueMessages) { + queuedMessages.push(message) + } else { + server.send(message) + } + }) + }) + } + await page.goto(viteTestUrl) + await page.waitForSelector('html[data-hmr-ready="true"]') const el = page.locator('.optional-chaining') await untilBrowserLogAfter( diff --git a/playground/hmr/__tests__/no-partial-accept/hmr-no-partial-accept.spec.ts b/playground/hmr/__tests__/no-partial-accept/hmr-no-partial-accept.spec.ts index 5f242bff704e03..7aeb3c09d276e0 100644 --- a/playground/hmr/__tests__/no-partial-accept/hmr-no-partial-accept.spec.ts +++ b/playground/hmr/__tests__/no-partial-accept/hmr-no-partial-accept.spec.ts @@ -1,7 +1,6 @@ import { beforeAll, describe, expect, it } from 'vitest' import { editFile, - gotoLatestBuild, hotUpdatedLog, isBuild, page, @@ -11,8 +10,10 @@ import { describe.runIf(!isBuild)('acceptExports with hmrPartialAccept off', () => { const CONNECTED = /connected/ - const openPage = (testDir: string) => - gotoLatestBuild(`${viteTestUrl}/${testDir}/`) + const openPage = async (testDir: string) => { + await page.goto(`${viteTestUrl}/${testDir}/`, { waitUntil: 'commit' }) + await page.waitForSelector('html[data-hmr-ready="true"]') + } describe('importer that uses only accepted exports', () => { const testDir = 'accept-exports/main-accepted' diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index ccf615f0b10b51..131f7fc2be4619 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -61,6 +61,33 @@ export default defineConfig(({ command }) => ({ }, }, plugins: [ + // A round trip settles fallback reloads and client registration before edits. + { + name: 'test-hmr-ready', + apply: 'serve', + configureServer(server) { + server.environments.client.hot.on('test:ready', (_data, client) => { + client.send('test:ready') + }) + }, + transformIndexHtml: { + order: 'pre', + handler() { + return [ + { + tag: 'script', + attrs: { type: 'module' }, + children: ` + import.meta.hot.on('test:ready', () => { + document.documentElement.dataset.hmrReady = 'true' + }) + import.meta.hot.send('test:ready') + `, + }, + ] + }, + }, + }, { name: 'mock-custom', async hotUpdate({ file, read }) { diff --git a/playground/test-utils.ts b/playground/test-utils.ts index 038a5a8a34bedd..a6c07f0ee885c5 100644 --- a/playground/test-utils.ts +++ b/playground/test-utils.ts @@ -25,7 +25,6 @@ import { page, sourcemapSnapshot, testDir, - viteServer, watchBuilds, } from './vitestSetupUtils' @@ -282,16 +281,6 @@ export function readDepOptimizationMetadata( ) } -export async function gotoLatestBuild(url: string): Promise { - if (isBundledDev) { - // Earlier HMR edits leave the bundled-dev output stale. Opening a page then - // serves the fallback page first, which reloads into the real page later. - const bundledDev = viteServer.environments.client.bundledDev as any - await bundledDev.devEngine.ensureLatestBuildOutput() - } - await page.goto(url) -} - /** * Wait for every output containing the edited module to finish rebuilding. * Workaround for Rolldown emitting END per output before all outputs finish: