import { expect, test } from "@playwright/test"; import { captureBrowserFailures, clickUntilVisible, gotoWithTransientRetry, isTransientNetworkError, loginWithPassword, projectEmail, registerAndConfirm, waitForApplicationEmailLink, } from "./helpers"; test("registration, email confirmation, password, and email change work end to end", async ({ browser, request, }, testInfo) => { const originalEmail = projectEmail("auth-user", testInfo.project.name); const changedEmail = projectEmail("auth-user-changed", testInfo.project.name); const password = "E2E public password 2026!"; const user = await registerAndConfirm( browser, request, originalEmail, "E2E Auth User", ); const recoverableOneTimeNavigations = new Set(); const assertBrowserClean = captureBrowserFailures(user.page, { recoverableOneTimeNavigations, }); await user.page.goto("/users/settings"); const passwordForm = user.page.locator("#update_password"); await passwordForm.getByLabel("New password", { exact: true }).fill(password); await passwordForm.getByLabel("Confirm new password", { exact: true }).fill(password); await clickUntilVisible( passwordForm.getByRole("button", { name: "Save Password" }), user.page.getByText("Password updated successfully."), ); const emailForm = user.page.locator("#update_email"); await emailForm.getByLabel("Email").fill(changedEmail); await clickUntilVisible( emailForm.getByRole("button", { name: "Change Email" }), user.page.getByText("A link to confirm your email change has been sent"), ); const emailChangeLink = await waitForApplicationEmailLink( request, changedEmail, "/users/settings/confirm-email", ); recoverableOneTimeNavigations.add(emailChangeLink); try { await user.page.goto(emailChangeLink); await expect(user.page.locator("#email-change-fragment-form")).toBeVisible(); await user.page .getByRole("button", { name: "Confirm email change" }) .click(); await expect(user.page.getByText("Email changed successfully.")).toBeVisible(); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (!isTransientNetworkError(message)) { throw error; } // The server may commit and consume the single-use token before a // transport reset reaches the browser. Never replay that URL: verify the // committed account state through an idempotent page instead. await gotoWithTransientRetry(user.page, "/users/settings"); await expect(user.page.locator("#update_email").getByLabel("Email")).toHaveValue( changedEmail, ); } await user.context.close(); const passwordLogin = await loginWithPassword(browser, changedEmail, password); const assertPasswordLoginClean = captureBrowserFailures(passwordLogin.page); await expect(passwordLogin.page.getByText(changedEmail, { exact: true })).toBeVisible(); assertBrowserClean(); assertPasswordLoginClean(); await passwordLogin.context.close(); });