import { test, expect } from './helpers/fixtures.js' test.describe('Admin events list', () => { test('events list loads for admin', async ({ adminPage }) => { await adminPage.goto('/admin/events') await expect(adminPage.locator('h1')).toContainText('Events') }) test('create event button present', async ({ adminPage }) => { await adminPage.goto('/admin/events') await expect( adminPage.getByRole('link', { name: 'Create Event' }) ).toBeVisible() }) }) test.describe('Admin events create', () => { test('create event page loads', async ({ adminPage }) => { await adminPage.goto('/admin/events/create') await expect(adminPage.locator('h1')).toContainText('Create Event') }) test('create event form has required fields', async ({ adminPage }) => { await adminPage.goto('/admin/events/create') // Title input await expect( adminPage.getByPlaceholder('Enter a clear, descriptive event title') ).toBeVisible() // Description textarea await expect( adminPage.getByPlaceholder( 'Provide a clear description of what attendees can expect from this event' ) ).toBeVisible() // Event type select (USelect renders a button with the selected value) await expect(adminPage.getByText('Event Type')).toBeVisible() }) }) test.describe('Admin events access control', () => { test('non-admin redirect', async ({ page }) => { await page.goto('/admin/events') // Admin middleware redirects unauthenticated users to / await page.waitForURL((url) => !url.pathname.startsWith('/admin')) expect(page.url()).not.toContain('/admin/events') }) }) test.describe('Admin events CRUD', () => { test('create, edit, and delete an event', async ({ adminPage }) => { const suffix = Date.now().toString().slice(-6) const title = `e2e-event-${suffix}` const editedTitle = `e2e-event-${suffix}-edited` // Re-prime the auth cookie immediately before this multi-step flow. // The shared test-admin account's tokenVersion is bumped whenever // auth.spec.js's logout test runs in parallel, which would otherwise // surface mid-flow as "Session has been revoked" on the first POST. const loginRes = await adminPage.context().request.get('/api/dev/test-login', { maxRedirects: 0 }) if (loginRes.status() !== 302) { throw new Error(`Failed to refresh admin session: ${loginRes.status()}`) } // --- Create --- await adminPage.goto('/admin/events/create') await expect(adminPage.locator('h1')).toContainText('Create Event') // Ensure Vue has hydrated (initial $fetch for series/tags has resolved) // before interacting — under cross-file load, hydration can lag and a // pre-hydration submit will native-POST against an empty form. await adminPage.waitForLoadState('networkidle') await adminPage .getByPlaceholder('Enter a clear, descriptive event title') .fill(title) await adminPage .getByPlaceholder( 'Provide a clear description of what attendees can expect from this event' ) .fill('e2e test event description') await adminPage .getByPlaceholder('e.g., https://zoom.us/j/123... or #channel-name') .fill('https://example.com/zoom') const startInput = adminPage.getByPlaceholder( "e.g., 'tomorrow at 3pm', 'next Friday at 9am'" ) await startInput.fill('next Tuesday at 3pm') await startInput.blur() const endInput = adminPage.getByPlaceholder( "e.g., 'tomorrow at 5pm', 'next Friday at 11am'" ) await endInput.fill('next Tuesday at 5pm') await endInput.blur() await adminPage.getByRole('button', { name: 'Create Event' }).click() // The form posts via $fetch and then auto-redirects after a 1.5s setTimeout. // Under cross-file load that auto-redirect can race against waitForURL. // Wait for the surfaced success/error state, fail fast on error, then // navigate explicitly so subsequent assertions are deterministic. await expect( adminPage.locator('.success-box').or(adminPage.locator('.error-box')) ).toBeVisible({ timeout: 15000 }) await expect(adminPage.locator('.success-box')).toBeVisible() await adminPage.goto('/admin/events') await adminPage.waitForLoadState('networkidle') // Filter to just our event — orphan rows from prior failed runs can push // the new row off page 1 of the paginated list. await adminPage.getByPlaceholder('Search events...').fill(title) const row = adminPage.locator('tr', { hasText: title }) await expect(row).toBeVisible({ timeout: 10000 }) // --- Edit --- // Find the event ID from the row's "View" link (href is /events/), // and use the row's Edit button. Pair the click with waitForURL so we don't // miss the navigation event under load. await Promise.all([ adminPage.waitForURL(/\/admin\/events\/create\?edit=/, { timeout: 15000 }), row.getByRole('button', { name: 'Edit' }).click(), ]) await expect(adminPage.locator('h1')).toContainText('Edit Event') const titleInput = adminPage.getByPlaceholder( 'Enter a clear, descriptive event title' ) await titleInput.fill(editedTitle) await adminPage.getByRole('button', { name: 'Update Event' }).click() await expect( adminPage.locator('.success-box').or(adminPage.locator('.error-box')) ).toBeVisible({ timeout: 15000 }) await expect(adminPage.locator('.success-box')).toBeVisible() await adminPage.goto('/admin/events') await adminPage.waitForLoadState('networkidle') // Filter to the edited event's unique title for the same pagination reason. await adminPage.getByPlaceholder('Search events...').fill(editedTitle) const editedRow = adminPage.locator('tr', { hasText: editedTitle }) await expect(editedRow).toBeVisible({ timeout: 10000 }) // --- Delete (custom modal, not browser dialog) --- await editedRow.getByRole('button', { name: 'Del' }).click() await expect( adminPage.getByRole('heading', { name: 'Delete Event' }) ).toBeVisible() await adminPage .locator('.modal') .getByRole('button', { name: 'Delete' }) .click() await expect( adminPage.locator('tr', { hasText: editedTitle }) ).toHaveCount(0, { timeout: 10000 }) }) })