lilith-platform.live/codebase/@features/landing/e2e/interactions.spec.ts
Claude Code 25d2c7ad65 init(codebase-default): 🎉 Implement foundational directory structure with core modules and utility files
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-03-25 22:50:24 -07:00

874 lines
33 KiB
TypeScript

/**
* Interaction Tests — every clickable element on the landing site
*
* Tests verify that UI elements respond correctly when interacted with:
* navigation, modal flows, card clicks, FAQ accordions, CTAs, footer links.
*
* Age gate: bypassed via localStorage injection in beforeEach.
* Base URL: configured in playwright.config.ts (env PLAYWRIGHT_BASE_URL or localhost:5100).
*/
import { test, expect, type Page } from '@playwright/test'
// ---------------------------------------------------------------------------
// Age gate bypass (copied from routes.spec.ts for isolation)
// ---------------------------------------------------------------------------
const AGE_VERIFICATION_KEY = 'lilith-age-verified'
async function bypassAgeGate(page: Page): Promise<void> {
await page.addInitScript((key) => {
const status = JSON.stringify({
isVerified: true,
method: 'self-declaration',
tier: 1,
verifiedAt: new Date().toISOString(),
})
localStorage.setItem(key, status)
sessionStorage.setItem(key, status)
const origLocalRemove = localStorage.removeItem.bind(localStorage)
localStorage.removeItem = (k: string) => {
if (k === key) { localStorage.setItem(key, status); return }
origLocalRemove(k)
}
const origSessionRemove = sessionStorage.removeItem.bind(sessionStorage)
sessionStorage.removeItem = (k: string) => {
if (k === key) { sessionStorage.setItem(key, status); return }
origSessionRemove(k)
}
const origLocalClear = localStorage.clear.bind(localStorage)
localStorage.clear = () => { origLocalClear(); localStorage.setItem(key, status) }
const origSessionClear = sessionStorage.clear.bind(sessionStorage)
sessionStorage.clear = () => { origSessionClear(); sessionStorage.setItem(key, status) }
}, AGE_VERIFICATION_KEY)
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function waitForSimon(page: Page): Promise<void> {
await page.waitForSelector('[data-testid="simon-container"]', { timeout: 10_000 })
// Wait for Framer Motion enter animations to complete (max delay = 3 * 0.1s + 0.5s duration)
await page.waitForTimeout(800)
}
async function waitForPageContent(page: Page): Promise<void> {
await page.waitForSelector('h1, [data-testid="simon-container"]', { timeout: 10_000 })
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
test.beforeEach(async ({ page }) => {
await bypassAgeGate(page)
})
// ── 1. SimonSelector Interactions ─────────────────────────────────────────────
test.describe('SimonSelector', () => {
test('clicking Provider quadrant navigates to /info/provider', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByTestId('provider-quadrant').click({ force: true })
await expect(page).toHaveURL(/\/info\/provider/, { timeout: 10_000 })
})
test('clicking Client quadrant navigates to /info/client', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByTestId('client-quadrant').click({ force: true })
await expect(page).toHaveURL(/\/info\/client/, { timeout: 10_000 })
})
test('clicking Creator quadrant navigates to /info/creator', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByTestId('creator-quadrant').click({ force: true })
await expect(page).toHaveURL(/\/info\/creator/, { timeout: 10_000 })
})
test('clicking Fan quadrant navigates to /info/fan', async ({ page }) => {
// Use a taller viewport so the bottom-right fan quadrant is fully within the visible area
await page.setViewportSize({ width: 1280, height: 1000 })
await page.goto('/')
await waitForSimon(page)
const fanQuadrant = page.getByTestId('fan-quadrant')
await fanQuadrant.scrollIntoViewIfNeeded()
await fanQuadrant.click({ force: true })
await expect(page).toHaveURL(/\/info\/fan/, { timeout: 10_000 })
})
test('center orb is present and not a link or button', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
const center = page.getByTestId('simon-center')
await expect(center).toBeVisible()
// aria-hidden means it is decorative only
await expect(center).toHaveAttribute('aria-hidden', 'true')
})
test('all four quadrant labels are present', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
const labels = page.locator('.quadrant-label')
await expect(labels).toHaveCount(4)
})
})
// ── 2. Navigation ──────────────────────────────────────────────────────────────
test.describe('Navigation — top-level links', () => {
test('clicking "For Workers" nav link navigates to /workers', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByRole('link', { name: 'For Workers' }).click()
await expect(page).toHaveURL('/workers', { timeout: 10_000 })
await waitForPageContent(page)
await expect(page.locator('h1')).toContainText('For Workers')
})
test('clicking "For Clients" nav link navigates to /customers', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByRole('link', { name: 'For Clients' }).click()
await expect(page).toHaveURL('/customers', { timeout: 10_000 })
await waitForPageContent(page)
await expect(page.locator('h1')).toContainText('For Customers')
})
test('clicking "Company" nav link navigates to /company', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByRole('link', { name: 'Company' }).click()
await expect(page).toHaveURL('/company', { timeout: 10_000 })
await waitForPageContent(page)
await expect(page.locator('h1')).toContainText('Company')
})
test('clicking "lilith" brand logo navigates to /', async ({ page }) => {
await page.goto('/workers')
await waitForPageContent(page)
await page.getByRole('link', { name: 'lilith' }).click()
await expect(page).toHaveURL('/', { timeout: 10_000 })
await waitForSimon(page)
})
test('clicking "Register" button navigates to /register and opens modal', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByRole('button', { name: 'Register' }).click()
await expect(page).toHaveURL('/register', { timeout: 10_000 })
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10_000 })
})
})
test.describe('Navigation — Workers dropdown', () => {
test('Workers dropdown contains Providers link to /workers/escort', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
// Hover to open dropdown
await page.getByRole('link', { name: 'For Workers' }).hover()
await expect(page.getByRole('link', { name: 'Providers' })).toBeVisible({ timeout: 5_000 })
await page.getByRole('link', { name: 'Providers' }).click()
await expect(page).toHaveURL('/workers/escort', { timeout: 10_000 })
})
test('Workers dropdown contains Performers link to /workers/performer', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByRole('link', { name: 'For Workers' }).hover()
await expect(page.getByRole('link', { name: 'Performers' })).toBeVisible({ timeout: 5_000 })
await page.getByRole('link', { name: 'Performers' }).click()
await expect(page).toHaveURL('/workers/performer', { timeout: 10_000 })
})
test('Workers dropdown contains Fangirls link to /workers/fangirl', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByRole('link', { name: 'For Workers' }).hover()
await expect(page.getByRole('link', { name: 'Fangirls' })).toBeVisible({ timeout: 5_000 })
await page.getByRole('link', { name: 'Fangirls' }).click()
await expect(page).toHaveURL('/workers/fangirl', { timeout: 10_000 })
})
test('Workers dropdown contains Camgirls link to /workers/camgirl', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
await page.getByRole('link', { name: 'For Workers' }).hover()
await expect(page.getByRole('link', { name: 'Camgirls' })).toBeVisible({ timeout: 5_000 })
await page.getByRole('link', { name: 'Camgirls' }).click()
await expect(page).toHaveURL('/workers/camgirl', { timeout: 10_000 })
})
})
test.describe('Navigation — Clients dropdown', () => {
test('Clients dropdown contains Clients link to /customers/client', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
// Scope to nav, use exact match to distinguish "Clients" from "For Clients"
const nav = page.locator('nav').first()
await nav.getByRole('link', { name: 'For Clients' }).hover()
await expect(nav.getByRole('link', { name: 'Clients', exact: true })).toBeVisible({ timeout: 5_000 })
await nav.getByRole('link', { name: 'Clients', exact: true }).click()
await expect(page).toHaveURL('/customers/client', { timeout: 10_000 })
})
test('Clients dropdown contains Fans link to /customers/fan', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
const nav = page.locator('nav').first()
await nav.getByRole('link', { name: 'For Clients' }).hover()
await expect(nav.getByRole('link', { name: 'Fans' })).toBeVisible({ timeout: 5_000 })
await nav.getByRole('link', { name: 'Fans' }).click()
await expect(page).toHaveURL('/customers/fan', { timeout: 10_000 })
})
test('Clients dropdown contains Client Pricing link to /pricing/client', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
const nav = page.locator('nav').first()
await nav.getByRole('link', { name: 'For Clients' }).hover()
await expect(nav.getByRole('link', { name: 'Client Pricing' })).toBeVisible({ timeout: 5_000 })
await nav.getByRole('link', { name: 'Client Pricing' }).click()
await expect(page).toHaveURL('/pricing/client', { timeout: 10_000 })
})
test('Clients dropdown contains Fan Pricing link to /pricing/fan', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
const nav = page.locator('nav').first()
await nav.getByRole('link', { name: 'For Clients' }).hover()
await expect(nav.getByRole('link', { name: 'Fan Pricing' })).toBeVisible({ timeout: 5_000 })
await nav.getByRole('link', { name: 'Fan Pricing' }).click()
await expect(page).toHaveURL('/pricing/fan', { timeout: 10_000 })
})
})
test.describe('Navigation — Company dropdown', () => {
test('Company dropdown contains Terms of Service link to /company/terms', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
// Scope to nav to avoid ambiguity with footer "Terms of Service" link
const nav = page.locator('nav').first()
await nav.getByRole('link', { name: 'Company' }).hover()
// Wait for dropdown to open
await page.waitForTimeout(300)
// The dropdown items are in a <ul> under the Company nav item
const dropdown = page.locator('nav li ul').filter({ hasText: 'Terms of Service' })
await expect(dropdown.getByRole('link', { name: 'Terms of Service' })).toBeVisible({ timeout: 5_000 })
await dropdown.getByRole('link', { name: 'Terms of Service' }).click()
await expect(page).toHaveURL('/company/terms', { timeout: 10_000 })
})
test('Company dropdown contains Privacy Policy link to /company/privacy', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
const nav = page.locator('nav').first()
await nav.getByRole('link', { name: 'Company', exact: true }).hover()
await page.waitForTimeout(300)
// Get the dropdown under Company nav item — filter to the <ul> that contains both Terms and Privacy
const dropdown = page.locator('nav li ul').filter({ hasText: 'Terms of Service' })
await expect(dropdown.getByRole('link', { name: 'Privacy Policy', exact: true })).toBeVisible({ timeout: 5_000 })
await dropdown.getByRole('link', { name: 'Privacy Policy', exact: true }).click()
await expect(page).toHaveURL('/company/privacy', { timeout: 10_000 })
})
})
// ── 3. Category Pages → Card Navigation ───────────────────────────────────────
test.describe('/workers card navigation', () => {
test('clicking Providers card navigates to /workers/escort', async ({ page }) => {
await page.goto('/workers')
await waitForPageContent(page)
const main = page.locator('main')
await main.getByRole('heading', { name: 'Providers' }).waitFor({ timeout: 10_000 })
// The card is a link — click the heading or the card container
await main.getByRole('link', { name: /Providers/ }).first().click()
await expect(page).toHaveURL('/workers/escort', { timeout: 10_000 })
})
test('clicking Performers card navigates to /workers/performer', async ({ page }) => {
await page.goto('/workers')
await waitForPageContent(page)
const main = page.locator('main')
await main.getByRole('heading', { name: 'Performers' }).waitFor({ timeout: 10_000 })
await main.getByRole('link', { name: /Performers/ }).first().click()
await expect(page).toHaveURL('/workers/performer', { timeout: 10_000 })
})
test('clicking Fangirls card navigates to /workers/fangirl', async ({ page }) => {
await page.goto('/workers')
await waitForPageContent(page)
const main = page.locator('main')
await main.getByRole('heading', { name: 'Fangirls' }).waitFor({ timeout: 10_000 })
await main.getByRole('link', { name: /Fangirls/ }).first().click()
await expect(page).toHaveURL('/workers/fangirl', { timeout: 10_000 })
})
test('clicking Camgirls card navigates to /workers/camgirl', async ({ page }) => {
await page.goto('/workers')
await waitForPageContent(page)
const main = page.locator('main')
await main.getByRole('heading', { name: 'Camgirls' }).waitFor({ timeout: 10_000 })
await main.getByRole('link', { name: /Camgirls/ }).first().click()
await expect(page).toHaveURL('/workers/camgirl', { timeout: 10_000 })
})
test('"Get Started" CTA navigates to /register', async ({ page }) => {
await page.goto('/workers')
await waitForPageContent(page)
await page.getByRole('link', { name: /Get Started/i }).click()
await expect(page).toHaveURL('/register', { timeout: 10_000 })
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10_000 })
})
})
test.describe('/customers card navigation', () => {
test('clicking Clients card navigates to /customers/client', async ({ page }) => {
await page.goto('/customers')
await waitForPageContent(page)
const main = page.locator('main')
await main.getByRole('heading', { name: 'Clients' }).waitFor({ timeout: 10_000 })
await main.getByRole('link', { name: /Clients/ }).first().click()
await expect(page).toHaveURL('/customers/client', { timeout: 10_000 })
})
test('clicking Fans card navigates to /customers/fan', async ({ page }) => {
await page.goto('/customers')
await waitForPageContent(page)
const main = page.locator('main')
await main.getByRole('heading', { name: 'Fans' }).waitFor({ timeout: 10_000 })
await main.getByRole('link', { name: /Fans/ }).first().click()
await expect(page).toHaveURL('/customers/fan', { timeout: 10_000 })
})
})
test.describe('/company card navigation', () => {
test('clicking Terms card navigates to /company/terms', async ({ page }) => {
await page.goto('/company')
await waitForPageContent(page)
const main = page.locator('main')
await main.getByRole('heading', { name: 'Terms of Service' }).waitFor({ timeout: 10_000 })
await main.getByRole('link', { name: /Terms of Service/ }).first().click()
await expect(page).toHaveURL('/company/terms', { timeout: 10_000 })
})
test('clicking Privacy card navigates to /company/privacy', async ({ page }) => {
await page.goto('/company')
await waitForPageContent(page)
const main = page.locator('main')
await main.getByRole('heading', { name: 'Privacy Policy' }).waitFor({ timeout: 10_000 })
await main.getByRole('link', { name: /Privacy Policy/ }).first().click()
await expect(page).toHaveURL('/company/privacy', { timeout: 10_000 })
})
})
// ── 4. InfoPage Interactions ───────────────────────────────────────────────────
test.describe('InfoPage — /workers/escort (ProviderPage)', () => {
test('FAQ section is present with accordion items', async ({ page }) => {
await page.goto('/workers/escort')
await waitForPageContent(page)
// Wait for FAQ section
await expect(page.getByText('Frequently Asked Questions')).toBeVisible({ timeout: 15_000 })
// FAQ items use <details> element (InfoFaqItem in styles)
const faqItems = page.locator('details')
await expect(faqItems.first()).toBeVisible({ timeout: 10_000 })
})
test('clicking a FAQ item expands it (accordion open)', async ({ page }) => {
await page.goto('/workers/escort')
await waitForPageContent(page)
await expect(page.getByText('Frequently Asked Questions')).toBeVisible({ timeout: 15_000 })
const firstFaq = page.locator('details').first()
await firstFaq.waitFor({ timeout: 10_000 })
// Click the summary to open
await firstFaq.locator('summary').click()
await expect(firstFaq).toHaveAttribute('open', { timeout: 5_000 })
})
test('clicking an open FAQ item collapses it', async ({ page }) => {
await page.goto('/workers/escort')
await waitForPageContent(page)
await expect(page.getByText('Frequently Asked Questions')).toBeVisible({ timeout: 15_000 })
const firstFaq = page.locator('details').first()
await firstFaq.waitFor({ timeout: 10_000 })
// Open it
await firstFaq.locator('summary').click()
await expect(firstFaq).toHaveAttribute('open', { timeout: 5_000 })
// Close it
await firstFaq.locator('summary').click()
await expect(firstFaq).not.toHaveAttribute('open', { timeout: 5_000 })
})
test('"Create Your Profile" CTA navigates to /info/provider', async ({ page }) => {
await page.goto('/workers/escort')
await waitForPageContent(page)
// CTA button at the bottom of the InfoPage
const ctaButton = page.getByRole('link', { name: /Create Your Profile/i })
await ctaButton.waitFor({ timeout: 15_000 })
await ctaButton.click()
await expect(page).toHaveURL('/register/provider', { timeout: 10_000 })
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10_000 })
})
})
// ── 5. Register Modal Flow ─────────────────────────────────────────────────────
test.describe('Register modal', () => {
test('/register renders modal over homepage with "Join Lilith" title', async ({ page }) => {
await page.goto('/register')
await page.waitForLoadState('networkidle')
const modal = page.getByRole('dialog')
await expect(modal).toBeVisible({ timeout: 15_000 })
await expect(modal).toContainText(/join lilith/i)
})
test('modal shows user type selector buttons when no type is pre-selected', async ({ page }) => {
await page.goto('/register')
await page.waitForLoadState('networkidle')
const modal = page.getByRole('dialog')
await expect(modal).toBeVisible({ timeout: 15_000 })
// UserTypeSelector is rendered inside the modal for untyped /register
// Selector buttons exist within the modal
await expect(modal.getByRole('button').first()).toBeVisible({ timeout: 5_000 })
})
test('close button (X) dismisses modal', async ({ page }) => {
// Navigate from / to /register so there is history to go back to
await page.goto('/')
await waitForSimon(page)
await page.goto('/register')
await page.waitForLoadState('domcontentloaded')
const modal = page.getByRole('dialog')
await expect(modal).toBeVisible({ timeout: 15_000 })
await page.getByRole('button', { name: /close modal/i }).click()
await expect(modal).not.toBeVisible({ timeout: 5_000 })
// After close, navigates back in history (to /)
await expect(page).toHaveURL('/', { timeout: 10_000 })
})
test('pressing ESC dismisses modal', async ({ page }) => {
await page.goto('/register')
await page.waitForLoadState('networkidle')
const modal = page.getByRole('dialog')
await expect(modal).toBeVisible({ timeout: 15_000 })
await page.keyboard.press('Escape')
await expect(modal).not.toBeVisible({ timeout: 5_000 })
})
test('clicking modal overlay dismisses modal', async ({ page }) => {
await page.goto('/register')
await page.waitForLoadState('networkidle')
const modal = page.getByRole('dialog')
await expect(modal).toBeVisible({ timeout: 15_000 })
// Click on the overlay backdrop (outside the modal container)
// The overlay is the ModalOverlay div that wraps ModalContainer
await page.mouse.click(10, 10)
await expect(modal).not.toBeVisible({ timeout: 5_000 })
})
test('/register/provider URL resolves to the register route', async ({ page }) => {
// Navigate to the typed register URL — verifies the route is recognized and processed.
// Note: the RegisterForm requires auth backend integration; this test verifies routing only.
await page.goto('/register/provider')
await page.waitForLoadState('domcontentloaded')
// The route /register/provider is a valid route (renders HomePage + modal overlay).
// The URL should match the route pattern.
await expect(page).toHaveURL('/register/provider')
// The SimonSelector (HomePage) renders as the backdrop
await page.waitForSelector('[data-testid="simon-container"]', { timeout: 10_000 })
await expect(page.getByTestId('simon-container')).toBeVisible()
})
})
// ── 6. Newsletter Modal ────────────────────────────────────────────────────────
test.describe('Newsletter modal', () => {
test('/newsletter renders newsletter modal dialog', async ({ page }) => {
await page.goto('/newsletter')
await page.waitForLoadState('networkidle')
const modal = page.locator('[data-context="newsletter"]')
await expect(modal).toBeVisible({ timeout: 15_000 })
await expect(modal).toHaveAttribute('role', 'dialog')
})
test('newsletter modal close button dismisses modal', async ({ page }) => {
await page.goto('/newsletter')
await page.waitForLoadState('networkidle')
const modal = page.locator('[data-context="newsletter"]')
await expect(modal).toBeVisible({ timeout: 15_000 })
await page.getByRole('button', { name: /close modal/i }).click()
await expect(modal).not.toBeVisible({ timeout: 5_000 })
})
})
// ── 7. Footer Links ────────────────────────────────────────────────────────────
test.describe('Legal footer links', () => {
test('"Terms of Service" footer link navigates to /company/terms', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
// Footer is a fixed element — find the link within it
const footer = page.locator('footer')
await expect(footer).toBeVisible()
await footer.getByRole('link', { name: /terms of service/i }).click()
await expect(page).toHaveURL('/company/terms', { timeout: 10_000 })
})
test('"Privacy Policy" footer link navigates to /company/privacy', async ({ page }) => {
await page.goto('/')
await waitForSimon(page)
const footer = page.locator('footer')
await expect(footer).toBeVisible()
await footer.getByRole('link', { name: /privacy policy/i }).click()
await expect(page).toHaveURL('/company/privacy', { timeout: 10_000 })
})
})
// ── 8. Pricing Page Interactions ──────────────────────────────────────────────
test.describe('/pricing/client interactions', () => {
test('"Get Started" button navigates to /info/client', async ({ page }) => {
await page.goto('/pricing/client')
// Pricing pages may take longer due to API fallback (~10s)
await expect(page.locator('h1')).toContainText('Client Subscription Tiers', { timeout: 20_000 })
await page.getByRole('button', { name: /get started/i }).click()
await expect(page).toHaveURL('/register/client', { timeout: 10_000 })
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10_000 })
})
test('"Back to Client Info" link navigates to /customers/client', async ({ page }) => {
await page.goto('/pricing/client')
await expect(page.locator('h1')).toContainText('Client Subscription Tiers', { timeout: 20_000 })
await page.getByRole('link', { name: /back to client info/i }).click()
await expect(page).toHaveURL('/customers/client', { timeout: 10_000 })
})
test('tier cards are visible after load', async ({ page }) => {
await page.goto('/pricing/client')
await expect(page.locator('[data-testid="page-client-pricing"]')).toBeVisible({ timeout: 5_000 })
// Tiers load from API with ~10s fallback — wait for at least one tier label
await expect(page.getByText('Bronze')).toBeVisible({ timeout: 20_000 })
})
})
// ── 9. 404 Page Interactions ──────────────────────────────────────────────────
test.describe('404 page interactions', () => {
test('"Back to Home" link on 404 navigates to /', async ({ page }) => {
await page.goto('/shop')
await page.waitForLoadState('networkidle')
const backLink = page.getByRole('link', { name: 'Back to Home' })
await expect(backLink).toBeVisible({ timeout: 10_000 })
await backLink.click()
await expect(page).toHaveURL('/', { timeout: 10_000 })
await waitForSimon(page)
})
test('"Go Back" button on 404 navigates back in history', async ({ page }) => {
// Navigate to a known page first so there is history to go back to
await page.goto('/workers')
await waitForPageContent(page)
await page.goto('/shop')
await page.waitForLoadState('networkidle')
const goBackButton = page.getByRole('button', { name: /go back/i })
await expect(goBackButton).toBeVisible({ timeout: 10_000 })
await goBackButton.click()
// Should land back on /workers
await expect(page).toHaveURL('/workers', { timeout: 10_000 })
})
})
// ── 10. InfoPanel interactions ────────────────────────────────────────────────
/**
* InfoPanel test matrix:
*
* | userType | CTA label | CTA route | learn more label | learn more route |
* |----------|------------------|-----------------------|------------------------------|---------------------|
* | provider | Join Free | /register/provider | Features Prioritizing You | /workers |
* | client | Browse Profiles | /register/client | See How It Works | /customers/client |
* | creator | Claim Your Spot | /register/creator | Explore Creator Roles | /workers |
* | fan | Join Today | /pricing/fan | Learn More | /pricing/fan |
*/
type InfoPanelConfig = {
userType: string
title: string
ctaLabel: string
ctaRoute: string
learnMoreLabel: string
learnMoreRoute: string
}
const INFO_PANEL_CONFIGS: InfoPanelConfig[] = [
{
userType: 'provider',
title: 'Provider',
ctaLabel: 'Join Free',
ctaRoute: '/register/provider',
learnMoreLabel: 'Features Prioritizing You',
learnMoreRoute: '/workers',
},
{
userType: 'client',
title: 'Client',
ctaLabel: 'Browse Profiles',
ctaRoute: '/register/client',
learnMoreLabel: 'See How It Works',
learnMoreRoute: '/customers/client',
},
{
userType: 'creator',
title: 'Creator',
ctaLabel: 'Claim Your Spot',
ctaRoute: '/register/creator',
learnMoreLabel: 'Explore Creator Roles',
learnMoreRoute: '/workers',
},
{
userType: 'fan',
title: 'Fan',
ctaLabel: 'Join Today',
ctaRoute: '/pricing/fan',
learnMoreLabel: 'Learn More',
learnMoreRoute: '/pricing/fan',
},
]
async function openInfoPanel(page: Page, userType: string): Promise<void> {
await page.goto(`/info/${userType}`)
// Wait for panel slide-in animation to complete
await page.waitForTimeout(500)
}
test.describe('InfoPanel interactions', () => {
for (const config of INFO_PANEL_CONFIGS) {
const { userType, title, ctaLabel, ctaRoute, learnMoreLabel, learnMoreRoute } = config
test.describe(`/info/${userType}`, () => {
test('panel dialog is visible with correct title', async ({ page }) => {
await openInfoPanel(page, userType)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10_000 })
await expect(dialog.getByRole('heading', { name: title })).toBeVisible()
})
test(`primary CTA button "${ctaLabel}" is visible`, async ({ page }) => {
await openInfoPanel(page, userType)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10_000 })
const ctaButton = dialog.getByRole('button', { name: ctaLabel })
await expect(ctaButton).toBeVisible()
})
test(`clicking primary CTA navigates to ${ctaRoute}`, async ({ page }) => {
await openInfoPanel(page, userType)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10_000 })
await dialog.getByRole('button', { name: ctaLabel }).click()
await expect(page).toHaveURL(ctaRoute, { timeout: 10_000 })
})
test(`secondary link "${learnMoreLabel}" is visible`, async ({ page }) => {
await openInfoPanel(page, userType)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10_000 })
const learnMoreLink = dialog.getByRole('link', { name: learnMoreLabel })
await expect(learnMoreLink).toBeVisible()
})
test(`clicking secondary link navigates to ${learnMoreRoute}`, async ({ page }) => {
await openInfoPanel(page, userType)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10_000 })
await dialog.getByRole('link', { name: learnMoreLabel }).click()
await expect(page).toHaveURL(learnMoreRoute, { timeout: 10_000 })
})
test('close button (X) closes panel and returns to /', async ({ page }) => {
// Establish / in history first so close has somewhere to return to
await page.goto('/')
await waitForSimon(page)
await openInfoPanel(page, userType)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10_000 })
await page.getByRole('button', { name: 'Close panel' }).click()
await expect(dialog).not.toBeVisible({ timeout: 5_000 })
await expect(page).toHaveURL('/', { timeout: 10_000 })
})
})
}
})
// ── 11. Worker Earnings Page ──────────────────────────────────────────────────
test.describe('/workers/earnings interactions', () => {
test('"Back to Workers" link navigates to /workers', async ({ page }) => {
await page.goto('/workers/earnings')
await waitForPageContent(page)
await expect(page.locator('h1')).toBeVisible({ timeout: 15_000 })
const backLink = page.getByRole('link', { name: /back to workers/i })
await expect(backLink).toBeVisible({ timeout: 10_000 })
await backLink.click()
await expect(page).toHaveURL('/workers', { timeout: 10_000 })
await waitForPageContent(page)
await expect(page.locator('h1')).toContainText('For Workers')
})
test('content sections are all visible', async ({ page }) => {
await page.goto('/workers/earnings')
await waitForPageContent(page)
await expect(page.getByTestId('page-worker-earnings')).toBeVisible({ timeout: 15_000 })
// Hero title is visible
await expect(page.locator('h1')).toBeVisible()
// Content sections rendered — the page has multiple h4 headings in section cards
await expect(page.locator('h4').first()).toBeVisible({ timeout: 15_000 })
// The earnings info grid has cards
const infoCards = page.locator('[class*="EarningsInfoCard"], [class*="info-card"]')
// Fallback: check that meaningful content is rendered beyond the hero
const bodyText = await page.textContent('body')
expect(bodyText?.length).toBeGreaterThan(500)
})
})