feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata
This commit is contained in:
@@ -65,12 +65,12 @@ test.describe('Account Settings Page', () => {
|
||||
});
|
||||
|
||||
test('should display Profile section with all elements', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('Display Name')).toBeVisible();
|
||||
await expect(page.getByText('Email', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Email').first()).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Upload' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sync Google' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Save Profile' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Save Profile' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display Appearance section with color picker', async ({ page }) => {
|
||||
@@ -110,12 +110,12 @@ test.describe('Kanban Page - ContextMenu', () => {
|
||||
});
|
||||
|
||||
test('should have context menu button in header', async ({ page }) => {
|
||||
const moreBtn = page.locator('header button[aria-label="More options"]');
|
||||
const moreBtn = page.getByRole('button', { name: 'More options' });
|
||||
await expect(moreBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open context menu on click', async ({ page }) => {
|
||||
const moreBtn = page.locator('header button[aria-label="More options"]');
|
||||
const moreBtn = page.getByRole('button', { name: 'More options' });
|
||||
await moreBtn.click();
|
||||
await expect(moreBtn).toHaveAttribute('aria-expanded', 'true', { timeout: 3000 });
|
||||
// Menu dropdown should be visible
|
||||
@@ -134,12 +134,12 @@ test.describe('Calendar Page - ContextMenu', () => {
|
||||
});
|
||||
|
||||
test('should have context menu button in header', async ({ page }) => {
|
||||
const moreBtn = page.locator('header button[aria-label="More options"]');
|
||||
const moreBtn = page.getByRole('button', { name: 'More options' });
|
||||
await expect(moreBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show Refresh Events in context menu', async ({ page }) => {
|
||||
const moreBtn = page.locator('header button[aria-label="More options"]');
|
||||
const moreBtn = page.getByRole('button', { name: 'More options' });
|
||||
await moreBtn.click();
|
||||
await expect(moreBtn).toHaveAttribute('aria-expanded', 'true', { timeout: 3000 });
|
||||
await expect(page.getByText('Refresh Events')).toBeVisible({ timeout: 3000 });
|
||||
|
||||
@@ -27,7 +27,7 @@ function loadEnv() {
|
||||
const value = trimmed.slice(eqIdx + 1);
|
||||
if (!process.env[key]) process.env[key] = value;
|
||||
}
|
||||
} catch { /* .env not found — rely on process.env */ }
|
||||
} catch { /* .env not found - rely on process.env */ }
|
||||
}
|
||||
loadEnv();
|
||||
|
||||
@@ -39,12 +39,14 @@ const DOC_PREFIXES = ['Test Folder', 'Test Doc', 'Test Board', 'Nav Folder', 'Re
|
||||
const BOARD_PREFIXES = ['PW Board', 'PW Card Board', 'PW Detail Board', 'Board A', 'Board B'];
|
||||
const ROLE_PREFIX = 'Tester';
|
||||
const TAG_PREFIX = 'PW Tag';
|
||||
const EVENT_PREFIXES = ['PW Event', 'PW Detail', 'PW Delete'];
|
||||
const EVENT_PREFIXES = ['PW Event', 'PW Detail', 'PW Delete', 'PW Test Event', 'PW Finance'];
|
||||
const SPONSOR_PREFIXES = ['PW Sponsor'];
|
||||
const CONTACT_PREFIXES = ['PW Contact', 'PW Vendor'];
|
||||
const INVITE_EMAIL_PATTERN = 'playwright-test-%@example.com';
|
||||
|
||||
export default async function globalTeardown() {
|
||||
if (!SUPABASE_KEY) {
|
||||
console.log('[cleanup] No SUPABASE_ANON_KEY — skipping cleanup');
|
||||
console.log('[cleanup] No SUPABASE_ANON_KEY - skipping cleanup');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +58,7 @@ export default async function globalTeardown() {
|
||||
});
|
||||
|
||||
if (authError) {
|
||||
console.log('[cleanup] Auth failed — skipping cleanup:', authError.message);
|
||||
console.log('[cleanup] Auth failed - skipping cleanup:', authError.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +70,7 @@ export default async function globalTeardown() {
|
||||
.single();
|
||||
|
||||
if (!org) {
|
||||
console.log('[cleanup] root-test org not found — skipping cleanup');
|
||||
console.log('[cleanup] root-test org not found - skipping cleanup');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -183,6 +185,51 @@ export default async function globalTeardown() {
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Delete test sponsors (via name prefix)
|
||||
for (const prefix of SPONSOR_PREFIXES) {
|
||||
const { data: sponsors } = await (supabase as any)
|
||||
.from('sponsors')
|
||||
.select('id')
|
||||
.ilike('name', `${prefix}%`);
|
||||
|
||||
if (sponsors && sponsors.length > 0) {
|
||||
const ids = sponsors.map((s: any) => s.id);
|
||||
const { error } = await (supabase as any)
|
||||
.from('sponsors')
|
||||
.delete()
|
||||
.in('id', ids);
|
||||
|
||||
if (!error) {
|
||||
totalDeleted += sponsors.length;
|
||||
} else {
|
||||
console.log(`[cleanup] Failed to delete sponsors with prefix "${prefix}":`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Delete test org contacts (via name prefix)
|
||||
for (const prefix of CONTACT_PREFIXES) {
|
||||
const { data: contacts } = await (supabase as any)
|
||||
.from('org_contacts')
|
||||
.select('id')
|
||||
.eq('org_id', orgId)
|
||||
.ilike('name', `${prefix}%`);
|
||||
|
||||
if (contacts && contacts.length > 0) {
|
||||
const ids = contacts.map((c: any) => c.id);
|
||||
const { error } = await (supabase as any)
|
||||
.from('org_contacts')
|
||||
.delete()
|
||||
.in('id', ids);
|
||||
|
||||
if (!error) {
|
||||
totalDeleted += contacts.length;
|
||||
} else {
|
||||
console.log(`[cleanup] Failed to delete contacts with prefix "${prefix}":`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalDeleted > 0) {
|
||||
console.log(`[cleanup] Deleted ${totalDeleted} test-created items from root-test org`);
|
||||
} else {
|
||||
|
||||
780
tests/e2e/events.spec.ts
Normal file
780
tests/e2e/events.spec.ts
Normal file
@@ -0,0 +1,780 @@
|
||||
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 });
|
||||
});
|
||||
});
|
||||
@@ -8,13 +8,13 @@ test.describe('File Management', () => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/documents`);
|
||||
});
|
||||
|
||||
test('should load files page with header and + New button', async ({ page }) => {
|
||||
test('should load files page with header and New button', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Files' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '+ New' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'New' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open Create New modal with type selectors', async ({ page }) => {
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Create New')).toBeVisible();
|
||||
@@ -30,7 +30,7 @@ test.describe('File Management', () => {
|
||||
|
||||
test('should create a folder and see it in the file list', async ({ page }) => {
|
||||
const folderName = `Test Folder ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
// Select Folder type
|
||||
@@ -46,7 +46,7 @@ test.describe('File Management', () => {
|
||||
|
||||
test('should create a document and navigate to editor', async ({ page }) => {
|
||||
const docName = `Test Doc ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
// Document type is default
|
||||
@@ -59,7 +59,7 @@ test.describe('File Management', () => {
|
||||
|
||||
test('should create a kanban board from files and navigate to it', async ({ page }) => {
|
||||
const boardName = `Test Board ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
// Select Kanban type
|
||||
@@ -79,7 +79,7 @@ test.describe('File Management', () => {
|
||||
test('should navigate into a folder and see breadcrumbs', async ({ page }) => {
|
||||
// First create a folder
|
||||
const folderName = `Nav Folder ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByText('Folder').click();
|
||||
@@ -101,7 +101,7 @@ test.describe('File Management', () => {
|
||||
test('should rename a file via right-click context menu', async ({ page }) => {
|
||||
// Create a document first
|
||||
const originalName = `Rename Me ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByText('Folder').click();
|
||||
@@ -112,7 +112,7 @@ test.describe('File Management', () => {
|
||||
|
||||
// Right-click to open context menu
|
||||
await page.getByText(originalName).first().click({ button: 'right' });
|
||||
// Context menu should appear — find the Rename button inside the fixed z-50 context menu
|
||||
// Context menu should appear - find the Rename button inside the fixed z-50 context menu
|
||||
const contextMenuPanel = page.locator('.fixed.z-50.bg-night');
|
||||
await expect(contextMenuPanel).toBeVisible({ timeout: 3000 });
|
||||
const renameBtn = contextMenuPanel.locator('button', { hasText: 'Rename' });
|
||||
@@ -160,12 +160,12 @@ test.describe('Kanban Board Page', () => {
|
||||
|
||||
test('should load kanban page with header', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { name: 'Kanban' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '+ New' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'New' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open Create Board modal and create a board', async ({ page }) => {
|
||||
const boardName = `PW Board ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Create Board')).toBeVisible();
|
||||
@@ -181,7 +181,7 @@ test.describe('Kanban Board Page', () => {
|
||||
test('should show board selector when multiple boards exist', async ({ page }) => {
|
||||
// Create two boards
|
||||
for (const name of [`Board A ${Date.now()}`, `Board B ${Date.now()}`]) {
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByPlaceholder('e.g. Sprint 1').fill(name);
|
||||
@@ -254,20 +254,20 @@ test.describe('Settings Page - Members', () => {
|
||||
await expect(page.getByText('Pending Invites')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(testInviteEmail).first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Clean up: cancel the invite — find the row containing the email, then its Cancel button
|
||||
// Clean up: cancel the invite - find the row containing the email, then its close button
|
||||
const inviteRow = page.locator('.bg-light\\/5').filter({ hasText: testInviteEmail });
|
||||
await inviteRow.getByRole('button', { name: 'Cancel' }).click();
|
||||
await inviteRow.getByTitle('Cancel invite').click();
|
||||
await expect(page.getByText(testInviteEmail)).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should show pending invites section when invites exist', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Members' }).click();
|
||||
await expect(page.getByText('Team Members').first()).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByRole('heading', { name: /Team Members/ })).toBeVisible({ timeout: 5000 });
|
||||
// Check if Pending Invites section exists (from previous test runs or this session)
|
||||
const hasPending = await page.getByText('Pending Invites').isVisible().catch(() => false);
|
||||
if (hasPending) {
|
||||
// Each invite should have Copy Link and Cancel buttons
|
||||
await expect(page.getByRole('button', { name: 'Copy Link' }).first()).toBeVisible();
|
||||
// Each invite should have copy and cancel icon buttons
|
||||
await expect(page.getByTitle('Cancel invite').first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -320,7 +320,7 @@ test.describe('Settings Page - Roles', () => {
|
||||
// Role should appear in the list
|
||||
await expect(page.getByText(roleName)).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Clean up: delete the role — set up dialog handler BEFORE clicking
|
||||
// Clean up: delete the role - set up dialog handler BEFORE clicking
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
// Find the specific role card that contains the role name and click its Delete button
|
||||
const deleteBtn = page.getByRole('button', { name: 'Delete' }).last();
|
||||
@@ -348,7 +348,7 @@ test.describe('Settings Page - Tags', () => {
|
||||
await page.getByRole('button', { name: 'Create Tag' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByPlaceholder('e.g., Bug').fill(tagName);
|
||||
await modal.getByLabel('Tag name').fill(tagName);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(tagName)).toBeVisible({ timeout: 5000 });
|
||||
@@ -356,7 +356,7 @@ test.describe('Settings Page - Tags', () => {
|
||||
// Clean up: delete the tag
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
const tagCard = page.locator('div').filter({ hasText: tagName }).last();
|
||||
const deleteBtn = tagCard.getByRole('button').filter({ hasText: /delete/i }).first();
|
||||
const deleteBtn = tagCard.locator('button').filter({ hasText: /delete/i }).first();
|
||||
if (await deleteBtn.isVisible()) {
|
||||
await deleteBtn.click();
|
||||
await expect(page.getByText(tagName)).not.toBeVisible({ timeout: 5000 });
|
||||
@@ -386,8 +386,8 @@ test.describe('Calendar - Event CRUD', () => {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/calendar`);
|
||||
});
|
||||
|
||||
test('should open create event modal via + New button', async ({ page }) => {
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
test('should open create event modal via New button', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Title')).toBeVisible();
|
||||
@@ -399,7 +399,7 @@ test.describe('Calendar - Event CRUD', () => {
|
||||
test('should create an event and see it on the calendar', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const eventTitle = `PW Event ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
@@ -416,7 +416,7 @@ test.describe('Calendar - Event CRUD', () => {
|
||||
test.setTimeout(60000);
|
||||
// Create an event first
|
||||
const eventTitle = `PW Detail ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByLabel('Title').fill(eventTitle);
|
||||
@@ -433,22 +433,26 @@ test.describe('Calendar - Event CRUD', () => {
|
||||
test('should delete an event', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const eventTitle = `PW Delete ${Date.now()}`;
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByLabel('Title').fill(eventTitle);
|
||||
await modal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(eventTitle).first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click event to open detail modal, then delete
|
||||
// Switch to Day view so the event is always visible (month view hides overflow behind +N)
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('button', { hasText: 'Day' }).filter({ hasText: /^Day$/ }).click();
|
||||
await expect(page.getByText(eventTitle).first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click event on calendar to open detail modal, then delete
|
||||
await page.getByText(eventTitle).first().click();
|
||||
const detailModal = page.getByRole('dialog');
|
||||
await expect(detailModal).toBeVisible({ timeout: 3000 });
|
||||
await expect(detailModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
const deleteBtn = detailModal.getByRole('button', { name: 'Delete' });
|
||||
if (await deleteBtn.isVisible()) {
|
||||
if (await deleteBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await deleteBtn.click();
|
||||
await expect(detailModal).not.toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
@@ -467,7 +471,7 @@ test.describe('Kanban - Card CRUD', () => {
|
||||
const boardName = `PW Card Board ${Date.now()}`;
|
||||
|
||||
// Create board
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByPlaceholder('e.g. Sprint 1').fill(boardName);
|
||||
@@ -475,26 +479,19 @@ test.describe('Kanban - Card CRUD', () => {
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('To Do')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Add a card — click the + button in the first column
|
||||
const addCardBtn = page.locator('button[title="Add card"]').first();
|
||||
if (await addCardBtn.isVisible()) {
|
||||
await addCardBtn.click();
|
||||
} else {
|
||||
// Fallback: look for any add button in the column area
|
||||
const plusBtn = page.locator('button').filter({ hasText: '+' }).first();
|
||||
await plusBtn.click();
|
||||
}
|
||||
// Add a card - click the "Add card" button in the first column
|
||||
const addCardBtn = page.getByRole('button', { name: 'Add card' }).first();
|
||||
await expect(addCardBtn).toBeVisible({ timeout: 3000 });
|
||||
await addCardBtn.click();
|
||||
|
||||
// Card creation modal or inline input should appear
|
||||
// Card creation modal should appear
|
||||
const cardModal = page.getByRole('dialog');
|
||||
const hasModal = await cardModal.isVisible().catch(() => false);
|
||||
if (hasModal) {
|
||||
const cardTitle = `PW Card ${Date.now()}`;
|
||||
await cardModal.getByLabel('Title').fill(cardTitle);
|
||||
await cardModal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(cardModal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(cardTitle).first()).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
await expect(cardModal).toBeVisible({ timeout: 3000 });
|
||||
const cardTitle = `PW Card ${Date.now()}`;
|
||||
await cardModal.getByLabel('Title').fill(cardTitle);
|
||||
await cardModal.getByRole('button', { name: 'Add Card' }).click();
|
||||
await expect(cardModal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(cardTitle).first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should open card detail modal on card click', async ({ page }) => {
|
||||
@@ -502,7 +499,7 @@ test.describe('Kanban - Card CRUD', () => {
|
||||
const boardName = `PW Detail Board ${Date.now()}`;
|
||||
|
||||
// Create board
|
||||
await page.getByRole('button', { name: '+ New' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
const modal = page.getByRole('dialog');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.getByPlaceholder('e.g. Sprint 1').fill(boardName);
|
||||
@@ -510,25 +507,23 @@ test.describe('Kanban - Card CRUD', () => {
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('To Do')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Add a card via the + button
|
||||
const addCardBtn = page.locator('button[title="Add card"]').first();
|
||||
if (await addCardBtn.isVisible()) {
|
||||
await addCardBtn.click();
|
||||
const cardModal = page.getByRole('dialog');
|
||||
const hasModal = await cardModal.isVisible().catch(() => false);
|
||||
if (hasModal) {
|
||||
const cardTitle = `PW Click Card ${Date.now()}`;
|
||||
await cardModal.getByLabel('Title').fill(cardTitle);
|
||||
await cardModal.getByRole('button', { name: 'Create' }).click();
|
||||
await expect(cardModal).not.toBeVisible({ timeout: 5000 });
|
||||
// Add a card via the "Add card" button
|
||||
const addCardBtn = page.getByRole('button', { name: 'Add card' }).first();
|
||||
await expect(addCardBtn).toBeVisible({ timeout: 3000 });
|
||||
await addCardBtn.click();
|
||||
|
||||
// Click the card to open detail modal
|
||||
await page.getByText(cardTitle).first().click();
|
||||
const detailModal = page.getByRole('dialog');
|
||||
await expect(detailModal).toBeVisible({ timeout: 5000 });
|
||||
await expect(detailModal.getByText(cardTitle)).toBeVisible();
|
||||
}
|
||||
}
|
||||
const cardModal = page.getByRole('dialog');
|
||||
await expect(cardModal).toBeVisible({ timeout: 3000 });
|
||||
const cardTitle = `PW Click Card ${Date.now()}`;
|
||||
await cardModal.getByLabel('Title').fill(cardTitle);
|
||||
await cardModal.getByRole('button', { name: 'Add Card' }).click();
|
||||
await expect(cardModal).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click the card to open detail modal
|
||||
await page.getByText(cardTitle).first().click();
|
||||
const detailModal = page.getByRole('dialog');
|
||||
await expect(detailModal).toBeVisible({ timeout: 5000 });
|
||||
await expect(detailModal.getByLabel('Title')).toHaveValue(cardTitle);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
424
tests/e2e/finance-features.spec.ts
Normal file
424
tests/e2e/finance-features.spec.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_ORG_SLUG, navigateTo, waitForHydration } from './helpers';
|
||||
|
||||
// Unique suffix for this test run
|
||||
const RUN = Date.now();
|
||||
const EVENT_NAME = `PW Finance ${RUN}`;
|
||||
const DEPT_NAME = `PW Fin Dept ${RUN}`;
|
||||
|
||||
let eventSlug = '';
|
||||
let deptId = '';
|
||||
|
||||
async function gotoEventPath(page: import('@playwright/test').Page, subPath: string) {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/events/${eventSlug}/${subPath}`);
|
||||
}
|
||||
|
||||
async function gotoEvent(page: import('@playwright/test').Page) {
|
||||
await navigateTo(page, `/${TEST_ORG_SLUG}/events/${eventSlug}`);
|
||||
}
|
||||
|
||||
// ─── Serial flow: create event + dept, then test new pages ──────────────────
|
||||
|
||||
test.describe.serial('Finance Features - New Event Pages', () => {
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// 0. SETUP: Create event + department
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
test('should create a test event', 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);
|
||||
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();
|
||||
});
|
||||
|
||||
test('should create a test 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 });
|
||||
|
||||
// Reload the event page so the sidebar picks up the new department
|
||||
await gotoEvent(page);
|
||||
const sidebar = page.locator('aside');
|
||||
await expect(sidebar.getByText(DEPT_NAME).first()).toBeVisible({ timeout: 10000 });
|
||||
await sidebar.getByText(DEPT_NAME).first().click();
|
||||
await page.waitForURL(/\/dept\//, { timeout: 10000 });
|
||||
const match = page.url().match(/\/dept\/([^/]+)/);
|
||||
if (match) deptId = match[1];
|
||||
expect(deptId).toBeTruthy();
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// 1. SIDEBAR - new nav items
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
test('should show Sponsors, Contacts, and Files in sidebar', async ({ page }) => {
|
||||
await gotoEvent(page);
|
||||
// The event sidebar is the last <aside> on the page (org sidebar is first)
|
||||
const eventSidebar = page.locator('aside').last();
|
||||
await expect(eventSidebar.getByRole('link', { name: /Sponsors/ })).toBeVisible({ timeout: 5000 });
|
||||
await expect(eventSidebar.getByRole('link', { name: /Contacts/ })).toBeVisible({ timeout: 5000 });
|
||||
await expect(eventSidebar.getByRole('link', { name: /Files/ })).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should navigate to Sponsors page via sidebar', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
// The h1 uses text-heading-sm class - match by text
|
||||
await expect(page.locator('h1').filter({ hasText: 'Sponsors' })).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('Total Pipeline')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should navigate to Contacts page via sidebar', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'contacts');
|
||||
await expect(page.locator('h1').filter({ hasText: 'Contacts' })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should navigate to Files page via sidebar', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'files');
|
||||
await expect(page.locator('h1').filter({ hasText: 'Files' })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// 2. SPONSORS PAGE - empty state + CRUD
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
test('should show sponsors empty state with summary cards', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
await expect(page.getByText('Total Pipeline')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Confirmed / Active')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('No sponsors yet')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should show Add Sponsor button and open modal', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
const addBtn = page.getByRole('button', { name: /Add Sponsor/i });
|
||||
await expect(addBtn).toBeVisible({ timeout: 5000 });
|
||||
await addBtn.click();
|
||||
const modal = page.locator('.fixed.inset-0.z-\\[60\\]');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Add Sponsor')).toBeVisible();
|
||||
// Form fields
|
||||
await expect(modal.locator('#sp-name')).toBeVisible();
|
||||
await expect(modal.locator('#sp-dept')).toBeVisible();
|
||||
await expect(modal.locator('#sp-status')).toBeVisible();
|
||||
await expect(modal.locator('#sp-amount')).toBeVisible();
|
||||
// Close
|
||||
await modal.getByRole('button', { name: /Cancel/i }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should create a sponsor and see it in the list', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
|
||||
// Capture console errors to debug RLS/API issues
|
||||
const consoleErrors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
||||
});
|
||||
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
await page.getByRole('button', { name: /Add Sponsor/i }).click();
|
||||
const modal = page.locator('.fixed.inset-0.z-\\[60\\]');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Ensure department select has a value (auto-selected first dept)
|
||||
const deptSelect = modal.locator('#sp-dept');
|
||||
await expect(deptSelect).toBeVisible({ timeout: 3000 });
|
||||
const deptVal = await deptSelect.inputValue();
|
||||
expect(deptVal).toBeTruthy();
|
||||
|
||||
await modal.locator('#sp-name').fill(`PW Sponsor A ${RUN}`);
|
||||
await modal.locator('#sp-status').selectOption('confirmed');
|
||||
await modal.locator('#sp-amount').fill('5000');
|
||||
await modal.locator('#sp-contact').fill('John Doe');
|
||||
|
||||
const addBtn = modal.getByRole('button', { name: /^Add$/ });
|
||||
await expect(addBtn).toBeEnabled({ timeout: 3000 });
|
||||
await addBtn.click();
|
||||
|
||||
// Wait for the network request to complete and UI to update
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Log any console errors for debugging
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('[DEBUG] Console errors during sponsor creation:', consoleErrors);
|
||||
}
|
||||
|
||||
// Check if modal closed (success) or shows error
|
||||
const modalStillVisible = await modal.isVisible();
|
||||
if (modalStillVisible) {
|
||||
// Take note - API likely failed
|
||||
console.log('[DEBUG] Modal still visible after clicking Add - API may have failed');
|
||||
// Check for error toast
|
||||
const errorToast = page.getByText('Failed to save sponsor');
|
||||
const hasError = await errorToast.isVisible().catch(() => false);
|
||||
console.log('[DEBUG] Error toast visible:', hasError);
|
||||
}
|
||||
|
||||
await expect(modal).not.toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText(`PW Sponsor A ${RUN}`).first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should show updated summary after adding sponsor', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
// Pipeline should show €5,000.00
|
||||
await expect(page.getByText('€5,000.00').first()).toBeVisible({ timeout: 5000 });
|
||||
// Confirmed should also show €5,000.00
|
||||
await expect(page.getByText('1').first()).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should create a second sponsor', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
await page.getByRole('button', { name: /Add Sponsor/i }).click();
|
||||
const modal = page.locator('.fixed.inset-0.z-\\[60\\]');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await modal.locator('#sp-name').fill(`PW Sponsor B ${RUN}`);
|
||||
await modal.locator('#sp-status').selectOption('prospect');
|
||||
await modal.locator('#sp-amount').fill('2000');
|
||||
|
||||
await modal.getByRole('button', { name: /^Add$/ }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(`PW Sponsor B ${RUN}`).first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should filter sponsors by status', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
// Filter to Confirmed only
|
||||
await page.locator('select').first().selectOption('confirmed');
|
||||
await expect(page.getByText(`PW Sponsor A ${RUN}`).first()).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.getByText(`PW Sponsor B ${RUN}`)).not.toBeVisible({ timeout: 2000 });
|
||||
// Reset to All
|
||||
await page.locator('select').first().selectOption('all');
|
||||
await expect(page.getByText(`PW Sponsor B ${RUN}`).first()).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should search sponsors by name', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
await page.locator('input[placeholder="Search sponsors..."]').fill('Sponsor A');
|
||||
await expect(page.getByText(`PW Sponsor A ${RUN}`).first()).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.getByText(`PW Sponsor B ${RUN}`)).not.toBeVisible({ timeout: 2000 });
|
||||
// Clear search
|
||||
await page.locator('input[placeholder="Search sponsors..."]').fill('');
|
||||
await expect(page.getByText(`PW Sponsor B ${RUN}`).first()).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should edit a sponsor via edit button', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
// Click edit on Sponsor A - target the grid row that directly contains the sponsor name
|
||||
const sponsorText = page.getByText(`PW Sponsor A ${RUN}`).first();
|
||||
await expect(sponsorText).toBeVisible({ timeout: 5000 });
|
||||
const row = sponsorText.locator('xpath=ancestor::div[contains(@class,"grid")]').first();
|
||||
await row.locator('button[title="Edit"]').click();
|
||||
const modal = page.locator('.fixed.inset-0.z-\\[60\\]');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Edit Sponsor')).toBeVisible();
|
||||
// Update amount
|
||||
await modal.locator('#sp-amount').fill('6000');
|
||||
await modal.getByRole('button', { name: /Save/i }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should delete a sponsor via delete button', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'sponsors');
|
||||
// Delete Sponsor B - target the grid row
|
||||
const sponsorText = page.getByText(`PW Sponsor B ${RUN}`).first();
|
||||
await expect(sponsorText).toBeVisible({ timeout: 5000 });
|
||||
const row = sponsorText.locator('xpath=ancestor::div[contains(@class,"grid")]').first();
|
||||
await row.locator('button[title="Delete"]').click();
|
||||
await expect(page.getByText(`PW Sponsor B ${RUN}`)).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// 3. CONTACTS PAGE - empty state + CRUD
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
test('should show contacts page with header and controls', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'contacts');
|
||||
await expect(page.locator('h1').filter({ hasText: 'Contacts' })).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Organization-wide')).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.getByRole('button', { name: /Add Contact/i })).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should open Add Contact modal', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'contacts');
|
||||
await page.getByRole('button', { name: /Add Contact/i }).click();
|
||||
const modal = page.locator('.fixed.inset-0.z-\\[60\\]');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Add Contact')).toBeVisible();
|
||||
await expect(modal.locator('#ct-name')).toBeVisible();
|
||||
await expect(modal.locator('#ct-company')).toBeVisible();
|
||||
await expect(modal.locator('#ct-email')).toBeVisible();
|
||||
await modal.getByRole('button', { name: /Cancel/i }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should create a contact and see it in the list', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'contacts');
|
||||
await page.getByRole('button', { name: /Add Contact/i }).click();
|
||||
const modal = page.locator('.fixed.inset-0.z-\\[60\\]');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await modal.locator('#ct-name').fill(`PW Contact ${RUN}`);
|
||||
await modal.locator('#ct-company').fill('Test Corp');
|
||||
await modal.locator('#ct-email').fill('test@example.com');
|
||||
await modal.locator('#ct-phone').fill('+372 5551234');
|
||||
|
||||
await modal.getByRole('button', { name: /^Add$/ }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(`PW Contact ${RUN}`).first()).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Test Corp').first()).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should expand a contact to see details', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'contacts');
|
||||
// Click the contact row to expand
|
||||
await page.getByText(`PW Contact ${RUN}`).first().click();
|
||||
// Should show email and phone
|
||||
await expect(page.getByText('test@example.com').first()).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.getByText('+372 5551234').first()).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should edit a contact', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'contacts');
|
||||
// Expand first
|
||||
await page.getByText(`PW Contact ${RUN}`).first().click();
|
||||
// Click Edit in expanded area
|
||||
await page.getByRole('button', { name: /^Edit$/ }).first().click();
|
||||
const modal = page.locator('.fixed.inset-0.z-\\[60\\]');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await expect(modal.getByText('Edit Contact')).toBeVisible();
|
||||
await modal.locator('#ct-company').fill('Updated Corp');
|
||||
await modal.getByRole('button', { name: /Save/i }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Updated Corp').first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should delete a contact', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'contacts');
|
||||
// Expand
|
||||
await page.getByText(`PW Contact ${RUN}`).first().click();
|
||||
// Delete
|
||||
await page.getByRole('button', { name: /^Delete$/ }).first().click();
|
||||
await expect(page.getByText(`PW Contact ${RUN}`)).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should filter contacts by category', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'contacts');
|
||||
// Create a vendor contact first
|
||||
await page.getByRole('button', { name: /Add Contact/i }).click();
|
||||
const modal = page.locator('.fixed.inset-0.z-\\[60\\]');
|
||||
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||
await modal.locator('#ct-name').fill(`PW Vendor ${RUN}`);
|
||||
await modal.locator('#ct-category').selectOption('vendor');
|
||||
await modal.getByRole('button', { name: /^Add$/ }).click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText(`PW Vendor ${RUN}`).first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Filter by vendor
|
||||
await page.locator('select').first().selectOption('vendor');
|
||||
await expect(page.getByText(`PW Vendor ${RUN}`).first()).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// 4. FILES PAGE - empty/no-folder state
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
test('should show Files page with header', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'files');
|
||||
await expect(page.getByRole('heading', { name: 'Files' })).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// 5. FINANCES PAGE - updated tabs
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
test('should load Finances page with tab buttons', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'finances');
|
||||
await expect(page.getByRole('heading', { name: 'Finances' })).toBeVisible({ timeout: 5000 });
|
||||
// Tab buttons
|
||||
await expect(page.getByRole('button', { name: 'Overview' })).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.getByRole('button', { name: 'Details' })).toBeVisible({ timeout: 3000 });
|
||||
await expect(page.getByRole('button', { name: 'Sponsors' })).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test('should show Overview tab with department table headers', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'finances');
|
||||
// Overview tab should be active by default
|
||||
await expect(page.getByText('Planned Budget')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should switch to Details tab', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'finances');
|
||||
await page.getByRole('button', { name: 'Details' }).click();
|
||||
// Details tab content - should show line items or empty state
|
||||
await page.waitForTimeout(500);
|
||||
// The details tab should be visible (either items or empty)
|
||||
const detailsVisible = await page.getByText('No budget items').isVisible().catch(() => false);
|
||||
const itemsVisible = await page.locator('.grid.grid-cols-12').first().isVisible().catch(() => false);
|
||||
expect(detailsVisible || itemsVisible).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should switch to Sponsors tab', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'finances');
|
||||
await page.getByRole('button', { name: 'Sponsors' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
// Should show sponsor allocation content or empty state
|
||||
const hasSponsors = await page.getByText('No funded sponsors').isVisible().catch(() => false);
|
||||
const hasSponsorCards = await page.locator('.bg-surface\\/30').first().isVisible().catch(() => false);
|
||||
expect(hasSponsors || hasSponsorCards).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show department row in Overview with our department', async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
await gotoEventPath(page, 'finances');
|
||||
// Our department should appear in the overview table
|
||||
await expect(page.getByText(DEPT_NAME).first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,7 @@ export async function navigateTo(page: Page, path: string) {
|
||||
|
||||
/**
|
||||
* Wait for Svelte 5 hydration on the current page.
|
||||
* Svelte 5 uses event delegation — handlers only work after hydration.
|
||||
* Svelte 5 uses event delegation - handlers only work after hydration.
|
||||
*/
|
||||
export async function waitForHydration(page: Page) {
|
||||
await page.waitForFunction(() => {
|
||||
|
||||
@@ -23,7 +23,7 @@ test.describe('Kanban card move latency', () => {
|
||||
// Wait for columns
|
||||
await page.waitForSelector('[data-column-id]', { timeout: 10000 });
|
||||
|
||||
// Ensure there's a card to move — check first column
|
||||
// Ensure there's a card to move - check first column
|
||||
const firstCard = page.locator('[data-column-id]').first().locator('[data-card-id]').first();
|
||||
if (!(await firstCard.isVisible({ timeout: 2000 }).catch(() => false))) {
|
||||
const addBtn = page.locator('[data-column-id]').first().getByRole('button', { name: /add card/i });
|
||||
@@ -120,7 +120,7 @@ test.describe('Kanban card move latency', () => {
|
||||
console.log(` DROP → RENDER LATENCY: ${dropToRender.toFixed(1)}ms`);
|
||||
if (dropToRender < 0) {
|
||||
console.log(' ⚠️ Card never appeared or mouseup not captured');
|
||||
console.log(' (HTML5 drag may not have fired — trying synthetic fallback)');
|
||||
console.log(' (HTML5 drag may not have fired - trying synthetic fallback)');
|
||||
} else if (dropToRender < 20) {
|
||||
console.log(' ✅ INSTANT (<20ms)');
|
||||
} else if (dropToRender < 50) {
|
||||
|
||||
Reference in New Issue
Block a user