2025-12-22 20:12:19 +01:00
|
|
|
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", () => {
|
2025-12-25 22:35:27 +01:00
|
|
|
test.beforeEach(async ({ context, page }) => {
|
|
|
|
|
// Set English language before any navigation
|
|
|
|
|
await context.addInitScript(() => {
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
window.localStorage.setItem("arbret-locale", "en");
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-22 20:12:19 +01:00
|
|
|
await clearAuth(page);
|
|
|
|
|
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
test("regular user can access exchange page, all UI elements work, and buy/sell toggle functions", async ({
|
2025-12-24 23:52:52 +01:00
|
|
|
page,
|
|
|
|
|
}) => {
|
|
|
|
|
// Test navigation
|
2025-12-22 20:12:19 +01:00
|
|
|
await page.goto("/trades");
|
|
|
|
|
await expect(page.getByRole("link", { name: "Exchange" })).toBeVisible();
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test page access
|
2025-12-22 20:12:19 +01:00
|
|
|
await page.goto("/exchange");
|
2025-12-24 23:52:52 +01:00
|
|
|
await expect(page).toHaveURL("/exchange");
|
|
|
|
|
await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible();
|
2025-12-22 20:12:19 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test price information
|
2025-12-22 20:12:19 +01:00
|
|
|
await expect(page.getByText("Market:")).toBeVisible();
|
|
|
|
|
await expect(page.getByText("Our price:")).toBeVisible();
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Test buy/sell toggle visibility and functionality
|
|
|
|
|
const buyButton = page.getByRole("button", { name: "Buy BTC" });
|
|
|
|
|
await expect(buyButton).toBeVisible();
|
2025-12-22 20:12:19 +01:00
|
|
|
await expect(page.getByRole("button", { name: "Sell BTC" })).toBeVisible();
|
2025-12-23 14:51:28 +01:00
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Test clicking buy/sell changes direction
|
|
|
|
|
await page.getByRole("button", { name: "Sell BTC" }).click();
|
2025-12-25 22:35:27 +01:00
|
|
|
// The summary text is split across elements, so we check for the text parts separately
|
|
|
|
|
await expect(page.getByText(/You buy/)).toBeVisible();
|
|
|
|
|
await expect(page.getByText(/€\d/)).toBeVisible();
|
2025-12-25 00:06:32 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test payment method selector
|
2025-12-23 14:51:28 +01:00
|
|
|
await expect(page.getByText("Payment Method")).toBeVisible();
|
|
|
|
|
await expect(page.getByRole("button", { name: /Onchain/ })).toBeVisible();
|
|
|
|
|
await expect(page.getByRole("button", { name: /Lightning/ })).toBeVisible();
|
2025-12-22 20:12:19 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test amount slider
|
2025-12-22 20:12:19 +01:00
|
|
|
await expect(page.getByText("Amount")).toBeVisible();
|
|
|
|
|
await expect(page.locator('input[type="range"]')).toBeVisible();
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Test date selection appears after continue
|
2025-12-23 11:30:27 +01:00
|
|
|
await page.getByRole("button", { name: "Continue to Booking" }).click();
|
2025-12-22 20:12:19 +01:00
|
|
|
await expect(page.getByRole("heading", { name: "Select a Date" })).toBeVisible();
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
test("booking flow - shows slots, confirmation form, and trade details", async ({ page }) => {
|
2025-12-22 20:12:19 +01:00
|
|
|
await page.goto("/exchange");
|
|
|
|
|
|
2025-12-23 11:30:27 +01:00
|
|
|
// 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
|
2025-12-23 11:00:32 +01:00
|
|
|
const tomorrowStr = getTomorrowDateStr();
|
|
|
|
|
const dateButton = page.getByTestId(`date-${tomorrowStr}`);
|
2025-12-22 21:42:42 +01:00
|
|
|
await expect(dateButton).toBeEnabled({ timeout: 15000 });
|
2025-12-22 20:12:19 +01:00
|
|
|
await dateButton.click();
|
|
|
|
|
|
|
|
|
|
// Wait for "Available Slots" section to appear
|
|
|
|
|
await expect(page.getByRole("heading", { name: /Available Slots for/ })).toBeVisible();
|
|
|
|
|
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 });
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Click first slot - should show confirmation form
|
2025-12-22 20:12:19 +01:00
|
|
|
await slotButtons.first().click();
|
|
|
|
|
await expect(page.getByText("Confirm Trade")).toBeVisible();
|
|
|
|
|
await expect(page.getByRole("button", { name: /Confirm/ })).toBeVisible();
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Navigate back to exchange and test second slot selection
|
2025-12-22 20:12:19 +01:00
|
|
|
await page.goto("/exchange");
|
2025-12-23 11:30:27 +01:00
|
|
|
await page.getByRole("button", { name: "Continue to Booking" }).click();
|
2025-12-24 23:52:52 +01:00
|
|
|
await page.getByTestId(`date-${tomorrowStr}`).click();
|
2025-12-22 20:12:19 +01:00
|
|
|
await expect(page.getByText("Loading slots...")).not.toBeVisible({ timeout: 10000 });
|
2025-12-24 23:52:52 +01:00
|
|
|
const slotButtons2 = page.locator("button").filter({ hasText: /^\d{1,2}:\d{2}/ });
|
|
|
|
|
await expect(slotButtons2.first()).toBeVisible({ timeout: 10000 });
|
|
|
|
|
|
|
|
|
|
// Click second slot if available, otherwise first
|
|
|
|
|
if ((await slotButtons2.count()) > 1) {
|
|
|
|
|
await slotButtons2.nth(1).click();
|
|
|
|
|
} else {
|
|
|
|
|
await slotButtons2.first().click();
|
|
|
|
|
}
|
2025-12-22 20:12:19 +01:00
|
|
|
|
|
|
|
|
// 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();
|
2025-12-23 14:51:28 +01:00
|
|
|
await expect(page.getByText("Payment:")).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
test("payment method selector works and lightning disabled above threshold", async ({ page }) => {
|
2025-12-23 14:51:28 +01:00
|
|
|
await page.goto("/exchange");
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Test payment method selector
|
2025-12-23 14:51:28 +01:00
|
|
|
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)");
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Test lightning disabled above threshold
|
2025-12-23 14:51:28 +01:00
|
|
|
const amountInput = page.locator('input[type="text"]').filter({ hasText: "" });
|
|
|
|
|
await amountInput.fill("1100");
|
|
|
|
|
await expect(lightningButton).toBeDisabled();
|
|
|
|
|
await expect(page.getByText(/Lightning payments are only available/)).toBeVisible();
|
2025-12-22 20:12:19 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe("Exchange Page - Access Control", () => {
|
2025-12-24 23:52:52 +01:00
|
|
|
test("admin and unauthenticated users cannot access exchange page", async ({ page }) => {
|
|
|
|
|
// Test unauthenticated access
|
2025-12-22 20:12:19 +01:00
|
|
|
await clearAuth(page);
|
|
|
|
|
await page.goto("/exchange");
|
2025-12-24 23:52:52 +01:00
|
|
|
await expect(page).toHaveURL("/login");
|
2025-12-22 20:12:19 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test admin access
|
2025-12-22 20:12:19 +01:00
|
|
|
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
|
|
|
|
|
await page.goto("/admin/trades");
|
|
|
|
|
await expect(page.getByRole("link", { name: "Exchange" })).not.toBeVisible();
|
|
|
|
|
|
|
|
|
|
await page.goto("/exchange");
|
2025-12-24 23:52:52 +01:00
|
|
|
await expect(page).not.toHaveURL("/exchange");
|
2025-12-22 20:12:19 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe("Trades Page", () => {
|
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
|
|
|
await clearAuth(page);
|
|
|
|
|
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
test("regular user can access trades page and see empty state", async ({ page }) => {
|
2025-12-22 20:12:19 +01:00
|
|
|
await page.goto("/trades");
|
|
|
|
|
|
|
|
|
|
await expect(page).toHaveURL("/trades");
|
|
|
|
|
await expect(page.getByRole("heading", { name: "My Trades" })).toBeVisible();
|
|
|
|
|
|
|
|
|
|
// Either shows empty state message or trades list
|
|
|
|
|
const content = page.locator("body");
|
|
|
|
|
await expect(content).toBeVisible();
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Wait for loading to finish
|
2025-12-23 12:26:43 +01:00
|
|
|
await expect(page.getByText("Loading trades...")).not.toBeVisible({ timeout: 5000 });
|
2025-12-22 20:12:19 +01:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
test("admin can access trades page with tabs, regular user cannot", async ({ page }) => {
|
|
|
|
|
// Test admin access
|
2025-12-22 20:12:19 +01:00
|
|
|
await page.goto("/admin/trades");
|
|
|
|
|
await expect(page).toHaveURL("/admin/trades");
|
|
|
|
|
await expect(page.getByRole("heading", { name: "Trades" })).toBeVisible();
|
|
|
|
|
await expect(page.getByRole("button", { name: /Upcoming/ })).toBeVisible();
|
|
|
|
|
await expect(page.getByRole("button", { name: /History/ })).toBeVisible();
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Test regular user cannot access
|
2025-12-22 20:12:19 +01:00
|
|
|
await clearAuth(page);
|
|
|
|
|
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
|
|
|
|
await page.goto("/admin/trades");
|
|
|
|
|
await expect(page).not.toHaveURL("/admin/trades");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe("Exchange API", () => {
|
2025-12-24 23:52:52 +01:00
|
|
|
test("API access control - regular user can access exchange APIs, admin cannot", async ({
|
|
|
|
|
page,
|
|
|
|
|
request,
|
|
|
|
|
}) => {
|
|
|
|
|
// Test regular user can get price
|
2025-12-22 20:12:19 +01:00
|
|
|
await clearAuth(page);
|
|
|
|
|
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
2025-12-24 23:52:52 +01:00
|
|
|
let cookies = await page.context().cookies();
|
|
|
|
|
let authCookie = cookies.find((c) => c.name === "auth_token");
|
2025-12-22 20:12:19 +01:00
|
|
|
|
|
|
|
|
if (authCookie) {
|
2025-12-24 23:52:52 +01:00
|
|
|
const priceResponse = await request.get(`${API_URL}/api/exchange/price`, {
|
2025-12-22 20:12:19 +01:00
|
|
|
headers: {
|
|
|
|
|
Cookie: `auth_token=${authCookie.value}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-12-24 23:52:52 +01:00
|
|
|
expect(priceResponse.status()).toBe(200);
|
|
|
|
|
const priceData = await priceResponse.json();
|
|
|
|
|
expect(priceData.config).toBeDefined();
|
|
|
|
|
expect(priceData.config.eur_min).toBeDefined();
|
|
|
|
|
expect(priceData.config.eur_max).toBeDefined();
|
|
|
|
|
|
|
|
|
|
// Test regular user can get trades
|
|
|
|
|
const tradesResponse = await request.get(`${API_URL}/api/trades`, {
|
2025-12-22 20:12:19 +01:00
|
|
|
headers: {
|
|
|
|
|
Cookie: `auth_token=${authCookie.value}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-12-24 23:52:52 +01:00
|
|
|
expect(tradesResponse.status()).toBe(200);
|
|
|
|
|
const tradesData = await tradesResponse.json();
|
|
|
|
|
expect(Array.isArray(tradesData)).toBe(true);
|
2025-12-22 20:12:19 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test admin cannot get price
|
2025-12-22 20:12:19 +01:00
|
|
|
await clearAuth(page);
|
2025-12-24 23:52:52 +01:00
|
|
|
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
|
|
|
|
|
cookies = await page.context().cookies();
|
|
|
|
|
authCookie = cookies.find((c) => c.name === "auth_token");
|
2025-12-22 20:12:19 +01:00
|
|
|
|
|
|
|
|
if (authCookie) {
|
2025-12-24 23:52:52 +01:00
|
|
|
const adminPriceResponse = await request.get(`${API_URL}/api/exchange/price`, {
|
2025-12-22 20:12:19 +01:00
|
|
|
headers: {
|
|
|
|
|
Cookie: `auth_token=${authCookie.value}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-12-24 23:52:52 +01:00
|
|
|
expect(adminPriceResponse.status()).toBe(403);
|
2025-12-22 20:12:19 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test admin can get upcoming trades
|
|
|
|
|
const adminTradesResponse = await request.get(`${API_URL}/api/admin/trades/upcoming`, {
|
2025-12-22 20:12:19 +01:00
|
|
|
headers: {
|
|
|
|
|
Cookie: `auth_token=${authCookie.value}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-12-24 23:52:52 +01:00
|
|
|
expect(adminTradesResponse.status()).toBe(200);
|
|
|
|
|
const adminTradesData = await adminTradesResponse.json();
|
|
|
|
|
expect(Array.isArray(adminTradesData)).toBe(true);
|
2025-12-22 20:12:19 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|