feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata
This commit is contained in:
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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user