diff --git a/packages/fresh/src/runtime/client/partials.ts b/packages/fresh/src/runtime/client/partials.ts index ab7cbb35bb4..008e23a9592 100644 --- a/packages/fresh/src/runtime/client/partials.ts +++ b/packages/fresh/src/runtime/client/partials.ts @@ -83,7 +83,7 @@ if (!history.state) { history.replaceState(state, document.title); } -function maybeUpdateHistory(nextUrl: URL) { +function maybePushHistory(nextUrl: URL) { // Only add history entry when URL is new. Still apply // the partials because sometimes users click a link to // "refresh" the current page. @@ -106,6 +106,12 @@ function maybeUpdateHistory(nextUrl: URL) { } } +function maybeReplaceHistory(nextUrl: URL) { + if (nextUrl.href !== globalThis.location.href) { + history.replaceState(history.state, "", nextUrl.href); + } +} + document.addEventListener("click", async (e) => { let el = e.target; if (el && (el instanceof HTMLElement || el instanceof SVGElement)) { @@ -154,7 +160,7 @@ document.addEventListener("click", async (e) => { const nextUrl = new URL(el.href); try { - maybeUpdateHistory(nextUrl); + maybePushHistory(nextUrl); const partialUrl = new URL( partial ? partial : nextUrl.href, @@ -361,6 +367,7 @@ async function fetchPartials( actualUrl = nextUrl; } } + actualUrl.searchParams.delete(PARTIAL_SEARCH_PARAM); try { await applyPartials(res); @@ -375,7 +382,7 @@ async function fetchPartials( } if (shouldNavigate) { - maybeUpdateHistory(actualUrl); + maybeReplaceHistory(actualUrl); } } diff --git a/packages/fresh/tests/partials_test.tsx b/packages/fresh/tests/partials_test.tsx index 1ef3a78f5a8..9b4a17d510b 100644 --- a/packages/fresh/tests/partials_test.tsx +++ b/packages/fresh/tests/partials_test.tsx @@ -3498,3 +3498,93 @@ Deno.test({ }); }, }); + +Deno.test({ + name: "partials - redirect does not create back-button trap", + fn: async () => { + const app = testApp() + .get("/target", (ctx) => { + return ctx.render( + + +

target page

+
+
, + ); + }) + .get("/redirect", (ctx) => ctx.redirect("/target")) + .get("/", (ctx) => { + return ctx.render( + +
+ go + +

home

+
+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator(".init").wait(); + + // Click link that redirects + await page.locator(".nav").click(); + await page.locator(".done").wait(); + + // Should show the redirect target URL, not the intermediate + await page.waitForFunction(() => { + return window.location.pathname === "/target"; + }); + + // Going back should return to home, not to /redirect + await page.evaluate(() => window.history.go(-1)); + await page.locator(".init").wait(); + await page.waitForFunction(() => { + return window.location.pathname === "/"; + }); + }); + }, +}); + +Deno.test({ + name: "partials - redirect does not leak ?fresh-partial in URL", + fn: async () => { + const app = testApp() + .get("/target", (ctx) => { + return ctx.render( + + +

target page

+
+
, + ); + }) + .get("/redirect", (ctx) => ctx.redirect("/target")) + .get("/", (ctx) => { + return ctx.render( + +
+ go + +

home

+
+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator(".init").wait(); + + await page.locator(".nav").click(); + await page.locator(".done").wait(); + + const url = await page.evaluate(() => window.location.href); + expect(url).not.toContain("fresh-partial"); + }); + }, +});