ghostguild-org/e2e/join-flow.spec.js
Jennie Robinson Faber 8dd55ccc09 test(e2e): expand coverage and harden cross-file isolation
New specs (4):
- accept-invite: pre-registrant flow happy path + cadence/preset UX
- admin-pre-registrants: list, filter, action gating, redirect
- admin-series: list, create, edit (delete skipped — button no-ops)
- admin-site-content: list whitelist, edit + roundtrip on /

Extended specs (6):
- join-flow: cadence ×12 math, guidance label, paid-tier success
- events: series-pass-required, member-savings gating
- admin-events: full CRUD via /admin/events/create?edit=<id>
- admin-members: add-member submit, status select, detail nav
- a11y: add /accept-invite, /member/account, /board, /admin/pre-registrants
- wave-slack-onboarding: 9 of 16 scaffold tests now passing

Cross-file isolation hardening:
- admin-events CRUD: refresh auth cookie (auth.spec.js logout test
  bumps tokenVersion on the shared admin), wait for hydration
  before form fill, search by unique title to dodge pagination.
- board: switch memberPage from shared admin to dedicated seeded
  member to avoid the same tokenVersion race.
- wave-slack §6.4: create dedicated test member, filter by email
  before clicking, removing the "first row" anchor.

Also fixed board heading drift ("Board" → "Bulletin Board").
2026-04-30 22:26:11 +01:00

226 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from '@playwright/test'
// Mock Helcim API responses for join flow (avoids dependency on external API)
function mockHelcimAPIs(page, { failCustomer = false } = {}) {
// Mock Helcim customer creation
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'
})
})
})
// Mock subscription creation
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('#circle-community')).toBeAttached()
await expect(page.locator('#circle-founder')).toBeAttached()
await expect(page.locator('#circle-practitioner')).toBeAttached()
await expect(page.locator('#join-contribution')).toBeVisible()
await expect(page.locator('.form-submit')).toBeVisible()
})
test('submit button disabled when form incomplete', async ({ page }) => {
await page.goto('/join')
await page.waitForLoadState('networkidle')
// Clear name and email — circle defaults to community, contribution defaults to $15
await page.locator('#join-name').fill('')
await page.locator('#join-email').fill('')
// Button should be disabled with empty required fields
await expect(page.locator('.form-submit')).toBeDisabled()
// Fill only name — still incomplete
await page.locator('#join-name').fill('Test User')
await expect(page.locator('.form-submit')).toBeDisabled()
// Fill email — agreement still unchecked, so still disabled
await page.locator('#join-email').fill('incomplete-test@example.com')
await expect(page.locator('.form-submit')).toBeDisabled()
// Check the Community Guidelines agreement — now all required fields satisfied
await page.getByRole('checkbox', { name: /Community Guidelines/ }).check()
await expect(page.locator('.form-submit')).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')
// Fill in the form
await page.locator('#join-name').fill('E2E Test User')
await page.locator('#join-email').fill(uniqueEmail)
await page.locator('#circle-community').check({ force: true })
// Contribution is now a numeric input with preset chips, not a select
await page.locator('#join-contribution').fill('0')
await page.getByRole('checkbox', { name: /Community Guidelines/ }).check()
await expect(page.locator('.form-submit')).toBeEnabled()
// Mock Helcim APIs before submitting
await mockHelcimAPIs(page)
await page.locator('.form-submit').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 updates billing summary to annual ×12', async ({ page }) => {
await page.goto('/join')
await page.waitForLoadState('networkidle')
await page.locator('#join-contribution').fill('10')
await page.locator('label[for="cadence-annual"]').click()
const summary = page.locator('.billing-summary')
await expect(summary).toBeVisible()
await expect(summary).toContainText('$120 today')
await expect(summary).toContainText('$10/month × 12')
await expect(summary).toContainText('$120 every year')
await page.locator('label[for="cadence-monthly"]').click()
await expect(summary).toContainText('$10 today')
await expect(summary).toContainText('$10 every month')
})
test('contribution guidance label changes with amount tier', async ({ page }) => {
await page.goto('/join')
await page.waitForLoadState('networkidle')
const guidance = page.locator('.contribution-guidance')
await page.locator('#join-contribution').fill('5')
await expect(guidance).toHaveText(/I can contribute/)
await page.locator('#join-contribution').fill('30')
await expect(guidance).toHaveText(/I can support others too/)
})
test('paid tier flow reaches success state with HelcimPay stubbed', async ({ page }) => {
const uniqueEmail = `test-e2e-paid-${Date.now()}@example.com`
// Stub HelcimPay window globals before the page loads so the composable's
// script-load path is bypassed and we resolve verifyPayment synchronously.
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)
await page.locator('#circle-community').check({ force: true })
await page.locator('#join-contribution').fill('15')
await page.getByRole('checkbox', { name: /Community Guidelines/ }).check()
await expect(page.locator('.form-submit')).toBeEnabled()
await page.locator('.form-submit').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')
// Mock customer endpoint to return 409 (email already exists)
await mockHelcimAPIs(page, { failCustomer: true })
await page.locator('#join-name').fill('Dup Test User')
await page.locator('#join-email').fill(duplicateEmail)
await page.locator('#circle-community').check({ force: true })
await page.locator('#join-contribution').fill('0')
await page.getByRole('checkbox', { name: /Community Guidelines/ }).check()
await page.locator('.form-submit').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)
})
})