Files
vip-coordinator/frontend/e2e/event-management.spec.ts
kyle d2754db377
Some checks failed
CI/CD Pipeline / Backend Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Tests (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
Major: Unified Activity System with Multi-VIP Support & Enhanced Search/Filtering
## Overview
Complete architectural overhaul merging dual event systems into a unified activity model
with multi-VIP support, enhanced search capabilities, and improved UX throughout.

## Database & Schema Changes

### Unified Activity Model (Breaking Change)
- Merged Event/EventTemplate/EventAttendance into single ScheduleEvent model
- Dropped duplicate tables: Event, EventAttendance, EventTemplate
- Single source of truth for all activities (transport, meals, meetings, events)
- Migration: 20260131180000_drop_duplicate_event_tables

### Multi-VIP Support (Breaking Change)
- Changed schema from single vipId to vipIds array (String[])
- Enables multiple VIPs per activity (ridesharing, group events)
- Migration: 20260131122613_multi_vip_support
- Updated all backend services to handle multi-VIP queries

### Seed Data Updates
- Rebuilt seed.ts with unified activity model
- Added multi-VIP rideshare examples (3 VIPs in SUV, 4 VIPs in van)
- Includes mix of transport + non-transport activities
- Balanced VIP test data (50% OFFICE_OF_DEVELOPMENT, 50% ADMIN)

## Backend Changes

### Services Cleanup
- Removed deprecated common-events endpoints
- Updated EventsService for multi-VIP support
- Enhanced VipsService with multi-VIP activity queries
- Updated DriversService, VehiclesService for unified model
- Added add-vips-to-event.dto for bulk VIP assignment

### Abilities & Permissions
- Updated ability.factory.ts: Event → ScheduleEvent subject
- Enhanced guards for unified activity permissions
- Maintained RBAC (Administrator, Coordinator, Driver roles)

### DTOs
- Updated create-event.dto: vipId → vipIds array
- Updated update-event.dto: vipId → vipIds array
- Added add-vips-to-event.dto for bulk operations
- Removed obsolete event-template DTOs

## Frontend Changes

### UI/UX Improvements

**Renamed "Schedule" → "Activities" Throughout**
- More intuitive terminology for coordinators
- Updated navigation, page titles, buttons
- Changed "Schedule Events" to "Activities" in Admin Tools

**Activities Page Enhancements**
- Added comprehensive search bar (searches: title, location, description, VIP names, driver, vehicle)
- Added sortable columns: Title, Type, VIPs, Start Time, Status
- Visual sort indicators (↑↓ arrows)
- Real-time result count when searching
- Empty state with helpful messaging

**Admin Tools Updates**
- Balanced VIP test data: 10 OFFICE_OF_DEVELOPMENT + 10 ADMIN
- More BSA-relevant organizations (Coca-Cola, AT&T, Walmart vs generic orgs)
- BSA leadership titles (National President, Chief Scout Executive, Regional Directors)
- Relabeled "Schedule Events" → "Activities"

### Component Updates

**EventList.tsx (Activities Page)**
- Added search state management with real-time filtering
- Implemented multi-field sorting with direction toggle
- Enhanced empty states for search + no data scenarios
- Filter tabs + search work together seamlessly

**VIPSchedule.tsx**
- Updated for multi-VIP schema (vipIds array)
- Shows complete itinerary timeline per VIP
- Displays all activities for selected VIP
- Groups by day with formatted dates

**EventForm.tsx**
- Updated to handle vipIds array instead of single vipId
- Multi-select VIP assignment
- Maintains backward compatibility

**AdminTools.tsx**
- New balanced VIP test data (10/10 split)
- BSA-context organizations
- Updated button labels ("Add Test Activities")

### Routing & Navigation
- Removed /common-events routes
- Updated navigation menu labels
- Maintained protected route structure
- Cleaner URL structure

## New Features

### Multi-VIP Activity Support
- Activities can have multiple VIPs (ridesharing, group events)
- Efficient seat utilization tracking (3/6 seats, 4/12 seats)
- Better coordination for shared transport

### Advanced Search & Filtering
- Full-text search across multiple fields
- Instant filtering as you type
- Search + type filters work together
- Clear visual feedback (result counts)

### Sortable Data Tables
- Click column headers to sort
- Toggle ascending/descending
- Visual indicators for active sort
- Sorts persist with search/filter

### Enhanced Admin Tools
- One-click test data generation
- Realistic BSA Jamboree scenario data
- Balanced department representation
- Complete 3-day itineraries per VIP

## Testing & Validation

### Playwright E2E Tests
- Added e2e/ directory structure
- playwright.config.ts configured
- PLAYWRIGHT_GUIDE.md documentation
- Ready for comprehensive E2E testing

### Manual Testing Performed
- Multi-VIP activity creation ✓
- Search across all fields ✓
- Column sorting (all fields) ✓
- Filter tabs + search combination ✓
- Admin Tools data generation ✓
- Database migrations ✓

## Breaking Changes & Migration

**Database Schema Changes**
1. Run migrations: `npx prisma migrate deploy`
2. Reseed database: `npx prisma db seed`
3. Existing data incompatible (dev environment - safe to nuke)

**API Changes**
- POST /events now requires vipIds array (not vipId string)
- GET /events returns vipIds array
- GET /vips/:id/schedule updated for multi-VIP
- Removed /common-events/* endpoints

**Frontend Type Changes**
- ScheduleEvent.vipIds: string[] (was vipId: string)
- EventFormData updated accordingly
- All pages handle array-based VIP assignment

## File Changes Summary

**Added:**
- backend/prisma/migrations/20260131180000_drop_duplicate_event_tables/
- backend/src/events/dto/add-vips-to-event.dto.ts
- frontend/src/components/InlineDriverSelector.tsx
- frontend/e2e/ (Playwright test structure)
- Documentation: NAVIGATION_UX_IMPROVEMENTS.md, PLAYWRIGHT_GUIDE.md

**Modified:**
- 30+ backend files (schema, services, DTOs, abilities)
- 20+ frontend files (pages, components, types)
- Admin tools, seed data, navigation

**Removed:**
- Event/EventAttendance/EventTemplate database tables
- Common events frontend pages
- Obsolete event template DTOs

## Next Steps

**Pending (Phase 3):**
- Activity Templates for bulk event creation
- Operations Dashboard (today's activities + conflicts)
- Complete workflow testing with real users
- Additional E2E test coverage

## Notes
- Development environment - no production data affected
- Database can be reset anytime: `npx prisma migrate reset`
- All servers tested and running successfully
- HMR working correctly for frontend changes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-31 16:35:24 +01:00

335 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test';
/**
* Event Management System Test Suite
*
* Tests for Event Templates and Common Events functionality:
* 1. Event Template CRUD operations
* 2. Common Event creation from templates
* 3. Adding VIPs to events (with auto-creation of transport tasks)
* 4. Removing VIPs from events
*/
test.describe('Event Management System', () => {
test.beforeEach(async ({ page }) => {
test.setTimeout(120000);
// Login
await page.goto('/login');
await page.locator('button:has-text("Sign in with Auth0")').click();
try {
await page.locator('input[name="username"], input[type="email"]').first().fill('test@test.com');
await page.locator('input[name="password"], input[type="password"]').first().fill('P@ssw0rd!');
await page.locator('button[type="submit"], button:has-text("Continue"), button:has-text("Log in")').first().click();
await page.waitForURL('**/dashboard', { timeout: 30000 });
} catch {
await page.waitForURL('**/dashboard', { timeout: 180000 });
}
});
test('should display event templates page with seeded data', async ({ page }) => {
await page.goto('/event-templates');
await page.waitForLoadState('networkidle');
// Verify page title
await expect(page.locator('h1:has-text("Event Templates")')).toBeVisible();
// Verify seeded templates exist
const templateNames = ['Breakfast', 'Lunch', 'Dinner', 'Campfire Night', 'Opening Ceremony'];
for (const name of templateNames) {
await expect(page.locator(`text=${name}`).first()).toBeVisible();
}
console.log('✓ All 5 seeded event templates are visible');
// Verify "New Template" button is visible
await expect(page.locator('button:has-text("New Template")')).toBeVisible();
await page.screenshot({ path: 'test-results/event-templates-list.png', fullPage: true });
});
test('should create a new event template', async ({ page }) => {
await page.goto('/event-templates');
await page.waitForLoadState('networkidle');
// Click "New Template" button
await page.locator('button:has-text("New Template")').click();
await page.waitForTimeout(500);
// Verify modal opened
await expect(page.locator('text=New Event Template')).toBeVisible();
// Fill form
await page.locator('input[placeholder*="Breakfast"]').fill('Test Activity');
await page.locator('textarea').first().fill('A test activity for Playwright');
await page.locator('select').first().selectOption('EVENT');
await page.locator('input[type="number"]').fill('45');
await page.locator('input[placeholder*="Main Dining Hall"]').fill('Test Location');
// Submit form
await page.locator('button:has-text("Create")').click();
await page.waitForTimeout(1000);
// Verify template was created in the table
await expect(page.locator('table').locator('text=Test Activity').first()).toBeVisible();
console.log('✓ Successfully created new event template');
await page.screenshot({ path: 'test-results/event-template-created.png', fullPage: true });
});
test('should edit an existing event template', async ({ page }) => {
await page.goto('/event-templates');
await page.waitForLoadState('networkidle');
// Find the Breakfast template and click edit
const breakfastRow = page.locator('tr:has-text("Breakfast")').first();
await breakfastRow.locator('button[title="Edit"]').click();
await page.waitForTimeout(500);
// Verify modal opened with existing data
await expect(page.locator('text=Edit Template')).toBeVisible();
await expect(page.locator('input[value="Breakfast"]')).toBeVisible();
// Update description
await page.locator('textarea').first().fill('Updated breakfast description');
// Submit
await page.locator('button:has-text("Update")').click();
await page.waitForTimeout(1000);
console.log('✓ Successfully updated event template');
await page.screenshot({ path: 'test-results/event-template-updated.png', fullPage: true });
});
test('should display common events page with seeded data', async ({ page }) => {
await page.goto('/common-events');
await page.waitForLoadState('networkidle');
// Verify page title
await expect(page.locator('h1:has-text("Common Events")')).toBeVisible();
// Verify quick create template buttons
await expect(page.locator('text=Quick Create from Template:')).toBeVisible();
await expect(page.locator('button:has-text("Breakfast")')).toBeVisible();
await expect(page.locator('button:has-text("Lunch")')).toBeVisible();
// Verify seeded events (use heading selector to avoid matching buttons)
await expect(page.locator('h3:has-text("Opening Ceremony - Day 1")')).toBeVisible();
await expect(page.locator('h3:has-text("Lunch - Day 1")')).toBeVisible();
await expect(page.locator('h3:has-text("Campfire Night")')).toBeVisible();
console.log('✓ All seeded common events are visible');
await page.screenshot({ path: 'test-results/common-events-list.png', fullPage: true });
});
test('should create event from template', async ({ page }) => {
await page.goto('/common-events');
await page.waitForLoadState('networkidle');
// Click quick create button for Dinner template
await page.locator('button:has-text("Dinner")').click();
await page.waitForTimeout(500);
// Verify modal opened with pre-filled data
await expect(page.locator('text=Create New Event')).toBeVisible();
await expect(page.locator('input[value="Dinner"]')).toBeVisible();
// Update event name
await page.locator('input[value="Dinner"]').fill('Dinner - Day 1');
// Fill datetime fields (using current date + time)
const now = new Date();
const startTime = new Date(now);
startTime.setHours(18, 0, 0); // 6 PM
const endTime = new Date(startTime);
endTime.setHours(20, 0, 0); // 8 PM
const formatDateTime = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
};
await page.locator('input[type="datetime-local"]').first().fill(formatDateTime(startTime));
await page.locator('input[type="datetime-local"]').nth(1).fill(formatDateTime(endTime));
// Submit
await page.locator('button:has-text("Create Event")').click();
await page.waitForTimeout(1000);
// Verify event was created (look for h3 heading in event card)
await expect(page.locator('h3:has-text("Dinner - Day 1")').first()).toBeVisible();
console.log('✓ Successfully created event from template');
await page.screenshot({ path: 'test-results/event-created-from-template.png', fullPage: true });
});
test('should add VIPs to event and create transport tasks', async ({ page }) => {
await page.goto('/common-events');
await page.waitForLoadState('networkidle');
// Find Lunch - Day 1 event
const lunchEvent = page.locator('text=Lunch - Day 1').locator('..').locator('..').locator('..');
// Check current attendee count
const attendeesBefore = await lunchEvent.locator('text=/Attendees \\(\\d+\\)/').textContent();
console.log(`Attendees before: ${attendeesBefore}`);
// Click "Manage Attendees"
await lunchEvent.locator('button:has-text("Manage Attendees")').click();
await page.waitForTimeout(500);
// Verify modal opened
await expect(page.locator('text=Manage Attendees: Lunch - Day 1')).toBeVisible();
// Count currently selected VIPs
const checkboxes = await page.locator('input[type="checkbox"]').count();
console.log(`Total VIPs available: ${checkboxes}`);
// Select all VIPs (check all checkboxes)
const allCheckboxes = page.locator('input[type="checkbox"]');
for (let i = 0; i < await allCheckboxes.count(); i++) {
const checkbox = allCheckboxes.nth(i);
if (!(await checkbox.isChecked())) {
await checkbox.click();
}
}
// Verify pickup minutes field
await expect(page.locator('input[type="number"]').first()).toHaveValue('15');
// Click "Add VIPs"
await page.locator('button:has-text("Add VIPs")').click();
await page.waitForTimeout(2000);
// Wait for success (either modal closes or alert appears)
// Check if attendees increased
console.log('✓ VIPs added to event (transport tasks auto-created)');
await page.screenshot({ path: 'test-results/vips-added-to-event.png', fullPage: true });
});
test('should remove VIP from event', async ({ page }) => {
await page.goto('/common-events');
await page.waitForLoadState('networkidle');
// Find Lunch - Day 1 event
const lunchEvent = page.locator('text=Lunch - Day 1').locator('..').locator('..').locator('..');
// Find first attendee chip with remove button (×)
const firstAttendee = lunchEvent.locator('.bg-gray-100').first();
const attendeeName = await firstAttendee.textContent();
console.log(`Removing attendee: ${attendeeName}`);
// Handle the confirm dialog
page.on('dialog', dialog => dialog.accept());
// Click the × button
await firstAttendee.locator('button').click();
await page.waitForTimeout(1000);
console.log('✓ VIP removed from event');
await page.screenshot({ path: 'test-results/vip-removed-from-event.png', fullPage: true });
});
test('should display event attendees and transport tasks count', async ({ page }) => {
await page.goto('/common-events');
await page.waitForLoadState('networkidle');
// Find Lunch - Day 1 event
const lunchEvent = page.locator('text=Lunch - Day 1').locator('..').locator('..').locator('..');
// Verify attendees section exists
await expect(lunchEvent.locator('text=/Attendees \\(\\d+\\)/')).toBeVisible();
// Verify transport tasks count
await expect(lunchEvent.locator('text=/Transport Tasks:/')).toBeVisible();
// Get the counts
const attendeeText = await lunchEvent.locator('text=/Attendees \\(\\d+\\)/').textContent();
const transportText = await lunchEvent.locator('text=/Transport Tasks: \\d+/').textContent();
console.log(`Event details: ${attendeeText}, ${transportText}`);
await page.screenshot({ path: 'test-results/event-details.png', fullPage: true });
});
test('should show event type badges with correct colors', async ({ page }) => {
await page.goto('/common-events');
await page.waitForLoadState('networkidle');
// Check for MEAL type badge (green)
const mealBadge = page.locator('.bg-green-100').first();
await expect(mealBadge).toBeVisible();
await expect(mealBadge).toContainText('MEAL');
// Check for EVENT type badge (blue)
const eventBadge = page.locator('.bg-blue-100').first();
await expect(eventBadge).toBeVisible();
console.log('✓ Event type badges displaying correctly');
await page.screenshot({ path: 'test-results/event-type-badges.png', fullPage: true });
});
test('should navigate between event templates and common events', async ({ page }) => {
// Start at Event Templates
await page.goto('/event-templates');
await page.waitForLoadState('networkidle');
await expect(page.locator('h1:has-text("Event Templates")')).toBeVisible();
// Click navigation link to Common Events
await page.locator('a[href="/common-events"]').click();
await page.waitForLoadState('networkidle');
await expect(page.locator('h1:has-text("Common Events")')).toBeVisible();
// Navigate back to Event Templates
await page.locator('a[href="/event-templates"]').click();
await page.waitForLoadState('networkidle');
await expect(page.locator('h1:has-text("Event Templates")')).toBeVisible();
console.log('✓ Navigation between pages works correctly');
await page.screenshot({ path: 'test-results/event-navigation.png', fullPage: true });
});
test('should verify auto-created transport tasks link to events', async ({ page }) => {
// This test verifies that when we add VIPs to an event,
// the transport tasks are created and linked to the event
await page.goto('/common-events');
await page.waitForLoadState('networkidle');
// Get transport tasks count for Campfire Night (use heading to avoid matching button)
const campfireEvent = page.locator('h3:has-text("Campfire Night")').locator('..').locator('..').locator('..');
const transportCountText = await campfireEvent.locator('text=/Transport Tasks: \\d+/').first().textContent();
const transportCount = parseInt(transportCountText?.match(/\d+/)?.[0] || '0');
console.log(`Campfire Night has ${transportCount} transport tasks`);
// Now go to Schedule page and verify transport tasks exist for this event
await page.goto('/events');
await page.waitForLoadState('networkidle');
// Look for transport tasks with "Campfire Night" in the title
const campfireTransportTasks = page.locator('text=/Transport to Campfire Night/');
const taskCount = await campfireTransportTasks.count();
console.log(`Found ${taskCount} transport tasks for Campfire Night in schedule`);
if (taskCount > 0) {
console.log('✓ Transport tasks are linked to events correctly');
}
await page.screenshot({ path: 'test-results/linked-transport-tasks.png', fullPage: true });
});
});