diff --git a/bifrost/renderer/onBeforeRoute.ts b/bifrost/renderer/onBeforeRoute.ts index 72c3d6c..c9351e0 100644 --- a/bifrost/renderer/onBeforeRoute.ts +++ b/bifrost/renderer/onBeforeRoute.ts @@ -10,17 +10,29 @@ const onBeforeRoute = (pageContext: PageContext) => { let currentVisit = pageContext._turbolinksVisit; + if (pageContext.isHistoryNavigation) { + if (!pageContext.pageContextsAborted?.length) { + // This can be called multiple times if guards throw redirect, only notify history pop once + Turbolinks.controller.historyPoppedToLocationWithRestorationIdentifier( + pageContext.urlOriginal, + "" + ); + } + // Initially, currentVisit was just created by historyPoppedToLocationWithRestorationIdentifier + // If abort rendering, we'd rather Vike pass _turbolinksVisit, but it does not, so we recover from Turbolinks global + // There is risk of race condition if a history nav and a regular nav happen at the same time. + currentVisit = Turbolinks.controller.currentVisit; + } + + if (pageContext.pageContextsAborted?.length && currentVisit) { + currentVisit.updateIfRedirect(pageContext.urlOriginal); + } + if (pageContext.isHistoryNavigation) { const snapshot = Turbolinks.controller.getCachedSnapshotForLocation( pageContext.urlOriginal ); - Turbolinks.controller.historyPoppedToLocationWithRestorationIdentifier( - pageContext.urlOriginal, - "" - ); - // currentVisit was just created by historyPoppedToLocationWithRestorationIdentifier - currentVisit = Turbolinks.controller.currentVisit; if (!!snapshot) { return { pageContext: { @@ -29,8 +41,6 @@ const onBeforeRoute = (pageContext: PageContext) => { }, }; } - } else if (pageContext.pageContextsAborted && currentVisit) { - currentVisit.updateIfRedirect(pageContext.urlOriginal); } return { pageContext: { _turbolinksVisit: currentVisit } }; } diff --git a/tests/e2e/specs/e2e.spec.ts b/tests/e2e/specs/e2e.spec.ts index 9cdb76a..0de8c6e 100644 --- a/tests/e2e/specs/e2e.spec.ts +++ b/tests/e2e/specs/e2e.spec.ts @@ -4,6 +4,7 @@ import { expectNoMoreScripts, ensureNoBrowserNavigation, storeConsoleLog, + waitForConsoleLog, validateDOMOnTurbolinks, StringMatcher, sleep, @@ -83,6 +84,44 @@ test.describe("pages", () => { ).toHaveCount(1); }); + test("back button to a page proxied by render() fires each turbolinks event once", async ({ + page, + }) => { + ensureAllNetworkSucceeds(page); + await page.goto("./proxy-to", { waitUntil: "networkidle" }); + await waitForTurbolinksInit(page); + await expect(page).toHaveTitle("b"); + + await ensureNoBrowserNavigation(page, async () => { + const loaded = waitForConsoleLog(page, (m) => m.text() === T.load); + await page.getByText("vite page").click(); + await loaded; + }); + await expect(page).toHaveTitle("vite page"); + + // capture only the events from the back navigation + const logs = storeConsoleLog(page); + await ensureNoBrowserNavigation(page, async () => { + const loaded = waitForConsoleLog(page, (m) => m.text() === T.load); + await page.goBack(); + await loaded; + }); + + await expect(page).toHaveTitle("b"); + await expect( + page.locator("nav", { hasText: "Main Nav Layout" }) + ).toHaveCount(1); + + // restoration visits skip click/before-visit, and each event fires exactly once + expect(logs.filter((s) => s.startsWith("turbolinks:"))).toEqual([ + T.visit, + T.beforeRender, + T.render, + T.load, + ]); + await expectNoMoreScripts(page); + }); + test("it serves vite pages", async ({ page }) => { await page.goto("./vite-page"); diff --git a/tests/vite/pages/vite-page/+Page.tsx b/tests/vite/pages/vite-page/+Page.tsx index c7cda3d..74cddd4 100644 --- a/tests/vite/pages/vite-page/+Page.tsx +++ b/tests/vite/pages/vite-page/+Page.tsx @@ -44,6 +44,7 @@ export default function Page() { redirect page anchor link