import { test, expect } from '@playwright/test' // Mock Helcim API responses for join flow (avoids dependency on external API) function mockHelcimAPIs(page, { failCustomer = false } = {}) { page.route('**/api/helcim/customer', async (route) => { if (failCustomer) { return route.fulfill({ status: 409, contentType: 'application/json', body: JSON.stringify({ statusCode: 409, statusMessage: 'A member with this email already exists', message: 'A member with this email already exists' }) }) } return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, customerId: 'test-cust-123', customerCode: 'CUST-TEST-001' }) }) }) page.route('**/api/helcim/subscription', async (route) => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, subscription: { id: 'test-sub-123', status: 'ACTIVE' } }) }) }) } test.describe('Join page — member signup flow', () => { test('join form loads with all fields', async ({ page }) => { await page.goto('/join') await page.waitForLoadState('networkidle') await expect(page.locator('#join-name')).toBeVisible() await expect(page.locator('#join-email')).toBeVisible() await expect(page.locator('#pwyc-0')).toBeAttached() await expect(page.locator('#pwyc-15')).toBeAttached() await expect(page.locator('#pwyc-50')).toBeAttached() await expect(page.getByTestId('cadence-monthly')).toBeVisible() await expect(page.getByTestId('cadence-annual')).toBeVisible() await expect(page.locator('.submit-btn')).toBeVisible() }) test('submit button disabled when form incomplete', async ({ page }) => { await page.goto('/join') await page.waitForLoadState('networkidle') // Clear name and email — contribution defaults to $15 await page.locator('#join-name').fill('') await page.locator('#join-email').fill('') await expect(page.locator('.submit-btn')).toBeDisabled() // Fill only name — still incomplete await page.locator('#join-name').fill('Test User') await expect(page.locator('.submit-btn')).toBeDisabled() // Fill email — agreement still unchecked, so still disabled await page.locator('#join-email').fill('incomplete-test@example.com') await expect(page.locator('.submit-btn')).toBeDisabled() // Check the Community Guidelines agreement — now all required fields satisfied await page.getByRole('checkbox', { name: /Community Guidelines/ }).check() await expect(page.locator('.submit-btn')).toBeEnabled() }) test('fill and submit free tier', async ({ page }) => { const uniqueEmail = `test-e2e-${Date.now()}@example.com` await page.goto('/join') await page.waitForLoadState('networkidle') await page.locator('#join-name').fill('E2E Test User') await page.locator('#join-email').fill(uniqueEmail) // Pick the $0 preset await page.locator('label[for="pwyc-0"]').click() await page.getByRole('checkbox', { name: /Community Guidelines/ }).check() await expect(page.locator('.submit-btn')).toBeEnabled() await mockHelcimAPIs(page) await page.locator('.submit-btn').click() // Free tier flips the SignupFlowOverlay into its success state await expect( page.getByRole('heading', { name: 'Welcome to Ghost Guild!' }) ).toBeVisible({ timeout: 15000 }) }) test('cadence toggle multiplies preset row amounts by 12', async ({ page }) => { await page.goto('/join') await page.waitForLoadState('networkidle') // Default is Monthly: the $15 row reads "$15" const suggestedRow = page.locator('#pwyc-15 + .pwyc-row-content .pwyc-amt') await expect(suggestedRow).toHaveText('$15') // Switch to Annual: same row now reads "$180" await page.getByTestId('cadence-annual').click() await expect(suggestedRow).toHaveText('$180') // Switch back to Monthly: returns to "$15" await page.getByTestId('cadence-monthly').click() await expect(suggestedRow).toHaveText('$15') }) test('paid tier flow reaches success state with HelcimPay stubbed', async ({ page }) => { const uniqueEmail = `test-e2e-paid-${Date.now()}@example.com` await page.addInitScript(() => { window.appendHelcimPayIframe = (checkoutToken) => { const eventName = 'helcim-pay-js-' + checkoutToken setTimeout(() => { window.postMessage({ eventName, eventStatus: 'SUCCESS', eventMessage: JSON.stringify({ data: { data: { transactionId: 'stub-txn-1', cardToken: 'stub-card-token-1', cardNumber: '4111111111111234', cardType: 'visa' } } }) }, '*') }, 50) } window.removeHelcimPayIframe = () => {} }) await page.goto('/join') await page.waitForLoadState('networkidle') await mockHelcimAPIs(page) await page.route('**/api/helcim/initialize-payment', async (route) => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, checkoutToken: 'stub-checkout-token', secretToken: 'stub-secret-token' }) }) }) await page.route('**/api/helcim/verify-payment', async (route) => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true }) }) }) await page.locator('#join-name').fill('Paid E2E User') await page.locator('#join-email').fill(uniqueEmail) // $15 preset is selected by default — verify and submit await page.locator('#pwyc-15').check({ force: true }) await page.getByRole('checkbox', { name: /Community Guidelines/ }).check() await expect(page.locator('.submit-btn')).toBeEnabled() await page.locator('.submit-btn').click() await expect( page.getByRole('heading', { name: 'Welcome to Ghost Guild!' }) ).toBeVisible({ timeout: 15000 }) }) test('duplicate email shows error', async ({ page }) => { const duplicateEmail = `test-e2e-dup-${Date.now()}@example.com` await page.goto('/join') await page.waitForLoadState('networkidle') await mockHelcimAPIs(page, { failCustomer: true }) await page.locator('#join-name').fill('Dup Test User') await page.locator('#join-email').fill(duplicateEmail) await page.locator('label[for="pwyc-0"]').click() await page.getByRole('checkbox', { name: /Community Guidelines/ }).check() await page.locator('.submit-btn').click() // Helcim 409 puts SignupFlowOverlay into its error state const overlayError = page.locator('.signup-flow-overlay .error-box') await expect(overlayError).toBeVisible({ timeout: 10000 }) await expect(overlayError).toContainText(/already/i) }) })