diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index dee5a5c3764b8..7ebc992fa8e2e 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -989,6 +989,49 @@ it.live( ).pipe(Effect.provide(layer)), ); +it.effect("a navigation that fails while a load reads its title keeps its failure", () => + Effect.scoped( + Effect.gen(function* () { + const { tabId } = yield* ready; + const manager = yield* Manager.PreviewManager; + const events = yield* manager.subscribeEvents; + const page = contexts[0]!.page; + const heldTitle = Promise.withResolvers(); + page.title.mockImplementationOnce(() => heldTitle.promise); + yield* Effect.promise(() => page.goto("https://example.com/first")); + const failing = { + url: () => "https://example.com/missing", + method: () => "GET", + isNavigationRequest: () => true, + frame: () => page, + failure: () => ({ errorText: "net::ERR_NAME_NOT_RESOLVED" }), + }; + page.emit("request", failing); + page.emit("requestfailed", failing); + heldTitle.resolve("Held"); + // A load at the same await depth reports after the held one, so its Success marks the end. + page.emit("load"); + + const statuses: Array = []; + while (!statuses.at(-1)?.startsWith("Success")) { + const event = yield* PubSub.take(events); + if (event.type === "failed" && event.tabId === tabId) { + statuses.push(`LoadFailed ${event.url}`); + } else if (event.type === "navigated" && event.tabId === tabId) { + const status = event.snapshot.navStatus; + if (status._tag === "Success") statuses.push(`Success ${status.title}`); + else if (status._tag === "Loading") statuses.push(`Loading ${status.url}`); + } + } + expect(statuses).toEqual([ + "Loading https://example.com/missing", + "LoadFailed https://example.com/missing", + "Success test page", + ]); + }), + ).pipe(Effect.provide(layer)), +); + it.effect.each([ { name: "hover burst before a click", diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 9947aed88f692..1587c56a3cc2e 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -590,8 +590,10 @@ const make = Effect.gen(function* () { const url = tab.page.url(); // Chromium's error page loads after `requestfailed` and must not clear LoadFailed. if (url === "about:blank" || url.startsWith("chrome-error://")) return; + // A navigation that starts and fails during the title read leaves the URL unchanged. + const generation = tab.navigationGeneration; const title = (await tab.page.title().catch(() => "")).slice(0, 512); - if (tab.loading || tab.page.url() !== url) return; + if (tab.navigationGeneration !== generation || tab.loading || tab.page.url() !== url) return; report(tab, { _tag: "Success", url: ServerBrowserPage.withoutDataPayload(url).slice(0, 2048),