83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
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<string>();
|
|
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.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();
|
|
});
|