370 lines
13 KiB
TypeScript
370 lines
13 KiB
TypeScript
/**
|
|
* Route Render Tests — all 18 shipping routes
|
|
*
|
|
* Verifies each route renders its expected heading and key content,
|
|
* with styled-components applied (toBeVisible), no console errors,
|
|
* and no unhandled JavaScript exceptions.
|
|
*
|
|
* 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
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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)
|
|
|
|
// Prevent dev-mode AgeGateWrapper from clearing the bypass on mount
|
|
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)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Console error capture
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type ConsoleError = { url: string; message: string }
|
|
|
|
function captureConsoleErrors(page: Page): ConsoleError[] {
|
|
const errors: ConsoleError[] = []
|
|
|
|
page.on('pageerror', (err) => {
|
|
errors.push({ url: page.url(), message: err.message })
|
|
})
|
|
|
|
page.on('console', (msg) => {
|
|
if (msg.type() === 'error') {
|
|
errors.push({ url: page.url(), message: msg.text() })
|
|
}
|
|
})
|
|
|
|
return errors
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function waitForContent(page: Page): Promise<void> {
|
|
await page.waitForSelector('h1, [data-testid="simon-container"]', { timeout: 10_000 })
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await bypassAgeGate(page)
|
|
})
|
|
|
|
// ── Homepage ─────────────────────────────────────────────────────────────────
|
|
|
|
test.describe('Homepage', () => {
|
|
test('/ — renders Lilith brand, quadrant labels, footer join text', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/')
|
|
await page.waitForSelector('[data-testid="simon-container"]', { timeout: 10_000 })
|
|
|
|
// Brand heading
|
|
await expect(page.locator('h1')).toContainText('Lilith')
|
|
|
|
// Four quadrant labels from useUserTypes() — provider, client, creator, fan
|
|
// Labels are rendered as .quadrant-label inside each quadrant
|
|
const quadrantLabels = page.locator('.quadrant-label')
|
|
await expect(quadrantLabels).toHaveCount(4)
|
|
|
|
// Footer join text (from common.json footerText)
|
|
await expect(page.locator('.simon-footer')).toBeVisible()
|
|
await expect(page.locator('.simon-footer')).toContainText(/join us/i)
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
// ── Workers ──────────────────────────────────────────────────────────────────
|
|
|
|
test.describe('Workers', () => {
|
|
test('/workers — shows For Workers heading and four worker type cards', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/workers')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('For Workers')
|
|
|
|
// Four card titles from landing-categories.json (scoped to main to avoid nav matches)
|
|
const main = page.locator('main')
|
|
await expect(main.getByRole('heading', { name: 'Providers' })).toBeVisible()
|
|
await expect(main.getByRole('heading', { name: 'Performers' })).toBeVisible()
|
|
await expect(main.getByRole('heading', { name: 'Fangirls' })).toBeVisible()
|
|
await expect(main.getByRole('heading', { name: 'Camgirls' })).toBeVisible()
|
|
|
|
// CTA button text
|
|
await expect(page.getByText('Get Started')).toBeVisible()
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/workers/escort — shows For Providers heading with benefits and platform features', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/workers/escort')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('For Providers')
|
|
|
|
// Section titles from work-provider.json sectionTitles
|
|
await expect(page.getByText('Why Join as a Provider')).toBeVisible()
|
|
await expect(page.getByText('Platform Features')).toBeVisible()
|
|
await expect(page.getByText('Frequently Asked Questions')).toBeVisible()
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/workers/performer — shows For Performers heading', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/workers/performer')
|
|
await expect(page.locator('h1')).toContainText('For Performers', { timeout: 15_000 })
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/workers/fangirl — shows For Fangirls heading', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/workers/fangirl')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('For Fangirls')
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/workers/camgirl — shows For Camgirls heading', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/workers/camgirl')
|
|
await expect(page.locator('h1')).toContainText('For Cam', { timeout: 15_000 })
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/workers/earnings — shows earnings heading with Earn content', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/workers/earnings')
|
|
await waitForContent(page)
|
|
|
|
// Hero title: "You Earn It. You Keep It." (from worker-earnings.json)
|
|
const h1 = page.locator('h1')
|
|
await expect(h1).toBeVisible()
|
|
await expect(h1).toContainText(/earn/i)
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
// ── Customers ─────────────────────────────────────────────────────────────────
|
|
|
|
test.describe('Customers', () => {
|
|
test('/customers — shows For Customers heading with Clients and Fans cards', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/customers')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('For Customers')
|
|
|
|
const main = page.locator('main')
|
|
await expect(main.getByRole('heading', { name: 'Clients' })).toBeVisible()
|
|
await expect(main.getByRole('heading', { name: 'Fans' })).toBeVisible()
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/customers/client — shows For Clients heading', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/customers/client')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('For Clients')
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/customers/fan — shows For Fans heading', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/customers/fan')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('For Fans')
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
// ── Pricing ───────────────────────────────────────────────────────────────────
|
|
|
|
test.describe('Pricing', () => {
|
|
test('/pricing/client — shows Client Subscription Tiers with Bronze, Silver, Gold, Platinum', async ({ page }) => {
|
|
// Pricing pages may fetch from merchant API which can timeout — allow 404 console errors
|
|
await page.goto('/pricing/client')
|
|
|
|
// Wait for tier cards to appear (fallback tiers load after API timeout ~5s)
|
|
await expect(page.locator('h1')).toContainText('Client Subscription Tiers', { timeout: 15_000 })
|
|
|
|
await expect(page.getByText('Bronze')).toBeVisible({ timeout: 15_000 })
|
|
await expect(page.getByText('Silver')).toBeVisible()
|
|
await expect(page.getByText('Gold')).toBeVisible()
|
|
await expect(page.getByText('Platinum')).toBeVisible()
|
|
})
|
|
|
|
test('/pricing/fan — shows tier cards', async ({ page }) => {
|
|
await page.goto('/pricing/fan')
|
|
|
|
// Wait for tier cards to appear (fallback tiers load after API timeout)
|
|
await expect(page.getByText('Bronze')).toBeVisible({ timeout: 25_000 })
|
|
await expect(page.getByText('Platinum')).toBeVisible()
|
|
})
|
|
})
|
|
|
|
// ── Company ───────────────────────────────────────────────────────────────────
|
|
|
|
test.describe('Company', () => {
|
|
test('/company — shows Company heading with Terms and Privacy cards', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/company')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('Company')
|
|
|
|
const main = page.locator('main')
|
|
await expect(main.getByRole('heading', { name: 'Terms of Service' })).toBeVisible()
|
|
await expect(main.getByRole('heading', { name: 'Privacy Policy' })).toBeVisible()
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/company/terms — shows Terms of Service heading with legal content', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/company/terms')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('Terms of Service')
|
|
|
|
// Should have legal body content (sections)
|
|
const bodyText = await page.textContent('body')
|
|
expect(bodyText?.length).toBeGreaterThan(200)
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/company/privacy — shows Privacy Policy heading', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/company/privacy')
|
|
await waitForContent(page)
|
|
|
|
await expect(page.locator('h1')).toContainText('Privacy Policy')
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
// ── CTA Modals ────────────────────────────────────────────────────────────────
|
|
|
|
test.describe('CTA Modals', () => {
|
|
test('/register — shows modal dialog with user type selection', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/register')
|
|
await page.waitForLoadState('domcontentloaded')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Modal dialog appears with "Join Lilith" heading
|
|
const modal = page.getByRole('dialog')
|
|
await expect(modal).toBeVisible({ timeout: 15_000 })
|
|
await expect(modal).toContainText(/join lilith/i)
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
test('/newsletter — shows newsletter modal dialog', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/newsletter')
|
|
await page.waitForLoadState('domcontentloaded')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const modal = page.locator('[data-context="newsletter"]')
|
|
await expect(modal).toBeVisible({ timeout: 15_000 })
|
|
await expect(modal).toHaveAttribute('role', 'dialog')
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
// ── 404 ───────────────────────────────────────────────────────────────────────
|
|
|
|
test.describe('404', () => {
|
|
test('/shop — shows 404 Not Found with suggestions', async ({ page }) => {
|
|
const errors = captureConsoleErrors(page)
|
|
|
|
await page.goto('/shop')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const bodyText = await page.textContent('body')
|
|
expect(bodyText).toBeTruthy()
|
|
|
|
const lower = bodyText!.toLowerCase()
|
|
const has404Content =
|
|
lower.includes('404') ||
|
|
lower.includes('not found') ||
|
|
lower.includes("doesn't exist") ||
|
|
lower.includes('page not found')
|
|
|
|
expect(has404Content).toBe(true)
|
|
|
|
// NotFoundPage renders a "Back to Home" link
|
|
await expect(page.getByRole('link', { name: 'Back to Home' })).toBeVisible()
|
|
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
})
|