Files
pleno-vue/tests/e2e/userProfileSecurity.spec.ts
T
Jeppe B 664b50d4ef Clarify customer login and invoice emails (#250)
Separate Pleno login email from e-conomic invoice email, make the login-email update flow authoritative and cache-safe, clear stale economic profile state, and add focused frontend coverage.
2026-08-03 10:43:53 +02:00

321 lines
13 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { loginAsUser } from "./fixtures";
type ProfileEmailSession = {
loginEmail: string;
invoiceEmail?: string | null;
};
const buildProfileSession = ({ loginEmail, invoiceEmail }: ProfileEmailSession) => ({
id: 1,
customer_number: 12345679,
group_id: 1,
email: loginEmail,
phone: {
number: "12345678",
country_code: 45,
},
notifications: {
wash_certificate_email: null,
email_notifications_enabled: true,
sms_notifications_enabled: false,
superuser_new_customer_email_notifications_enabled: false,
},
created_at: "2026-01-01T00:00:00.000Z",
updated_at: "2026-01-01T00:00:00.000Z",
display_name: "E2E User",
permissions: ["user"],
economic_customer: invoiceEmail
? {
customerNumber: 12345679,
name: "E2E User",
address: "Test Street 1",
zip: "1000",
city: "Copenhagen",
mobilePhone: "12345678",
email: invoiceEmail,
corporateIdentificationNumber: "12345678",
currency: "DKK",
country: "DK",
}
: [],
runtime_config: {
economic: {
transaction_draft_customer_number: null,
default_distribution_department_id: null,
},
release: {},
},
});
async function openUserProfileWithEmails(page: Page, session: ProfileEmailSession) {
await loginAsUser(page);
let sessionRequestCount = 0;
await page.route("**/auth/session", async (route) => {
sessionRequestCount += 1;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: buildProfileSession(session) }),
});
});
await page.goto("/user/profile");
const invoicingCard = page.getByTestId("user-profile-invoicing-card");
const securityCard = page.getByTestId("user-profile-security-card");
await expect(invoicingCard).toBeVisible();
await expect(securityCard).toBeVisible();
await invoicingCard.locator(".card-header").click();
await securityCard.locator(".card-header").click();
await expect(page.getByTestId("user-profile-login-email")).toBeVisible();
await expect(page.getByTestId("user-profile-invoice-email")).toBeVisible();
return {
getSessionRequestCount: () => sessionRequestCount,
};
}
const emailValue = (page: Page, testId: string) => page.getByTestId(testId).locator("input");
async function completePasswordStep(page: Page, password = "valid-password") {
await page.getByTestId("user-profile-security-edit-email").click();
const popup = page.locator(".swal2-popup");
await expect(popup).toBeVisible();
await popup.locator(".swal2-input").fill(password);
await popup.locator(".swal2-confirm").click();
await expect(popup.locator(".swal2-input")).toHaveAttribute("type", "text");
}
async function openUserSecurity(page) {
await loginAsUser(page);
await page.goto("/user/profile");
const securityCard = page.getByTestId("user-profile-security-card");
await expect(securityCard).toBeVisible();
await securityCard.locator(".card-header").click();
await expect(page.getByTestId("user-profile-security-edit-email")).toBeVisible();
return securityCard;
}
async function expectLogoutButtonStyling(page, button) {
await expect(button).toHaveClass(/is-danger/);
await expect(button).toHaveClass(/is-outlined/);
const viewportWidth = page.viewportSize()?.width ?? Number.MAX_SAFE_INTEGER;
if (viewportWidth <= 768) {
const dimensions = await button.evaluate((element) => {
const rect = element.getBoundingClientRect();
const parentRect = element.parentElement?.getBoundingClientRect();
return {
width: rect.width,
parentWidth: parentRect?.width ?? rect.width,
};
});
expect(Math.abs(dimensions.parentWidth - dimensions.width)).toBeLessThanOrEqual(2);
}
}
test("[PROFILE][User][Security] renders the current security surface", async ({ page }) => {
await openUserSecurity(page);
await expect(page.getByTestId("user-profile-security-edit-email")).toBeVisible();
await expect(page.getByTestId("user-profile-security-edit-phone")).toBeVisible();
await expect(page.getByTestId("user-profile-security-change-password")).toBeVisible();
const logoutButton = page.getByTestId("user-profile-security-logout");
await expect(logoutButton).toBeVisible();
await expectLogoutButtonStyling(page, logoutButton);
await expect(page.getByTestId("user-profile-passkeys").getByTestId("passkey-add")).toBeVisible();
await expect(page.getByTestId("user-profile-two-factor").getByTestId("two-factor-status")).toBeVisible();
await expect(page.getByTestId("subuser-profile-security-logout")).toHaveCount(0);
});
test("[PROFILE][User][Security] shows exact login and invoice emails under distinct labels", async ({ page }) => {
await openUserProfileWithEmails(page, {
loginEmail: "k.sand@example.test",
invoiceEmail: "billing+wash@example.test",
});
const loginEmail = page.getByTestId("user-profile-login-email");
const invoiceEmail = page.getByTestId("user-profile-invoice-email");
const loginLabel = (await loginEmail.locator("label").innerText()).trim();
const invoiceLabel = (await invoiceEmail.locator("label").innerText()).trim();
expect(loginLabel).not.toBe("");
expect(invoiceLabel).not.toBe("");
expect(loginLabel).not.toBe(invoiceLabel);
await expect(page.getByRole("textbox", { name: loginLabel, exact: true })).toHaveValue("k.sand@example.test");
await expect(page.getByRole("textbox", { name: invoiceLabel, exact: true })).toHaveValue("billing+wash@example.test");
await expect(emailValue(page, "user-profile-login-email")).toHaveValue("k.sand@example.test");
await expect(emailValue(page, "user-profile-invoice-email")).toHaveValue("billing+wash@example.test");
for (const field of [loginEmail, invoiceEmail]) {
const bounds = await field.boundingBox();
const viewport = page.viewportSize();
expect(bounds).not.toBeNull();
expect(viewport).not.toBeNull();
expect(bounds!.x).toBeGreaterThanOrEqual(0);
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(viewport!.width + 1);
}
});
test("[PROFILE][User][Security] keeps equal login and invoice emails as separate fields", async ({ page }) => {
await openUserProfileWithEmails(page, {
loginEmail: "same.address@example.test",
invoiceEmail: "same.address@example.test",
});
await expect(emailValue(page, "user-profile-login-email")).toHaveValue("same.address@example.test");
await expect(emailValue(page, "user-profile-invoice-email")).toHaveValue("same.address@example.test");
await expect(page.getByTestId("user-profile-login-email").locator("label")).not.toHaveText("");
await expect(page.getByTestId("user-profile-invoice-email").locator("label")).not.toHaveText("");
});
test("[PROFILE][User][Security] shows a neutral state when the invoice email is missing", async ({ page }) => {
await openUserProfileWithEmails(page, {
loginEmail: "k.sand@example.test",
invoiceEmail: null,
});
const invoiceEmail = page.getByTestId("user-profile-invoice-email");
await expect(emailValue(page, "user-profile-invoice-email")).toHaveValue(/e-conomic/i);
await expect(invoiceEmail.locator(".message.is-warning")).toHaveCount(0);
});
test("[PROFILE][User][Security] applies the confirmed login email without changing invoice email or refetching session", async ({
page,
}) => {
const profile = await openUserProfileWithEmails(page, {
loginEmail: "old.login@example.test",
invoiceEmail: "invoice@example.test",
});
const sessionRequestsBeforeSave = profile.getSessionRequestCount();
let changeEmailPayload: Record<string, unknown> | null = null;
await page.route("**/account/security/validate-password", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: { valid: true } }),
});
});
await page.route("**/account/security/change-email", async (route) => {
changeEmailPayload = route.request().postDataJSON();
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: { message: "Email changed", email: "k.sand@example.test" } }),
});
});
await completePasswordStep(page);
const popup = page.locator(".swal2-popup");
await popup.locator(".swal2-input").fill("K.Sand@example.test");
await popup.locator(".swal2-confirm").click();
await expect(emailValue(page, "user-profile-login-email")).toHaveValue("k.sand@example.test");
await expect(emailValue(page, "user-profile-invoice-email")).toHaveValue("invoice@example.test");
expect(changeEmailPayload).toEqual({ email: "K.Sand@example.test", password: "valid-password" });
expect(profile.getSessionRequestCount()).toBe(sessionRequestsBeforeSave);
});
test("[PROFILE][User][Security] falls back to the submitted email for an older success response", async ({ page }) => {
await openUserProfileWithEmails(page, {
loginEmail: "old.login@example.test",
invoiceEmail: "invoice@example.test",
});
await page.route("**/account/security/validate-password", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: { valid: true } }),
});
});
await page.route("**/account/security/change-email", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: { message: "Email changed" } }),
});
});
await completePasswordStep(page);
const popup = page.locator(".swal2-popup");
await popup.locator(".swal2-input").fill("fallback.address@example.test");
await popup.locator(".swal2-confirm").click();
await expect(emailValue(page, "user-profile-login-email")).toHaveValue("fallback.address@example.test");
await expect(emailValue(page, "user-profile-invoice-email")).toHaveValue("invoice@example.test");
});
test("[PROFILE][User][Security] leaves both emails unchanged when saving fails", async ({ page }) => {
await openUserProfileWithEmails(page, {
loginEmail: "old.login@example.test",
invoiceEmail: "invoice@example.test",
});
await page.route("**/account/security/validate-password", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: { valid: true } }),
});
});
await page.route("**/account/security/change-email", async (route) => {
await route.fulfill({
status: 422,
contentType: "application/json",
body: JSON.stringify({ message: "Email is already in use" }),
});
});
await completePasswordStep(page);
const popup = page.locator(".swal2-popup");
await popup.locator(".swal2-input").fill("duplicate@example.test");
await popup.locator(".swal2-confirm").click();
await expect(popup.locator(".swal2-validation-message")).toBeVisible();
await expect(emailValue(page, "user-profile-login-email")).toHaveValue("old.login@example.test");
await expect(emailValue(page, "user-profile-invoice-email")).toHaveValue("invoice@example.test");
});
test("[PROFILE][User][Security] opens the logout confirmation dialog", async ({ page }) => {
await openUserSecurity(page);
await page.getByTestId("user-profile-security-logout").click();
await expect(page.locator(".swal2-popup")).toBeVisible();
});
test("[PROFILE][User][Security] opens the email password-confirmation dialog", async ({ page }) => {
await openUserSecurity(page);
await page.getByTestId("user-profile-security-edit-email").click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await expect(page.locator(".swal2-input")).toBeVisible();
});
test("[PROFILE][User][Security] validates the phone-number dialog before submit", async ({ page }) => {
await openUserSecurity(page);
await page.getByTestId("user-profile-security-edit-phone").click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.click(".swal2-confirm");
await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
test("[PROFILE][User][Security] validates the password dialog before submit", async ({ page }) => {
await openUserSecurity(page);
await page.getByTestId("user-profile-security-change-password").click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.click(".swal2-confirm");
await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
test("[PROFILE][User][Security] closes the password dialog when cancelled", async ({ page }) => {
await openUserSecurity(page);
await page.getByTestId("user-profile-security-change-password").click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.click(".swal2-cancel");
await expect(page.locator(".swal2-popup")).toBeHidden();
});