diff --git a/src/app/auth/login/__tests__/page.browser.test.tsx b/src/app/auth/login/__tests__/page.browser.test.tsx
index e40943d97..c20442d4c 100644
--- a/src/app/auth/login/__tests__/page.browser.test.tsx
+++ b/src/app/auth/login/__tests__/page.browser.test.tsx
@@ -3,12 +3,19 @@
*
* Single sign-on leaves the page for the identity provider and the session
* starts on the way back, so the previous account's offline copies have to
- * be gone before the page is left.
+ * be gone before the page is left. A sign-in that stays in the app replaces
+ * the sign-in page in the history, so Back does not show the form again.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { stubFetch } from "../../../../../tests/browser/fetch-stub";
+import { page } from "vitest/browser";
+
+import {
+ stubFetch,
+ type FetchStub,
+} from "../../../../../tests/browser/fetch-stub";
import { renderWithApp } from "../../../../../tests/browser/render";
+import { recordHistory } from "../../../../../tests/browser/url";
const clearOffline = vi.hoisted(() => vi.fn(async () => {}));
const loadDocument = vi.hoisted(() => vi.fn());
@@ -20,8 +27,10 @@ vi.mock("@/lib/navigation/load-document", () => ({ loadDocument }));
import LoginPage from "../page";
+let api: FetchStub;
+
beforeEach(() => {
- stubFetch()
+ api = stubFetch()
.on("GET", "/api/auth/registration-status", {
data: { registrationEnabled: false, sessionCookieSecure: false },
})
@@ -50,3 +59,23 @@ describe("sign-in page, single sign-on", () => {
);
});
});
+
+describe("sign-in page, password", () => {
+ it("replaces itself with the page asked for", async () => {
+ api.on("POST", "/api/auth/login", { data: { id: "signed-in" } });
+ window.history.replaceState(null, "", "/auth/login?next=%2Fmood%3Fadd%3D1");
+ const history = recordHistory();
+ const { screen } = await renderWithApp(, { user: null });
+
+ await screen.getByTestId("login-use-password").click();
+ await screen.getByTestId("login-email").fill("someone");
+ await page
+ .elementLocator(document.querySelector("#password")!)
+ .fill("a long enough passphrase");
+ await screen.getByTestId("login-password-submit").click();
+
+ await expect.poll(() => window.location.pathname).toBe("/mood");
+ expect(window.location.search).toBe("?add=1");
+ expect(history.pushes()).toEqual([]);
+ });
+});
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
index 7aa4db1ce..60eb3b53b 100644
--- a/src/app/auth/login/page.tsx
+++ b/src/app/auth/login/page.tsx
@@ -236,7 +236,7 @@ export default function LoginPage() {
if (target === "/" && isDashboardSnapshotEnabled()) {
prefetchDashboardSnapshot(queryClient);
}
- router.push(target);
+ router.replace(target);
}
async function handlePasskeyLogin() {
diff --git a/src/app/auth/register/__tests__/page.browser.test.tsx b/src/app/auth/register/__tests__/page.browser.test.tsx
index 798139d4d..0182022db 100644
--- a/src/app/auth/register/__tests__/page.browser.test.tsx
+++ b/src/app/auth/register/__tests__/page.browser.test.tsx
@@ -3,7 +3,8 @@
*
* A new account starts from an empty cache, like every other sign-in: the
* client cache outlives the navigation, and nothing read before the account
- * existed belongs to it.
+ * existed belongs to it. The app replaces the page in the history, so Back
+ * does not show the form again.
*/
import { describe, expect, it } from "vitest";
import { page } from "vitest/browser";
@@ -12,6 +13,7 @@ import { queryKeys } from "@/lib/query-keys";
import { stubFetch } from "../../../../../tests/browser/fetch-stub";
import { renderWithApp } from "../../../../../tests/browser/render";
+import { recordHistory } from "../../../../../tests/browser/url";
import RegisterPage from "../page";
async function fill(selector: string, value: string): Promise {
@@ -24,6 +26,7 @@ describe("registration page", () => {
.on("GET", "/api/auth/me", { status: 401, error: "Unauthorized" })
.on("POST", "/api/auth/register", { data: { id: "new-account" } });
window.history.replaceState(null, "", "/auth/register");
+ const history = recordHistory();
const { screen, queryClient } = await renderWithApp(, {
user: null,
});
@@ -37,6 +40,7 @@ describe("registration page", () => {
await expect.poll(() => window.location.pathname).toBe("/");
expect(api.calls("POST", "/api/auth/register")).toHaveLength(1);
+ expect(history.pushes()).toEqual([]);
expect(queryClient.getQueryData(queryKeys.dashboardSnapshot())).toBe(
undefined,
);
diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx
index df5167099..d88ba1c32 100644
--- a/src/app/auth/register/page.tsx
+++ b/src/app/auth/register/page.tsx
@@ -74,7 +74,7 @@ export default function RegisterPage() {
});
queryClient.clear(); // nothing cached before belongs to the new account
- router.push("/");
+ router.replace("/");
} catch (err) {
setError(
err instanceof ApiError && err.message
diff --git a/src/components/auth/__tests__/claim-profile.browser.test.tsx b/src/components/auth/__tests__/claim-profile.browser.test.tsx
index 04d5cb3d8..210081d0d 100644
--- a/src/components/auth/__tests__/claim-profile.browser.test.tsx
+++ b/src/components/auth/__tests__/claim-profile.browser.test.tsx
@@ -2,9 +2,10 @@
* Taking over a managed profile, driven by the claim page's buttons.
*
* Both ways in start a session for a different account than whatever this
- * browser held before: the password claim stays in the single-page app and
- * starts from an empty cache, the single-sign-on claim leaves for the
- * identity provider and clears the offline copies before it goes.
+ * browser held before: the password claim stays in the single-page app,
+ * starts from an empty cache and replaces the claim page in the history; the
+ * single-sign-on claim leaves for the identity provider and clears the
+ * offline copies before it goes.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { page } from "vitest/browser";
@@ -16,6 +17,7 @@ import {
type FetchStub,
} from "../../../../tests/browser/fetch-stub";
import { renderWithApp } from "../../../../tests/browser/render";
+import { recordHistory } from "../../../../tests/browser/url";
const clearOffline = vi.hoisted(() => vi.fn(async () => {}));
const loadDocument = vi.hoisted(() => vi.fn());
@@ -70,6 +72,7 @@ async function click(slot: string): Promise {
describe("ClaimProfile", () => {
it("starts the claimed account from an empty cache", async () => {
+ const history = recordHistory();
const { queryClient } = await renderWithApp(
,
{
@@ -87,6 +90,7 @@ describe("ClaimProfile", () => {
.poll(() => window.location.pathname)
.toBe("/onboarding/handover");
expect(api.calls("POST", "/api/auth/claim")).toHaveLength(1);
+ expect(history.pushes()).toEqual([]);
expect(queryClient.getQueryData(queryKeys.dashboardSnapshot())).toBe(
undefined,
);
diff --git a/src/components/auth/claim-profile.tsx b/src/components/auth/claim-profile.tsx
index a930826b4..d15a6db16 100644
--- a/src/components/auth/claim-profile.tsx
+++ b/src/components/auth/claim-profile.tsx
@@ -122,7 +122,7 @@ export function ClaimProfile({
onSuccess: () => {
// As after any sign-in: nothing cached before belongs to this account.
queryClient.clear();
- router.push("/onboarding/handover");
+ router.replace("/onboarding/handover");
},
},
);