Phase 4.2: Add Exchange E2E tests
New exchange.spec.ts with comprehensive E2E coverage: - Exchange page access and UI elements (price, buy/sell, slider) - Slot selection with availability - Confirmation form with trade details - Access control (regular user vs admin) - My Trades page - Admin Trades page with tabs - Exchange API permission tests
This commit is contained in:
parent
ab908c172e
commit
9e8d0af435
1 changed files with 376 additions and 0 deletions
376
frontend/e2e/exchange.spec.ts
Normal file
376
frontend/e2e/exchange.spec.ts
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
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 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");
|
||||
|
||||
// Click Sell BTC
|
||||
await page.getByRole("button", { name: "Sell BTC" }).click();
|
||||
|
||||
// The summary should mention "You send" for sell
|
||||
await expect(page.getByText(/You send/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("exchange page shows date selection", async ({ page }) => {
|
||||
await page.goto("/exchange");
|
||||
|
||||
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");
|
||||
|
||||
// Get tomorrow's display name to click the correct button
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" });
|
||||
|
||||
// Click tomorrow's date using the weekday name
|
||||
const dateButton = page
|
||||
.locator("button")
|
||||
.filter({ hasText: new RegExp(`^${weekday}`) })
|
||||
.first();
|
||||
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");
|
||||
|
||||
// Get tomorrow's display name
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" });
|
||||
|
||||
// Click tomorrow's date
|
||||
const dateButton = page
|
||||
.locator("button")
|
||||
.filter({ hasText: new RegExp(`^${weekday}`) })
|
||||
.first();
|
||||
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");
|
||||
|
||||
// Get tomorrow's display name
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const weekday = tomorrow.toLocaleDateString("en-US", { weekday: "short" });
|
||||
|
||||
// Click tomorrow's date
|
||||
const dateButton = page
|
||||
.locator("button")
|
||||
.filter({ hasText: new RegExp(`^${weekday}`) })
|
||||
.first();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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 content to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue