import { test, expect, Page } from "@playwright/test"; import { getBackendUrl } from "./helpers/backend-url"; /** * Profile E2E tests * * These tests verify that: * 1. Regular users can access and use the profile page * 2. Admin users cannot access the profile page * 3. Profile data persists correctly * 4. Validation works as expected */ // Test credentials - must match what's seeded in the database via seed.py function getRequiredEnv(name: string): string { const value = process.env[name]; if (!value) { throw new Error(`Required environment variable ${name} is not set.`); } return value; } const REGULAR_USER = { email: getRequiredEnv("DEV_USER_EMAIL"), password: getRequiredEnv("DEV_USER_PASSWORD"), }; const ADMIN_USER = { email: getRequiredEnv("DEV_ADMIN_EMAIL"), password: getRequiredEnv("DEV_ADMIN_PASSWORD"), }; // Valid test npub (32 zero bytes encoded as bech32) const VALID_NPUB = "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqujme"; // Helper to clear auth cookies async function clearAuth(page: Page) { await page.context().clearCookies(); } // Helper to login a user async function loginUser(page: Page, email: string, password: string) { await page.goto("/login"); await page.fill('input[type="email"]', email); await page.fill('input[type="password"]', password); await page.click('button[type="submit"]'); await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 }); } // Helper to clear profile data via API async function clearProfileData(page: Page) { const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { await page.request.put(`${getBackendUrl()}/api/profile`, { headers: { Cookie: `auth_token=${authCookie.value}`, "Content-Type": "application/json", }, data: { contact_email: null, telegram: null, signal: null, nostr_npub: null, }, }); } } test.describe("Profile - Regular User Access", () => { test.beforeEach(async ({ context, page }) => { // Set English language before any navigation await context.addInitScript(() => { localStorage.setItem("arbret-locale", "en"); }); await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); test("can navigate to profile page and page displays correct elements", async ({ page }) => { // Test navigation from exchange await page.goto("/exchange"); await expect(page.getByText("My Profile")).toBeVisible(); await page.click('a[href="/profile"]'); await expect(page).toHaveURL("/profile"); // Test navigation from trades await page.goto("/trades"); await expect(page.getByText("My Profile")).toBeVisible(); await page.click('a[href="/profile"]'); await expect(page).toHaveURL("/profile"); // Test page structure await expect(page.getByRole("heading", { name: "My Profile" })).toBeVisible(); // Check for email label with read-only badge (combined in parent element) await expect(page.getByText("EmailRead only")).toBeVisible(); await expect(page.getByText("Contact Details")).toBeVisible(); await expect(page.getByText(/communication purposes only/i)).toBeVisible(); // Test form fields visibility await expect(page.getByLabel("Contact Email")).toBeVisible(); await expect(page.getByLabel("Telegram")).toBeVisible(); await expect(page.getByLabel("Signal")).toBeVisible(); await expect(page.getByLabel("Nostr (npub)")).toBeVisible(); // Test login email is read-only const loginEmailInput = page.locator('input[type="email"][disabled]'); await expect(loginEmailInput).toHaveValue(REGULAR_USER.email); await expect(loginEmailInput).toBeDisabled(); // Test navigation links await expect(page.locator('a[href="/exchange"]')).toBeVisible(); await expect(page.locator('a[href="/trades"]')).toBeVisible(); }); }); test.describe("Profile - Form Behavior", () => { test.beforeEach(async ({ context, page }) => { await context.addInitScript(() => { window.localStorage.setItem("arbret-locale", "en"); }); await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); // Clear any existing profile data await clearProfileData(page); }); test("form state management, save, persistence, and clearing fields", async ({ page }) => { await page.goto("/profile"); // All editable fields should be empty await expect(page.getByLabel("Contact Email")).toHaveValue(""); await expect(page.getByLabel("Telegram")).toHaveValue(""); await expect(page.getByLabel("Signal")).toHaveValue(""); await expect(page.getByLabel("Nostr (npub)")).toHaveValue(""); // Save button should be disabled when no changes const saveButton = page.getByRole("button", { name: /save changes/i }); await expect(saveButton).toBeDisabled(); // Make a change - button should be enabled await page.fill("#telegram", "@testhandle"); await expect(saveButton).toBeEnabled(); // Now test saving and persistence - fill in all fields await page.fill("#contact_email", "contact@test.com"); await page.fill("#telegram", "@testuser"); await page.fill("#signal", "signal.42"); await page.fill("#nostr_npub", VALID_NPUB); // Save await page.click('button:has-text("Save Changes")'); await expect(page.getByText(/saved successfully/i)).toBeVisible(); // Reload and verify values persist await page.reload(); await expect(page.getByLabel("Contact Email")).toHaveValue("contact@test.com"); await expect(page.getByLabel("Telegram")).toHaveValue("@testuser"); await expect(page.getByLabel("Signal")).toHaveValue("signal.42"); await expect(page.getByLabel("Nostr (npub)")).toHaveValue(VALID_NPUB); // Test clearing a field await page.fill("#telegram", "@initial"); await page.click('button:has-text("Save Changes")'); await expect(page.getByText(/saved successfully/i)).toBeVisible(); await expect(page.getByText(/saved successfully/i)).not.toBeVisible({ timeout: 5000 }); // Clear the field await page.fill("#telegram", ""); await page.click('button:has-text("Save Changes")'); await expect(page.getByText(/saved successfully/i)).toBeVisible(); // Reload and verify it's cleared await page.reload(); await expect(page.getByLabel("Telegram")).toHaveValue(""); }); }); test.describe("Profile - Validation", () => { test.beforeEach(async ({ context, page }) => { await context.addInitScript(() => { window.localStorage.setItem("arbret-locale", "en"); }); await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); await clearProfileData(page); }); test("validation - all field validations and error fixing", async ({ page }) => { await page.goto("/profile"); // Test telegram auto-prepend await page.fill("#telegram", "testhandle"); await expect(page.locator("#telegram")).toHaveValue("@testhandle"); // Test telegram error - no characters after @ await page.fill("#telegram", "@"); await expect(page.getByText(/at least one character after @/i)).toBeVisible({ timeout: 2000 }); const saveButton = page.getByRole("button", { name: /save changes/i }); await expect(saveButton).toBeDisabled(); // Test invalid npub await page.fill("#nostr_npub", "invalidnpub"); await expect(page.getByText(/must start with 'npub1'/i)).toBeVisible(); await expect(saveButton).toBeDisabled(); // Test invalid email format await page.fill("#contact_email", "not-an-email"); await expect(page.getByText(/valid email/i)).toBeVisible(); await expect(saveButton).toBeDisabled(); // Fix all validation errors and save await page.fill("#telegram", "@validhandle"); await expect(page.getByText(/at least one character after @/i)).not.toBeVisible({ timeout: 2000, }); await page.fill("#nostr_npub", VALID_NPUB); await expect(page.getByText(/must start with 'npub1'/i)).not.toBeVisible({ timeout: 2000 }); await page.fill("#contact_email", "valid@email.com"); await expect(page.getByText(/valid email/i)).not.toBeVisible({ timeout: 2000 }); // Now all errors are fixed, save button should be enabled await expect(saveButton).toBeEnabled(); await page.click('button:has-text("Save Changes")'); await expect(page.getByText(/saved successfully/i)).toBeVisible(); }); }); test.describe("Profile - Admin User Access", () => { test.beforeEach(async ({ context, page }) => { await context.addInitScript(() => { window.localStorage.setItem("arbret-locale", "en"); }); await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); }); test("admin cannot access profile page or API", async ({ page, request }) => { // Admin should not see profile link await page.goto("/admin/trades"); await expect(page).toHaveURL("/admin/trades"); await expect(page.locator('a[href="/profile"]')).toHaveCount(0); // Admin should be redirected when accessing profile page await page.goto("/profile"); await expect(page).toHaveURL("/admin/trades"); // Admin API call should return 403 const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { const response = await request.get(`${getBackendUrl()}/api/profile`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(403); } }); }); test.describe("Profile - Unauthenticated Access", () => { test.beforeEach(async ({ context, page }) => { await context.addInitScript(() => { window.localStorage.setItem("arbret-locale", "en"); }); await clearAuth(page); }); test("profile page and API require authentication", async ({ page, request }) => { // Page redirects to login await page.goto("/profile"); await expect(page).toHaveURL("/login"); // API requires authentication const response = await request.get(`${getBackendUrl()}/api/profile`); expect(response.status()).toBe(401); }); });