more merging

This commit is contained in:
counterweight 2025-12-25 00:06:32 +01:00
parent 67ffe6a823
commit d6f955d2d9
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
5 changed files with 107 additions and 251 deletions

View file

@ -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", () => {

View file

@ -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();
});
});

View file

@ -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");
});
});

View file

@ -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", () => {

View file

@ -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",
},
});