import { test, expect, Page } from "@playwright/test"; import { getTomorrowDateStr } from "./helpers/date"; import { API_URL, REGULAR_USER, ADMIN_USER, clearAuth, loginUser } from "./helpers/auth"; /** * Exchange Page E2E Tests * * Tests for the Bitcoin exchange trading page. */ // Set up availability for a date using the API async function setAvailability(page: Page, dateStr: string) { const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (!authCookie) { throw new Error("No auth cookie found when trying to set availability"); } const response = await page.request.put(`${API_URL}/api/admin/availability`, { headers: { Cookie: `auth_token=${authCookie.value}`, "Content-Type": "application/json", }, data: { date: dateStr, slots: [{ start_time: "09:00:00", end_time: "12:00:00" }], }, }); if (!response.ok()) { const body = await response.text(); throw new Error(`Failed to set availability: ${response.status()} - ${body}`); } } test.describe("Exchange Page - Regular User Access", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); test("regular user can access exchange page", async ({ page }) => { await page.goto("/exchange"); await expect(page).toHaveURL("/exchange"); await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible(); }); test("regular user sees Exchange link in navigation", async ({ page }) => { await page.goto("/trades"); await expect(page.getByRole("link", { name: "Exchange" })).toBeVisible(); }); test("exchange page shows price information", async ({ page }) => { await page.goto("/exchange"); // Should show market and our price await expect(page.getByText("Market:")).toBeVisible(); await expect(page.getByText("Our price:")).toBeVisible(); }); test("exchange page shows buy/sell toggle", async ({ page }) => { await page.goto("/exchange"); await expect(page.getByRole("button", { name: "Buy BTC" })).toBeVisible(); await expect(page.getByRole("button", { name: "Sell BTC" })).toBeVisible(); }); test("exchange page shows payment method selector", async ({ page }) => { await page.goto("/exchange"); await expect(page.getByText("Payment Method")).toBeVisible(); await expect(page.getByRole("button", { name: /Onchain/ })).toBeVisible(); await expect(page.getByRole("button", { name: /Lightning/ })).toBeVisible(); }); test("exchange page shows amount slider", async ({ page }) => { await page.goto("/exchange"); // Should show amount section 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 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)/ }); await expect(dateButtons.first()).toBeVisible(); }); }); test.describe("Exchange Page - With Availability", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); // Login as admin to set availability await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); await setAvailability(page, getTomorrowDateStr()); await clearAuth(page); // Login as regular user await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); test("shows available slots when availability is set", async ({ page }) => { await page.goto("/exchange"); // Step 1: Click "Continue to Booking" to proceed to step 2 await page.getByRole("button", { name: "Continue to Booking" }).click(); // Step 2: Use data-testid for reliable date selection const tomorrowStr = getTomorrowDateStr(); const dateButton = page.getByTestId(`date-${tomorrowStr}`); await expect(dateButton).toBeEnabled({ timeout: 15000 }); await dateButton.click(); // Wait for "Available Slots" section to appear await expect(page.getByRole("heading", { name: /Available Slots for/ })).toBeVisible(); // Wait for loading to finish await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 }); // Should see some slot buttons const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ }); await expect(slotButtons.first()).toBeVisible({ timeout: 10000 }); }); test("clicking slot shows confirmation form", async ({ page }) => { await page.goto("/exchange"); // Step 1: Click "Continue to Booking" to proceed to step 2 await page.getByRole("button", { name: "Continue to Booking" }).click(); // Step 2: Use data-testid for reliable date selection const tomorrowStr = getTomorrowDateStr(); const dateButton = page.getByTestId(`date-${tomorrowStr}`); await expect(dateButton).toBeEnabled({ timeout: 15000 }); await dateButton.click(); // Wait for any slot to appear await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 }); const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ }); await expect(slotButtons.first()).toBeVisible({ timeout: 10000 }); // Click first slot await slotButtons.first().click(); // Should show confirmation form await expect(page.getByText("Confirm Trade")).toBeVisible(); await expect(page.getByRole("button", { name: /Confirm/ })).toBeVisible(); }); test("confirmation shows trade details", async ({ page }) => { await page.goto("/exchange"); // Step 1: Click "Continue to Booking" to proceed to step 2 await page.getByRole("button", { name: "Continue to Booking" }).click(); // Step 2: Use data-testid for reliable date selection const tomorrowStr = getTomorrowDateStr(); const dateButton = page.getByTestId(`date-${tomorrowStr}`); await expect(dateButton).toBeEnabled({ timeout: 15000 }); await dateButton.click(); // Wait for slots to load await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 }); const slotButtons = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ }); await expect(slotButtons.first()).toBeVisible({ timeout: 10000 }); // Click second slot await slotButtons.nth(1).click(); // Should show confirmation with trade details await expect(page.getByText("Confirm Trade")).toBeVisible(); await expect(page.getByText("Time:")).toBeVisible(); await expect(page.getByText("Direction:")).toBeVisible(); await expect(page.getByText("EUR:")).toBeVisible(); await expect(page.getByText("BTC:")).toBeVisible(); await expect(page.getByText("Rate:")).toBeVisible(); await expect(page.getByText("Payment:")).toBeVisible(); }); test("payment method selector works", async ({ page }) => { await page.goto("/exchange"); // Default should be Onchain const onchainButton = page.getByRole("button", { name: /Onchain/ }); const lightningButton = page.getByRole("button", { name: /Lightning/ }); await expect(onchainButton).toHaveCSS("border-color", "rgb(167, 139, 250)"); // Click Lightning await lightningButton.click(); await expect(lightningButton).toHaveCSS("border-color", "rgb(167, 139, 250)"); await expect(onchainButton).not.toHaveCSS("border-color", "rgb(167, 139, 250)"); // 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) 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(); }); }); test.describe("Exchange Page - Access Control", () => { test("admin cannot access exchange page", async ({ page }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); await page.goto("/exchange"); // Should be redirected away await expect(page).not.toHaveURL("/exchange"); }); test("admin does not see Exchange link", async ({ page }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); await page.goto("/admin/trades"); await expect(page.getByRole("link", { name: "Exchange" })).not.toBeVisible(); }); test("unauthenticated user redirected to login", async ({ page }) => { await clearAuth(page); await page.goto("/exchange"); await expect(page).toHaveURL("/login"); }); }); test.describe("Trades Page", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); test("regular user can access trades page", async ({ page }) => { await page.goto("/trades"); await expect(page).toHaveURL("/trades"); await expect(page.getByRole("heading", { name: "My Trades" })).toBeVisible(); }); test("trades page shows empty state when no trades", async ({ page }) => { await page.goto("/trades"); // Either shows empty state message or trades list const content = page.locator("body"); await expect(content).toBeVisible(); }); test("trades page shows Start trading link when empty", async ({ page }) => { await page.goto("/trades"); // Wait for loading to finish - either "Loading trades..." disappears or we see content await expect(page.getByText("Loading trades...")).not.toBeVisible({ timeout: 5000 }); // Check if it shows empty state with link, or trades exist const startTradingLink = page.getByRole("link", { name: "Start trading" }); const isLinkVisible = await startTradingLink.isVisible().catch(() => false); if (isLinkVisible) { await expect(startTradingLink).toHaveAttribute("href", "/exchange"); } }); }); test.describe("Admin Trades Page", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); }); test("admin can access trades page", async ({ page }) => { 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 }) => { 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"); }); }); test.describe("Exchange API", () => { test("regular user can get price via API", async ({ page, request }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { const response = await request.get(`${API_URL}/api/exchange/price`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(200); const data = await response.json(); expect(data.config).toBeDefined(); expect(data.config.eur_min).toBeDefined(); expect(data.config.eur_max).toBeDefined(); } }); test("admin cannot get price via API", async ({ page, request }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { const response = await request.get(`${API_URL}/api/exchange/price`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(403); } }); test("regular user can get trades via API", async ({ page, request }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { const response = await request.get(`${API_URL}/api/trades`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(200); const data = await response.json(); expect(Array.isArray(data)).toBe(true); } }); test("admin can get upcoming trades via API", async ({ page, request }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { const response = await request.get(`${API_URL}/api/admin/trades/upcoming`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(200); const data = await response.json(); expect(Array.isArray(data)).toBe(true); } }); });