diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index 1b8f580..b417087 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -75,107 +75,68 @@ test.describe("Authentication Flow", () => { }); }); -test.describe("Logged-in User Visiting Invite URL", () => { - test("redirects to exchange when logged-in user visits invite URL or signup page", async ({ - page, - request, - }) => { - const email = uniqueEmail(); - const inviteCode = await createInvite(request); - - // First sign up to create a user - await page.goto("/signup"); - await page.fill("input#inviteCode", inviteCode); - await page.click('button[type="submit"]'); - await expect(page.locator("h1")).toHaveText("Create account"); - - await page.fill("input#email", email); - await page.fill("input#password", "password123"); - await page.fill("input#confirmPassword", "password123"); - await page.click('button[type="submit"]'); - await expect(page).toHaveURL("/exchange"); - - // Create another invite - const anotherInvite = await createInvite(request); - - // Visit invite URL while logged in - should redirect to exchange - await page.goto(`/signup/${anotherInvite}`); - await expect(page).toHaveURL("/exchange"); - - // Try to visit signup page while logged in - should redirect to exchange - await page.goto("/signup"); - await expect(page).toHaveURL("/exchange"); - }); -}); - test.describe("Signup with Invite", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); }); - test("can create a new account with valid invite", async ({ page, request }) => { - const email = uniqueEmail(); - const inviteCode = await createInvite(request); - - await page.goto("/signup"); - - // Step 1: Enter invite code - await page.fill("input#inviteCode", inviteCode); - await page.click('button[type="submit"]'); - - // Wait for form to transition to registration form - await expect(page.locator("h1")).toHaveText("Create account"); - - // Step 2: Fill registration form - await page.fill("input#email", email); - await page.fill("input#password", "password123"); - await page.fill("input#confirmPassword", "password123"); - await page.click('button[type="submit"]'); - - // Should redirect to exchange after signup (regular user home) - await expect(page).toHaveURL("/exchange"); - // Should see Exchange page heading - await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible(); - }); - - test("signup with direct invite URL works", async ({ page, request }) => { - const email = uniqueEmail(); - const inviteCode = await createInvite(request); - - // Use direct URL with code - await page.goto(`/signup/${inviteCode}`); - - // Should redirect to signup with code in query and validate - await page.waitForURL(/\/signup\?code=/); - - // Wait for form to transition to registration form - await expect(page.locator("h1")).toHaveText("Create account"); - - // Fill registration form - await page.fill("input#email", email); - await page.fill("input#password", "password123"); - await page.fill("input#confirmPassword", "password123"); - await page.click('button[type="submit"]'); - - // Should redirect to exchange - await expect(page).toHaveURL("/exchange"); - }); - - test("shows error for invalid invite code", async ({ page }) => { - await page.goto("/signup"); - await page.fill("input#inviteCode", "fake-code-99"); - await page.click('button[type="submit"]'); - - // Should show error - await expect(page.getByText(/not found/i)).toBeVisible(); - }); - - test("shows validation errors for password mismatch and short password", async ({ + test("can create account with valid invite via form and direct URL, and logged-in users are redirected", async ({ page, request, }) => { - const inviteCode = await createInvite(request); + // Test signup via form + const email1 = uniqueEmail(); + const inviteCode1 = await createInvite(request); + await page.goto("/signup"); + await page.fill("input#inviteCode", inviteCode1); + await page.click('button[type="submit"]'); + await expect(page.locator("h1")).toHaveText("Create account"); + + await page.fill("input#email", email1); + await page.fill("input#password", "password123"); + await page.fill("input#confirmPassword", "password123"); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL("/exchange"); + await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible(); + + // Test logged-in user visiting invite URL - should redirect to exchange + const anotherInvite = await createInvite(request); + await page.goto(`/signup/${anotherInvite}`); + await expect(page).toHaveURL("/exchange"); + + // Test logged-in user visiting signup page - should redirect to exchange + await page.goto("/signup"); + await expect(page).toHaveURL("/exchange"); + + // Test signup via direct URL (new session) + await clearAuth(page); + const email2 = uniqueEmail(); + const inviteCode2 = await createInvite(request); + + await page.goto(`/signup/${inviteCode2}`); + await page.waitForURL(/\/signup\?code=/); + await expect(page.locator("h1")).toHaveText("Create account"); + + await page.fill("input#email", email2); + await page.fill("input#password", "password123"); + await page.fill("input#confirmPassword", "password123"); + await page.click('button[type="submit"]'); + await expect(page).toHaveURL("/exchange"); + }); + + test("shows errors for invalid invite code and password validation", async ({ + page, + request, + }) => { + // Test invalid invite code + await page.goto("/signup"); + await page.fill("input#inviteCode", "fake-code-99"); + await page.click('button[type="submit"]'); + await expect(page.getByText(/not found/i)).toBeVisible(); + + // Test password validation with valid invite + const inviteCode = await createInvite(request); await page.goto("/signup"); await page.fill("input#inviteCode", inviteCode); await page.click('button[type="submit"]'); @@ -220,11 +181,15 @@ test.describe("Login", () => { await clearAuth(page); }); - test("can login with valid credentials", async ({ page }) => { + test("can login with valid credentials and shows loading state", async ({ page }) => { + // Test loading state await page.goto("/login"); await page.fill('input[type="email"]', testEmail); await page.fill('input[type="password"]', testPassword); - await page.click('button[type="submit"]'); + + const submitPromise = page.click('button[type="submit"]'); + await expect(page.locator('button[type="submit"]')).toHaveText("Signing in..."); + await submitPromise; // Regular user redirects to exchange await expect(page).toHaveURL("/exchange"); @@ -246,16 +211,6 @@ test.describe("Login", () => { await page.click('button[type="submit"]'); await expect(page.getByText("Incorrect email or password")).toBeVisible(); }); - - test("shows loading state while submitting", async ({ page }) => { - await page.goto("/login"); - await page.fill('input[type="email"]', testEmail); - await page.fill('input[type="password"]', testPassword); - - const submitPromise = page.click('button[type="submit"]'); - await expect(page.locator('button[type="submit"]')).toHaveText("Signing in..."); - await submitPromise; - }); }); test.describe("Logout", () => { diff --git a/frontend/e2e/availability.spec.ts b/frontend/e2e/availability.spec.ts index f21cb02..2f43f84 100644 --- a/frontend/e2e/availability.spec.ts +++ b/frontend/e2e/availability.spec.ts @@ -45,44 +45,7 @@ test.describe("Availability Page - Admin Access", () => { await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible(); }); - test("can add availability slot", async ({ page }) => { - await page.goto("/admin/availability"); - - // Wait for initial data load to complete - await page.waitForLoadState("networkidle"); - - // Find a day card with "No availability" and click on it - // This ensures we're clicking on a day without existing slots - const dayCardWithNoAvailability = page - .locator('[data-testid^="day-card-"]') - .filter({ - has: page.getByText("No availability"), - }) - .first(); - await dayCardWithNoAvailability.click(); - - // Wait for modal - await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible(); - - // Set up listeners for both PUT and GET before clicking Save to avoid race condition - const putPromise = page.waitForResponse( - (resp) => resp.url().includes("/api/admin/availability") && resp.request().method() === "PUT" - ); - const getPromise = page.waitForResponse( - (resp) => resp.url().includes("/api/admin/availability") && resp.request().method() === "GET" - ); - await page.getByRole("button", { name: "Save" }).click(); - await putPromise; - await getPromise; - - // Wait for modal to close - await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible(); - - // Should now show the slot (the card we clicked should now have this slot) - await expect(page.getByText("09:00 - 17:00")).toBeVisible(); - }); - - test("can clear availability", async ({ page }) => { + test("can add, clear, and add multiple availability slots", async ({ page }) => { await page.goto("/admin/availability"); // Wait for initial data load to complete @@ -139,39 +102,31 @@ test.describe("Availability Page - Admin Access", () => { // Slot should be gone from this specific card await expect(targetCard.getByText("09:00 - 17:00")).not.toBeVisible(); - }); - test("can add multiple slots", async ({ page }) => { - await page.goto("/admin/availability"); - - // Wait for initial data load to complete + // Now test adding multiple slots - find another day card await page.waitForLoadState("networkidle"); - - // Find a day card with "No availability" and click on it (to avoid conflicts with booking tests) - const dayCardWithNoAvailability = page + const anotherDayCard = page .locator('[data-testid^="day-card-"]') .filter({ has: page.getByText("No availability"), }) .first(); - const testId = await dayCardWithNoAvailability.getAttribute("data-testid"); - const targetCard = page.locator(`[data-testid="${testId}"]`); - await dayCardWithNoAvailability.click(); + const anotherTestId = await anotherDayCard.getAttribute("data-testid"); + const anotherTargetCard = page.locator(`[data-testid="${anotherTestId}"]`); + await anotherDayCard.click(); await expect(page.getByRole("heading", { name: /Edit Availability/ })).toBeVisible(); // First slot is 09:00-17:00 by default - change it to morning only const timeSelects = page.locator("select"); - await timeSelects.nth(1).selectOption("12:00"); // Change first slot end to 12:00 + await timeSelects.nth(1).selectOption("12:00"); // Add another slot for afternoon await page.getByText("+ Add Time Range").click(); + await timeSelects.nth(2).selectOption("14:00"); + await timeSelects.nth(3).selectOption("17:00"); - // Change second slot times to avoid overlap - await timeSelects.nth(2).selectOption("14:00"); // Second slot start - await timeSelects.nth(3).selectOption("17:00"); // Second slot end - - // Set up listeners for both PUT and GET before clicking Save to avoid race condition + // Save multiple slots const putPromise = page.waitForResponse( (resp) => resp.url().includes("/api/admin/availability") && resp.request().method() === "PUT" ); @@ -183,9 +138,9 @@ test.describe("Availability Page - Admin Access", () => { await getPromise; await expect(page.getByRole("heading", { name: /Edit Availability/ })).not.toBeVisible(); - // Should see both slots in the card we clicked - await expect(targetCard.getByText("09:00 - 12:00")).toBeVisible(); - await expect(targetCard.getByText("14:00 - 17:00")).toBeVisible(); + // Should see both slots + await expect(anotherTargetCard.getByText("09:00 - 12:00")).toBeVisible(); + await expect(anotherTargetCard.getByText("14:00 - 17:00")).toBeVisible(); }); }); diff --git a/frontend/e2e/exchange.spec.ts b/frontend/e2e/exchange.spec.ts index 55dfd3f..fed0c90 100644 --- a/frontend/e2e/exchange.spec.ts +++ b/frontend/e2e/exchange.spec.ts @@ -40,7 +40,7 @@ test.describe("Exchange Page - Regular User Access", () => { await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); - test("regular user can access exchange page and all UI elements are visible", async ({ + test("regular user can access exchange page, all UI elements work, and buy/sell toggle functions", async ({ page, }) => { // Test navigation @@ -56,10 +56,15 @@ test.describe("Exchange Page - Regular User Access", () => { await expect(page.getByText("Market:")).toBeVisible(); await expect(page.getByText("Our price:")).toBeVisible(); - // Test buy/sell toggle - await expect(page.getByRole("button", { name: "Buy BTC" })).toBeVisible(); + // Test buy/sell toggle visibility and functionality + const buyButton = page.getByRole("button", { name: "Buy BTC" }); + await expect(buyButton).toBeVisible(); await expect(page.getByRole("button", { name: "Sell BTC" })).toBeVisible(); + // Test clicking buy/sell changes direction + await page.getByRole("button", { name: "Sell BTC" }).click(); + await expect(page.getByText(/You buy €\d/)).toBeVisible(); + // Test payment method selector await expect(page.getByText("Payment Method")).toBeVisible(); await expect(page.getByRole("button", { name: /Onchain/ })).toBeVisible(); @@ -68,33 +73,10 @@ test.describe("Exchange Page - Regular User Access", () => { // Test amount slider await expect(page.getByText("Amount")).toBeVisible(); await expect(page.locator('input[type="range"]')).toBeVisible(); - }); - test("clicking buy/sell changes direction", async ({ page }) => { - await page.goto("/exchange"); - - // Initially in buy mode - summary shows BTC first: "You buy [sats], you sell €X" - // Verify buy mode is initially active - const buyButton = page.getByRole("button", { name: "Buy BTC" }); - await expect(buyButton).toBeVisible(); - - // Click Sell BTC to switch direction - await page.getByRole("button", { name: "Sell BTC" }).click(); - - // In sell mode, the summary shows EUR first: "You buy €X, you sell [sats]" - // We can verify by checking the summary text contains "You buy €" (EUR comes first) - await expect(page.getByText(/You buy €\d/)).toBeVisible(); - }); - - test("exchange page shows date selection after continue", async ({ page }) => { - await page.goto("/exchange"); - - // Step 1: Click "Continue to Booking" to proceed to step 2 + // Test date selection appears after continue await page.getByRole("button", { name: "Continue to Booking" }).click(); - - // Step 2: Now date selection should be visible await expect(page.getByRole("heading", { name: "Select a Date" })).toBeVisible(); - // Should see multiple date buttons const dateButtons = page .locator("button") .filter({ hasText: /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)/ }); @@ -163,10 +145,10 @@ test.describe("Exchange Page - With Availability", () => { await expect(page.getByText("Payment:")).toBeVisible(); }); - test("payment method selector works", async ({ page }) => { + test("payment method selector works and lightning disabled above threshold", async ({ page }) => { await page.goto("/exchange"); - // Default should be Onchain + // Test payment method selector const onchainButton = page.getByRole("button", { name: /Onchain/ }); const lightningButton = page.getByRole("button", { name: /Lightning/ }); await expect(onchainButton).toHaveCSS("border-color", "rgb(167, 139, 250)"); @@ -179,20 +161,11 @@ test.describe("Exchange Page - With Availability", () => { // Click back to Onchain await onchainButton.click(); await expect(onchainButton).toHaveCSS("border-color", "rgb(167, 139, 250)"); - }); - test("lightning disabled above threshold", async ({ page }) => { - await page.goto("/exchange"); - - // Set amount above threshold (€1000 = 100000 cents) + // Test lightning disabled above threshold const amountInput = page.locator('input[type="text"]').filter({ hasText: "" }); await amountInput.fill("1100"); - - // Lightning button should be disabled - const lightningButton = page.getByRole("button", { name: /Lightning/ }); await expect(lightningButton).toBeDisabled(); - - // Should show threshold message await expect(page.getByText(/Lightning payments are only available/)).toBeVisible(); }); }); @@ -249,27 +222,18 @@ test.describe("Admin Trades Page", () => { await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); }); - test("admin can access trades page", async ({ page }) => { + test("admin can access trades page with tabs, regular user cannot", async ({ page }) => { + // Test admin access await page.goto("/admin/trades"); - await expect(page).toHaveURL("/admin/trades"); await expect(page.getByRole("heading", { name: "Trades" })).toBeVisible(); - }); - - test("admin trades page shows tabs", async ({ page }) => { - await page.goto("/admin/trades"); - await expect(page.getByRole("button", { name: /Upcoming/ })).toBeVisible(); await expect(page.getByRole("button", { name: /History/ })).toBeVisible(); - }); - test("regular user cannot access admin trades page", async ({ page }) => { + // Test regular user cannot access await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); - await page.goto("/admin/trades"); - - // Should be redirected away await expect(page).not.toHaveURL("/admin/trades"); }); }); diff --git a/frontend/e2e/profile.spec.ts b/frontend/e2e/profile.spec.ts index 2fb0bbb..4525e34 100644 --- a/frontend/e2e/profile.spec.ts +++ b/frontend/e2e/profile.spec.ts @@ -119,7 +119,7 @@ test.describe("Profile - Form Behavior", () => { await clearProfileData(page); }); - test("form state management - empty fields, button states", async ({ page }) => { + test("form state management, save, persistence, and clearing fields", async ({ page }) => { await page.goto("/profile"); // All editable fields should be empty @@ -135,12 +135,8 @@ test.describe("Profile - Form Behavior", () => { // Make a change - button should be enabled await page.fill("#telegram", "@testhandle"); await expect(saveButton).toBeEnabled(); - }); - test("can save profile, values persist, and can clear fields", async ({ page }) => { - await page.goto("/profile"); - - // Fill in all fields + // 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"); @@ -181,7 +177,7 @@ test.describe("Profile - Validation", () => { await clearProfileData(page); }); - test("validation - telegram auto-prepend, errors for invalid inputs", async ({ page }) => { + test("validation - all field validations and error fixing", async ({ page }) => { await page.goto("/profile"); // Test telegram auto-prepend @@ -190,8 +186,7 @@ test.describe("Profile - Validation", () => { // Test telegram error - no characters after @ await page.fill("#telegram", "@"); - await page.waitForTimeout(600); - await expect(page.getByText(/at least one character after @/i)).toBeVisible(); + 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(); @@ -199,45 +194,29 @@ test.describe("Profile - Validation", () => { await page.fill("#nostr_npub", "invalidnpub"); await expect(page.getByText(/must start with 'npub1'/i)).toBeVisible(); await expect(saveButton).toBeDisabled(); - }); - test("can fix validation error and save", async ({ page }) => { - await page.goto("/profile"); + // Test invalid email format + await page.fill("#contact_email", "not-an-email"); + await expect(page.getByText(/valid email/i)).toBeVisible(); + await expect(saveButton).toBeDisabled(); - // Enter invalid telegram (just @ with no handle) - await page.fill("#telegram", "@"); - - // Wait for debounced validation - await page.waitForTimeout(600); - - await expect(page.getByText(/at least one character after @/i)).toBeVisible(); - - // Fix it + // 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, + }); - // Wait for debounced validation - await page.waitForTimeout(600); + await page.fill("#nostr_npub", VALID_NPUB); + await expect(page.getByText(/must start with 'npub1'/i)).not.toBeVisible({ timeout: 2000 }); - // Error should disappear - await expect(page.getByText(/at least one character after @/i)).not.toBeVisible(); + await page.fill("#contact_email", "valid@email.com"); + await expect(page.getByText(/valid email/i)).not.toBeVisible({ timeout: 2000 }); - // Should be able to save - const saveButton = page.getByRole("button", { name: /save changes/i }); + // 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("shows error for invalid email format", async ({ page }) => { - await page.goto("/profile"); - - // Enter invalid email - await page.fill("#contact_email", "not-an-email"); - - // Should show error - await expect(page.getByText(/valid email/i)).toBeVisible(); - }); }); test.describe("Profile - Admin User Access", () => { diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index db1ec35..ebc1a25 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -17,5 +17,8 @@ export default defineConfig({ baseURL: "http://localhost:3000", // Action timeout (clicks, fills, etc.) actionTimeout: 5000, + // Reduce screenshot/recording overhead + screenshot: "only-on-failure", + trace: "retain-on-failure", }, });