import { test, expect } from '@playwright/test'; import { TEST_ORG_SLUG, navigateTo, waitForHydration } from './helpers'; // Unique suffix for this test run - keeps data isolated const RUN = Date.now(); const EVENT_NAME = `PW Test Event ${RUN}`; const EVENT_DESC = `Playwright test event created at ${RUN}`; const EVENT_VENUE = 'PW Test Venue'; // Shared state across serial tests - set by creation test, used by all others let eventSlug = ''; /** Navigate to the test event's overview page */ async function gotoEvent(page: import('@playwright/test').Page) { await navigateTo(page, `/${TEST_ORG_SLUG}/events/${eventSlug}`); } /** Navigate to the test event via a specific sub-path */ async function gotoEventPath(page: import('@playwright/test').Page, subPath: string) { await navigateTo(page, `/${TEST_ORG_SLUG}/events/${eventSlug}/${subPath}`); } // ─── Event List Page ───────────────────────────────────────────────────────── test.describe('Events List Page', () => { test.beforeEach(async ({ page }) => { await navigateTo(page, `/${TEST_ORG_SLUG}/events`); }); test('should load events page with status tabs and New Event button', async ({ page }) => { // Status filter tabs await expect(page.getByRole('button', { name: /All/i })).toBeVisible(); await expect(page.getByRole('button', { name: /Planning/i })).toBeVisible(); await expect(page.getByRole('button', { name: /Active/i })).toBeVisible(); await expect(page.getByRole('button', { name: /Completed/i })).toBeVisible(); await expect(page.getByRole('button', { name: /Archived/i })).toBeVisible(); // New Event button await expect(page.getByRole('button', { name: /New Event/i })).toBeVisible(); }); test('should open Create Event modal with all form fields', async ({ page }) => { await page.getByRole('button', { name: /New Event/i }).click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 3000 }); await expect(modal.getByRole('heading', { name: /Create Event/i })).toBeVisible(); // Form fields await expect(modal.locator('#event-name')).toBeVisible(); await expect(modal.locator('#event-desc')).toBeVisible(); await expect(modal.locator('#event-start')).toBeVisible(); await expect(modal.locator('#event-end')).toBeVisible(); await expect(modal.locator('#event-venue')).toBeVisible(); // Color swatches const colorSwatches = modal.locator('button.rounded-full'); const count = await colorSwatches.count(); expect(count).toBeGreaterThanOrEqual(8); // Action buttons await expect(modal.getByRole('button', { name: /Cancel/i })).toBeVisible(); await expect(modal.locator('button[type="submit"]')).toBeVisible(); }); test('should close Create Event modal on Cancel', async ({ page }) => { await page.getByRole('button', { name: /New Event/i }).click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 3000 }); await modal.getByRole('button', { name: /Cancel/i }).click(); await expect(modal).not.toBeVisible({ timeout: 3000 }); }); test('should close Create Event modal by clicking backdrop', async ({ page }) => { await page.getByRole('button', { name: /New Event/i }).click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 3000 }); // Click the backdrop (the outer dialog div itself, not the inner card) await modal.click({ position: { x: 10, y: 10 } }); await expect(modal).not.toBeVisible({ timeout: 5000 }); }); test('should disable Create button when name is empty', async ({ page }) => { await page.getByRole('button', { name: /New Event/i }).click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 3000 }); const submitBtn = modal.locator('button[type="submit"]'); await expect(submitBtn).toBeDisabled(); }); test('should switch between status filter tabs', async ({ page }) => { // Click Planning tab await page.getByRole('button', { name: /Planning/i }).click(); await page.waitForURL(/status=planning/, { timeout: 5000 }); // Click Active tab await page.getByRole('button', { name: /Active/i }).click(); await page.waitForURL(/status=active/, { timeout: 5000 }); // Click All tab - should remove status param await page.getByRole('button', { name: /All/i }).first().click(); await page.waitForURL((url) => !url.searchParams.has('status'), { timeout: 5000 }); }); }); // ─── Event Creation + Full Event Flow (serial) ────────────────────────────── // All tests below run in order. The first test creates the event and captures // its slug; every subsequent test navigates directly via that slug. test.describe.serial('Event Lifecycle', () => { let deptId = ''; const DEPT_NAME = `PW Dept ${RUN}`; // ════════════════════════════════════════════════════════════════════ // 1. EVENT CREATION // ════════════════════════════════════════════════════════════════════ test('should create an event with all fields and navigate to it', async ({ page }) => { test.setTimeout(60000); await navigateTo(page, `/${TEST_ORG_SLUG}/events`); await page.getByRole('button', { name: /New Event/i }).click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 3000 }); await modal.locator('#event-name').fill(EVENT_NAME); await modal.locator('#event-desc').fill(EVENT_DESC); await modal.locator('#event-venue').fill(EVENT_VENUE); const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const dayAfter = new Date(); dayAfter.setDate(dayAfter.getDate() + 2); const fmtDate = (d: Date) => d.toISOString().split('T')[0]; await modal.locator('#event-start').fill(fmtDate(tomorrow)); await modal.locator('#event-end').fill(fmtDate(dayAfter)); await modal.locator('button.rounded-full').nth(1).click(); const submitBtn = modal.locator('button[type="submit"]'); await expect(submitBtn).toBeEnabled({ timeout: 3000 }); await submitBtn.click(); await page.waitForURL((url) => { const path = url.pathname; return path.includes(`/${TEST_ORG_SLUG}/events/`) && !path.endsWith('/events') && !path.endsWith('/events/'); }, { timeout: 15000 }); await waitForHydration(page); const segments = new URL(page.url()).pathname.split('/'); eventSlug = segments[segments.indexOf('events') + 1]; expect(eventSlug).toBeTruthy(); await expect(page.locator('header').getByRole('heading', { name: EVENT_NAME })).toBeVisible({ timeout: 10000 }); }); // ════════════════════════════════════════════════════════════════════ // 2. EVENT OVERVIEW - read & verify // ════════════════════════════════════════════════════════════════════ test('should display event header with name, status, date, and venue', async ({ page }) => { await gotoEvent(page); await expect(page.locator('header').getByRole('heading', { name: EVENT_NAME })).toBeVisible(); await expect(page.getByText('planning').first()).toBeVisible(); await expect(page.getByText(EVENT_VENUE).first()).toBeVisible(); }); test('should display Event Details section with Team and Departments labels', async ({ page }) => { await gotoEvent(page); await expect(page.getByText('Event Details')).toBeVisible(); await expect(page.getByText('Start Date')).toBeVisible(); await expect(page.getByText('End Date')).toBeVisible(); const detailsCard = page.locator('.bg-dark\\/30').filter({ hasText: 'Event Details' }).first(); await expect(detailsCard.getByText('Team', { exact: true })).toBeVisible(); await expect(detailsCard.getByText('Departments', { exact: true })).toBeVisible(); }); test('should show sidebar with Overview, Finances, and Team tabs', async ({ page }) => { await gotoEvent(page); const sidebar = page.locator('aside'); await expect(sidebar.getByText('Overview')).toBeVisible(); await expect(sidebar.getByText('Finances')).toBeVisible(); await expect(sidebar.getByRole('link', { name: /Team/i })).toBeVisible(); }); test('should show edit and delete buttons for editors', async ({ page }) => { await gotoEvent(page); await expect(page.locator('button[title="Edit"]')).toBeVisible(); await expect(page.locator('button[title="Delete"]')).toBeVisible(); }); // ════════════════════════════════════════════════════════════════════ // 3. EVENT EDITING - change status, verify persistence // ════════════════════════════════════════════════════════════════════ test('should enter edit mode and see all editable fields', async ({ page }) => { await gotoEvent(page); await page.locator('button[title="Edit"]').click(); await expect(page.getByRole('button', { name: /Cancel/i })).toBeVisible({ timeout: 3000 }); await expect(page.getByRole('button', { name: /Save/i })).toBeVisible(); // Status dropdown await expect(page.locator('select')).toBeVisible(); // Editable name input await expect(page.locator('input[type="text"]').first()).toBeVisible(); }); test('should change event status to active and save', async ({ page }) => { test.setTimeout(60000); await gotoEvent(page); await page.locator('button[title="Edit"]').click(); await expect(page.locator('select')).toBeVisible({ timeout: 3000 }); await page.locator('select').selectOption('active'); await page.getByRole('button', { name: /Save/i }).click(); // After save, should exit edit mode and show updated status await expect(page.locator('button[title="Edit"]')).toBeVisible({ timeout: 5000 }); await expect(page.getByText('active').first()).toBeVisible({ timeout: 5000 }); }); test('should persist status change after page reload', async ({ page }) => { await gotoEvent(page); await expect(page.getByText('active').first()).toBeVisible({ timeout: 5000 }); }); test('should change status back to planning', async ({ page }) => { test.setTimeout(60000); await gotoEvent(page); await page.locator('button[title="Edit"]').click(); await expect(page.locator('select')).toBeVisible({ timeout: 3000 }); await page.locator('select').selectOption('planning'); await page.getByRole('button', { name: /Save/i }).click(); await expect(page.locator('button[title="Edit"]')).toBeVisible({ timeout: 5000 }); await expect(page.getByText('planning').first()).toBeVisible({ timeout: 5000 }); }); test('should cancel edit mode without saving changes', async ({ page }) => { await gotoEvent(page); await page.locator('button[title="Edit"]').click(); await page.locator('select').selectOption('archived'); await page.getByRole('button', { name: /Cancel/i }).click(); // Should still show planning, not archived await expect(page.getByText('planning').first()).toBeVisible(); await expect(page.locator('header').getByRole('heading', { name: EVENT_NAME })).toBeVisible(); }); // ════════════════════════════════════════════════════════════════════ // 4. SIDEBAR NAVIGATION // ════════════════════════════════════════════════════════════════════ test('should navigate to Finances tab via sidebar', async ({ page }) => { await gotoEvent(page); await page.locator('aside').getByText('Finances').click(); await page.waitForURL(/\/finances/, { timeout: 10000 }); await waitForHydration(page); await expect(page.getByRole('heading', { name: 'Finances' })).toBeVisible({ timeout: 5000 }); }); test('should navigate to Team tab via sidebar', async ({ page }) => { await gotoEvent(page); await page.locator('aside').getByRole('link', { name: /Team/i }).click(); await page.waitForURL(/\/team/, { timeout: 10000 }); await waitForHydration(page); }); test('should navigate back to all events via back link', async ({ page }) => { await gotoEvent(page); await page.locator('aside').getByText(/All Events/i).click(); await page.waitForURL(new RegExp(`/${TEST_ORG_SLUG}/events$`), { timeout: 10000 }); }); // ════════════════════════════════════════════════════════════════════ // 5. FINANCES PAGE - empty state // ════════════════════════════════════════════════════════════════════ test('should load Finances page with summary cards and empty state', async ({ page }) => { await gotoEventPath(page, 'finances'); await expect(page.getByRole('heading', { name: 'Event Finances' })).toBeVisible({ timeout: 10000 }); await expect(page.getByText('Total Planned')).toBeVisible(); await expect(page.getByText('Total Actual Costs')).toBeVisible(); await expect(page.getByText('Modified Budget')).toBeVisible(); await expect(page.getByText('Budget Remainder')).toBeVisible(); await expect(page.getByText('No departments yet')).toBeVisible(); }); test('should show view mode and type filter buttons on Finances', async ({ page }) => { await gotoEventPath(page, 'finances'); await expect(page.getByRole('heading', { name: 'Event Finances' })).toBeVisible({ timeout: 10000 }); await expect(page.getByRole('button', { name: 'Overview' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Details' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Sponsors' })).toBeVisible(); }); // ════════════════════════════════════════════════════════════════════ // 6. TEAM PAGE - department + role + member CRUD // ════════════════════════════════════════════════════════════════════ test('should load team page with action buttons', async ({ page }) => { await gotoEventPath(page, 'team'); await expect(page.getByRole('button', { name: /Add Department/i })).toBeVisible({ timeout: 5000 }); await expect(page.getByRole('button', { name: /Add Role/i })).toBeVisible({ timeout: 5000 }); await expect(page.getByRole('button', { name: /Add Member/i })).toBeVisible({ timeout: 5000 }); }); test('should create a department', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'team'); await page.getByRole('button', { name: /Add Department/i }).click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 3000 }); await modal.locator('#dept-name').fill(DEPT_NAME); await modal.getByRole('button', { name: /Save/i }).click(); await expect(modal).not.toBeVisible({ timeout: 10000 }); await expect(page.getByText(DEPT_NAME).first()).toBeVisible({ timeout: 5000 }); }); test('should show department in sidebar and capture its ID', async ({ page }) => { await gotoEventPath(page, 'team'); const sidebar = page.locator('aside'); await expect(sidebar.getByText(DEPT_NAME).first()).toBeVisible({ timeout: 5000 }); await sidebar.getByRole('link', { name: new RegExp(DEPT_NAME) }).click(); await page.waitForURL(/\/dept\//, { timeout: 10000 }); const match = page.url().match(/\/dept\/([^/]+)/); if (match) deptId = match[1]; expect(deptId).toBeTruthy(); }); test('should add a member to the event', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'team'); await page.getByRole('button', { name: /Add Member/i }).click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 3000 }); // Select the first available org member const memberSelect = modal.locator('#add-member-select'); await expect(memberSelect).toBeVisible(); const options = memberSelect.locator('option:not([disabled])'); const optCount = await options.count(); if (optCount > 0) { // Pick the first non-disabled option const firstVal = await options.first().getAttribute('value'); if (firstVal) { await memberSelect.selectOption(firstVal); } } // Assign to our department via chip toggle const deptChip = modal.getByText(DEPT_NAME).first(); if (await deptChip.isVisible()) { await deptChip.click(); } // Submit await modal.getByRole('button', { name: /Add Member/i }).click(); await expect(modal).not.toBeVisible({ timeout: 10000 }); // Member count should have increased - at least 1 member row visible await expect(page.locator('.group').first()).toBeVisible({ timeout: 5000 }); }); test('should show the added member in the team list', async ({ page }) => { await gotoEventPath(page, 'team'); // At least one member avatar/row should be visible const memberRows = page.locator('[class*="group"]').filter({ has: page.locator('.rounded-full') }); const count = await memberRows.count(); expect(count).toBeGreaterThan(0); }); // ════════════════════════════════════════════════════════════════════ // 7. DEPARTMENT DASHBOARD - module management // ════════════════════════════════════════════════════════════════════ test('should show layout picker and Add Module button', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const layoutPicker = page.locator('.bg-dark\\/50.rounded-lg'); await expect(layoutPicker.first()).toBeVisible({ timeout: 5000 }); await expect(page.getByRole('button', { name: /Add Module/i })).toBeVisible(); }); test('should add a budget module', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const addModuleBtn = page.getByRole('button', { name: /Add Module/i }); await addModuleBtn.scrollIntoViewIfNeeded(); await addModuleBtn.click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 5000 }); await modal.getByText('Budget').first().click(); await expect(modal).not.toBeVisible({ timeout: 5000 }); await expect(page.getByText('Income').first()).toBeVisible({ timeout: 5000 }); }); test('should add a notes module', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const addModuleBtn = page.getByRole('button', { name: /Add Module/i }); await addModuleBtn.scrollIntoViewIfNeeded(); await addModuleBtn.click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 5000 }); await modal.getByText('Notes').first().click(); await expect(modal).not.toBeVisible({ timeout: 5000 }); await expect(page.getByText('Notes').first()).toBeVisible({ timeout: 5000 }); }); test('should add a schedule module', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const addModuleBtn = page.getByRole('button', { name: /Add Module/i }); await addModuleBtn.scrollIntoViewIfNeeded(); await addModuleBtn.click(); const modal = page.locator('[role="dialog"]'); await expect(modal).toBeVisible({ timeout: 5000 }); await modal.getByText('Schedule').first().click(); await expect(modal).not.toBeVisible({ timeout: 5000 }); await expect(page.getByText('Schedule').first()).toBeVisible({ timeout: 5000 }); }); // ════════════════════════════════════════════════════════════════════ // 8. LAYOUT SWITCHING // ════════════════════════════════════════════════════════════════════ test('should switch between all 4 layout presets', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const layoutPicker = page.locator('.bg-dark\\/50.rounded-lg').first(); const layoutButtons = layoutPicker.locator('button'); const count = await layoutButtons.count(); expect(count).toBe(4); for (let i = 0; i < count; i++) { await layoutButtons.nth(i).click(); await expect(layoutButtons.nth(i)).toHaveClass(/bg-primary/, { timeout: 3000 }); } }); test('should expand and collapse a module', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const expandBtn = page.locator('button[title="Expand"]').first(); await expect(expandBtn).toBeVisible({ timeout: 5000 }); await expandBtn.click(); const collapseBtn = page.locator('button[title="Collapse"]'); await expect(collapseBtn).toBeVisible({ timeout: 3000 }); // Layout picker should be hidden when expanded await expect(page.locator('.bg-dark\\/50.rounded-lg').first()).not.toBeVisible(); await collapseBtn.click(); await expect(page.locator('button[title="Expand"]').first()).toBeVisible({ timeout: 3000 }); }); // ════════════════════════════════════════════════════════════════════ // 9. CHECKLIST WIDGET - full CRUD // ════════════════════════════════════════════════════════════════════ test('should show default General checklist', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); await expect(page.getByText('General').first()).toBeVisible({ timeout: 5000 }); }); test('should add a checklist item and see it appear', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const addInput = page.locator('input[placeholder="Add item..."]').first(); await addInput.scrollIntoViewIfNeeded(); await expect(addInput).toBeVisible({ timeout: 5000 }); await addInput.fill(`PW Task A ${RUN}`); await addInput.press('Enter'); await expect(page.getByText(`PW Task A ${RUN}`).first()).toBeVisible({ timeout: 5000 }); }); test('should add a second checklist item', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const addInput = page.locator('input[placeholder="Add item..."]').first(); await addInput.scrollIntoViewIfNeeded(); await expect(addInput).toBeVisible({ timeout: 5000 }); await addInput.fill(`PW Task B ${RUN}`); await addInput.press('Enter'); await expect(page.getByText(`PW Task B ${RUN}`).first()).toBeVisible({ timeout: 5000 }); }); test('should check a checklist item and see progress update', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); // Scroll to checklist item (may be off-screen in grid layout with many panels) const taskA = page.getByText(`PW Task A ${RUN}`).first(); await taskA.scrollIntoViewIfNeeded(); await expect(taskA).toBeVisible({ timeout: 5000 }); // The checkbox is in the same row as the task text const checklistRow = taskA.locator('..'); const checkbox = checklistRow.locator('input[type="checkbox"]'); await checkbox.check(); // Progress counter should show 1/2 await expect(page.getByText('1/2').first()).toBeVisible({ timeout: 5000 }); }); test('should uncheck the checklist item and see progress revert', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const taskA = page.getByText(`PW Task A ${RUN}`).first(); await taskA.scrollIntoViewIfNeeded(); await expect(taskA).toBeVisible({ timeout: 5000 }); const checklistRow = taskA.locator('..'); const checkbox = checklistRow.locator('input[type="checkbox"]'); if (await checkbox.isChecked()) { await checkbox.uncheck(); } await expect(page.getByText('0/2').first()).toBeVisible({ timeout: 5000 }); }); // ════════════════════════════════════════════════════════════════════ // 10. BUDGET WIDGET - full CRUD // ════════════════════════════════════════════════════════════════════ test('should show budget summary cards with zero values', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); await expect(page.getByText('Income').first()).toBeVisible({ timeout: 5000 }); await expect(page.getByText('Expenses').first()).toBeVisible({ timeout: 5000 }); // Initially €0.00 await expect(page.getByText('€0.00').first()).toBeVisible({ timeout: 5000 }); }); test('should add a budget expense item and see it in the list', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); await page.getByRole('button', { name: /Add Item/i }).first().click(); const modal = page.locator('.fixed.inset-0.z-\\[60\\]'); await expect(modal).toBeVisible({ timeout: 3000 }); await modal.locator('#budget-desc').fill(`PW Venue Rental ${RUN}`); await modal.locator('#budget-planned').fill('200'); await modal.locator('#budget-actual').fill('180'); await modal.getByRole('button', { name: /Add Item/i }).click(); await expect(modal).not.toBeVisible({ timeout: 5000 }); await expect(page.getByText(`PW Venue Rental ${RUN}`).first()).toBeVisible({ timeout: 5000 }); }); test('should show updated expense total after adding item', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); // Expense card should show €180.00 actual await expect(page.getByText('€180.00').first()).toBeVisible({ timeout: 5000 }); }); test('should show receipt warning for expense with actual amount but no receipt', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const warningIcon = page.locator('span.text-amber-400').filter({ hasText: 'warning' }); await expect(warningIcon.first()).toBeVisible({ timeout: 5000 }); }); test('should add a second expense item', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); await page.getByRole('button', { name: /Add Item/i }).first().click(); const modal = page.locator('.fixed.inset-0.z-\\[60\\]'); await expect(modal).toBeVisible({ timeout: 3000 }); await modal.locator('#budget-desc').fill(`PW Catering ${RUN}`); await modal.locator('#budget-planned').fill('300'); await modal.locator('#budget-actual').fill('0'); await modal.getByRole('button', { name: /Add Item/i }).click(); await expect(modal).not.toBeVisible({ timeout: 5000 }); await expect(page.getByText(`PW Catering ${RUN}`).first()).toBeVisible({ timeout: 5000 }); }); test('should add an income item', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); await page.getByRole('button', { name: /Add Item/i }).first().click(); const modal = page.locator('.fixed.inset-0.z-\\[60\\]'); await expect(modal).toBeVisible({ timeout: 3000 }); await modal.locator('#budget-desc').fill(`PW Ticket Sales ${RUN}`); await modal.locator('#budget-type').selectOption('income'); await modal.locator('#budget-planned').fill('1000'); await modal.locator('#budget-actual').fill('750'); await modal.getByRole('button', { name: /Add Item/i }).click(); await expect(modal).not.toBeVisible({ timeout: 5000 }); await expect(page.getByText(`PW Ticket Sales ${RUN}`).first()).toBeVisible({ timeout: 5000 }); }); test('should edit a budget item description and amount', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); // Click the venue rental item to open edit modal await page.getByText(`PW Venue Rental ${RUN}`).first().click(); const modal = page.locator('.fixed.inset-0.z-\\[60\\]'); await expect(modal).toBeVisible({ timeout: 3000 }); // Should show "Edit" in the title await expect(modal.getByText('Edit Budget Item')).toBeVisible(); // Update the actual amount await modal.locator('#budget-actual').fill('195'); await modal.getByRole('button', { name: /Save/i }).click(); await expect(modal).not.toBeVisible({ timeout: 5000 }); }); test('should filter budget items by Income tab', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); // Click Income filter await page.getByRole('button', { name: 'Income' }).first().click(); await expect(page.getByText(`PW Ticket Sales ${RUN}`).first()).toBeVisible({ timeout: 3000 }); // Expense items should not be visible await expect(page.getByText(`PW Venue Rental ${RUN}`)).not.toBeVisible({ timeout: 2000 }); }); test('should filter budget items by Expenses tab', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); await page.getByRole('button', { name: 'Expenses' }).first().click(); await expect(page.getByText(`PW Venue Rental ${RUN}`).first()).toBeVisible({ timeout: 3000 }); await expect(page.getByText(`PW Catering ${RUN}`).first()).toBeVisible({ timeout: 3000 }); // Income item should not be visible await expect(page.getByText(`PW Ticket Sales ${RUN}`)).not.toBeVisible({ timeout: 2000 }); }); test('should show All items when switching back to All tab', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); await page.getByRole('button', { name: 'All' }).first().click(); await expect(page.getByText(`PW Venue Rental ${RUN}`).first()).toBeVisible({ timeout: 3000 }); await expect(page.getByText(`PW Ticket Sales ${RUN}`).first()).toBeVisible({ timeout: 3000 }); }); // ════════════════════════════════════════════════════════════════════ // 11. NOTES WIDGET - full CRUD // ════════════════════════════════════════════════════════════════════ test('should show default note in Notes widget', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); // Notes widget should have a default note and a textarea await expect(page.locator('textarea[placeholder="Start writing..."]').first()).toBeVisible({ timeout: 5000 }); }); test('should type content into the default note', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const textarea = page.locator('textarea[placeholder="Start writing..."]').first(); await expect(textarea).toBeVisible({ timeout: 5000 }); await textarea.fill(`Meeting notes for ${DEPT_NAME} - ${RUN}`); // Wait for auto-save debounce (500ms) await page.waitForTimeout(1000); }); test('should persist note content after page reload', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const textarea = page.locator('textarea[placeholder="Start writing..."]').first(); await expect(textarea).toBeVisible({ timeout: 5000 }); await expect(textarea).toHaveValue(new RegExp(`Meeting notes for ${DEPT_NAME}`), { timeout: 5000 }); }); test('should create a second note via New note button', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); // Click "New note" button in the notes sidebar const newNoteBtn = page.getByText('New note').first(); await expect(newNoteBtn).toBeVisible({ timeout: 5000 }); await newNoteBtn.click(); // Title input should appear const titleInput = page.locator('input[placeholder="Note title..."]').first(); await expect(titleInput).toBeVisible({ timeout: 3000 }); await titleInput.fill(`PW Note ${RUN}`); await titleInput.press('Enter'); // New note should appear in the sidebar list await expect(page.getByText(`PW Note ${RUN}`).first()).toBeVisible({ timeout: 5000 }); }); test('should switch between notes and see different content', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); // Click the new note await page.getByText(`PW Note ${RUN}`).first().click(); const textarea = page.locator('textarea[placeholder="Start writing..."]').first(); // New note should be empty await expect(textarea).toHaveValue('', { timeout: 3000 }); // Type something in the new note await textarea.fill(`Second note content ${RUN}`); await page.waitForTimeout(1000); }); // ════════════════════════════════════════════════════════════════════ // 12. MODULE REORDERING // ════════════════════════════════════════════════════════════════════ test('should have move left/right buttons on panels', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const moveLeftBtns = page.locator('button[title="Move left"]'); const moveRightBtns = page.locator('button[title="Move right"]'); const leftCount = await moveLeftBtns.count(); const rightCount = await moveRightBtns.count(); expect(leftCount).toBeGreaterThan(0); expect(rightCount).toBeGreaterThan(0); await expect(moveLeftBtns.first()).toBeDisabled(); await expect(moveRightBtns.last()).toBeDisabled(); }); // ════════════════════════════════════════════════════════════════════ // 13. MODULE REMOVAL // ════════════════════════════════════════════════════════════════════ test('should remove a module and verify panel count decreases', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); const expandBtns = page.locator('button[title="Expand"]'); const panelsBefore = await expandBtns.count(); expect(panelsBefore).toBeGreaterThan(0); await page.locator('button[title="Remove module"]').last().click(); await expect(expandBtns).toHaveCount(panelsBefore - 1, { timeout: 10000 }); }); test('should verify remaining modules are still functional after removal', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, `dept/${deptId}`); // Budget items should still be visible await expect(page.getByText(`PW Venue Rental ${RUN}`).first()).toBeVisible({ timeout: 5000 }); // Checklist items should still be visible await expect(page.getByText(`PW Task A ${RUN}`).first()).toBeVisible({ timeout: 5000 }); }); // ════════════════════════════════════════════════════════════════════ // 14. FINANCES PAGE - with budget data // ════════════════════════════════════════════════════════════════════ test('should show budget items from department on Finances page', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'finances'); // Should no longer show empty state await expect(page.getByText('No budget data yet')).not.toBeVisible({ timeout: 5000 }); // Should show our budget items await expect(page.getByText(`PW Venue Rental ${RUN}`).first()).toBeVisible({ timeout: 5000 }); await expect(page.getByText(`PW Ticket Sales ${RUN}`).first()).toBeVisible({ timeout: 5000 }); }); test('should show non-zero summary totals on Finances page', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'finances'); // Total Income should show €750.00 (ticket sales actual) await expect(page.getByText('€750.00').first()).toBeVisible({ timeout: 5000 }); // Total Expenses should show €195.00 (venue rental edited actual) await expect(page.getByText('€195.00').first()).toBeVisible({ timeout: 5000 }); }); test('should filter Finances by Income and only show income items', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'finances'); await page.getByRole('button', { name: 'Income' }).click(); await page.waitForTimeout(500); await expect(page.getByText(`PW Ticket Sales ${RUN}`).first()).toBeVisible({ timeout: 3000 }); // Expense items should be hidden await expect(page.getByText(`PW Venue Rental ${RUN}`)).not.toBeVisible({ timeout: 2000 }); }); test('should filter Finances by Expenses and only show expense items', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'finances'); await page.getByRole('button', { name: 'Expenses' }).click(); await page.waitForTimeout(500); await expect(page.getByText(`PW Venue Rental ${RUN}`).first()).toBeVisible({ timeout: 3000 }); await expect(page.getByText(`PW Catering ${RUN}`).first()).toBeVisible({ timeout: 3000 }); await expect(page.getByText(`PW Ticket Sales ${RUN}`)).not.toBeVisible({ timeout: 2000 }); }); test('should switch to By Department view and see department group', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'finances'); await page.getByRole('button', { name: 'All' }).first().click(); await page.getByRole('button', { name: 'By Department' }).click(); await page.waitForTimeout(500); await expect(page.getByText(DEPT_NAME).first()).toBeVisible({ timeout: 3000 }); }); test('should switch to By Category view and see Uncategorized group', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'finances'); await page.getByRole('button', { name: 'By Category' }).click(); await page.waitForTimeout(500); await expect(page.getByText('Uncategorized').first()).toBeVisible({ timeout: 3000 }); }); test('should switch back to All Items view', async ({ page }) => { test.setTimeout(60000); await gotoEventPath(page, 'finances'); await page.getByRole('button', { name: 'All Items' }).click(); await expect(page.getByText(`PW Venue Rental ${RUN}`).first()).toBeVisible({ timeout: 3000 }); await expect(page.getByText(`PW Ticket Sales ${RUN}`).first()).toBeVisible({ timeout: 3000 }); }); });