import { test, expect } from '@playwright/test' test.describe('Events list page', () => { test('events list loads', async ({ page }) => { await page.goto('/events') await expect(page.locator('h1', { hasText: 'Events' })).toBeVisible() }) test('filter bar has type filters', async ({ page }) => { await page.goto('/events') const filterBar = page.locator('.filter-bar') await expect(filterBar).toBeVisible() for (const label of ['All', 'Workshops', 'Community', 'Social', 'Showcase']) { await expect(filterBar.locator('button', { hasText: label })).toBeVisible() } }) test('past events toggle exists and can be checked', async ({ page }) => { await page.goto('/events') await page.waitForLoadState('networkidle') const toggle = page.locator('.past-toggle') await expect(toggle).toBeVisible() await expect(toggle).toContainText('Show past events') await toggle.click() await expect(toggle).toHaveClass(/active/) // Page should still render without errors after toggling await expect(page.locator('h1', { hasText: 'Events' })).toBeVisible() }) test('clicking a filter button activates it', async ({ page }) => { await page.goto('/events') await page.waitForLoadState('networkidle') // Wait for Vue hydration — the "All" filter should have the active class once reactive const allBtn = page.locator('.filter-btn', { hasText: 'All' }) await expect(allBtn).toHaveClass(/active/, { timeout: 10000 }) const workshopsBtn = page.locator('.filter-bar button', { hasText: 'Workshops' }) await workshopsBtn.click() await expect(workshopsBtn).toHaveClass(/active/, { timeout: 5000 }) }) test('event links navigate to detail page', async ({ page }) => { await page.goto('/events') // Check the past events toggle so we see all events await page.locator('.past-toggle').click() const eventLinks = page.locator('.event-row a') const count = await eventLinks.count() if (count === 0) { // No events in the database — just verify the empty state renders await expect(page.locator('.empty', { hasText: 'No events found' })).toBeVisible() return } // Click the first event link and verify navigation const firstLink = eventLinks.first() const href = await firstLink.getAttribute('href') await firstLink.click() await page.waitForURL(/\/events\//) expect(page.url()).toContain('/events/') // Detail page should have an h1 with the event title await expect(page.locator('h1')).toBeVisible() }) }) async function navigateToFirstEventDetail(page) { await page.goto('/events') await page.locator('.past-toggle').click() await page.waitForLoadState('networkidle') const eventLinks = page.locator('.event-row a') const count = await eventLinks.count() if (count === 0) return null const href = await eventLinks.first().getAttribute('href') return href } test.describe('Event detail — ticket gating', () => { test('series-pass-required shows pass-required notice instead of buy button', async ({ page }) => { const href = await navigateToFirstEventDetail(page) test.skip(!href, 'No events in dev DB to navigate against') await page.route('**/api/events/*/tickets/available**', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ available: false, reason: 'series_pass_required', requiresSeriesPass: true, series: { id: 'series-stub', slug: 'series-stub', title: 'Stub Series' } }) }) }) await page.route('**/api/events/*/check-series-access**', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ requiresSeriesPass: false }) }) }) await page.locator(`.event-row a[href="${href}"]`).first().click() await page.waitForURL(`**${href}`) const ticketPanel = page.locator('.event-ticket-purchase') await expect(ticketPanel.locator('.ticket-status', { hasText: 'Series Pass Required' })).toBeVisible() await expect(ticketPanel.locator('button', { hasText: /Pay |Register for this event|Complete Registration/ })).toHaveCount(0) await expect(ticketPanel.locator('a[href="/series/series-stub"] button')).toBeVisible() }) test('memberSavings line is hidden for anonymous viewers', async ({ page }) => { const href = await navigateToFirstEventDetail(page) test.skip(!href, 'No events in dev DB to navigate against') await page.route('**/api/events/*/tickets/available**', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ available: true, alreadyRegistered: false, isFree: false, isMember: false, name: 'General Admission', formattedPrice: '$25.00', remaining: 10, memberSavings: 0, publicTicket: null }) }) }) await page.locator(`.event-row a[href="${href}"]`).first().click() await page.waitForURL(`**${href}`) const ticketCard = page.locator('.ticket-card') await expect(ticketCard).toBeVisible() await expect(page.locator('.ticket-savings')).toHaveCount(0) await expect(page.locator('text=/save .* as a member/i')).toHaveCount(0) }) test('memberSavings line is shown when API reports savings', async ({ page }) => { const href = await navigateToFirstEventDetail(page) test.skip(!href, 'No events in dev DB to navigate against') await page.route('**/api/events/*/tickets/available**', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ available: true, alreadyRegistered: false, isFree: false, isMember: true, name: 'Member Ticket', formattedPrice: '$10.00', remaining: 10, memberSavings: 15, publicTicket: { formattedPrice: '$25.00' } }) }) }) await page.locator(`.event-row a[href="${href}"]`).first().click() await page.waitForURL(`**${href}`) const savings = page.locator('.ticket-savings') await expect(savings).toBeVisible() await expect(savings).toContainText(/save/i) }) test.skip('hidden event returns 404', async () => { // Skipped: hidden-event gating happens during SSR useFetch in [slug].vue, // which page.route cannot intercept. Verifying this gate requires either // seeding a hidden event in the dev DB or a server-side mock layer. }) test.skip('past-deadline event shows registration-closed copy', async () => { // Skipped: when the available endpoint returns reason // "Registration deadline has passed", the current UI surfaces it as the // generic "Event Sold Out" panel — there is no distinct "Registration // closed" string to assert against without changing the component. }) test.skip('member with paid registration cannot self-cancel', async () => { // Skipped: requires seeding an authed member with a paid registration in // the DB, which is out of scope for API-level mocking. }) })