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.
321 lines
13 KiB
TypeScript
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();
|
|
});
|