Files
vip-coordinator/frontend/e2e/admin-test-data.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

289 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
test.describe('Admin Test Data Management', () => {
test.beforeEach(async ({ page }) => {
test.setTimeout(600000); // 10 minutes - test data creation can take a while with rate limiting
// Login
await page.goto('http://localhost:5173/login');
await page.locator('button:has-text("Sign in with Auth0")').click();
await page.waitForTimeout(2000);
await page.locator('input[name="username"]').fill('test@test.com');
await page.locator('input[name="password"]').fill('P@ssw0rd!');
await page.locator('button[type="submit"][data-action-button-primary="true"]').click();
await page.waitForURL('**/dashboard', { timeout: 30000 });
console.log('✅ Logged in successfully');
});
test('should load all test data via Admin menu and verify in Events view', async ({ page }) => {
console.log('\n🔧 Testing Admin Test Data Management');
// Navigate to Admin Tools
console.log('📋 Navigating to Admin Tools page');
await page.goto('http://localhost:5173/admin-tools', { waitUntil: 'networkidle' });
// Force reload to ensure latest JavaScript is loaded
await page.reload({ waitUntil: 'networkidle' });
await page.screenshot({ path: 'test-results/admin-01-tools-page.png', fullPage: true });
// Verify Admin Tools page loaded
const pageTitle = page.locator('h1:has-text("Administrator Tools")');
await expect(pageTitle).toBeVisible();
console.log('✅ Admin Tools page loaded');
// Step 1: Add Test VIPs
console.log('\n👥 Adding Test VIPs...');
const addVipsButton = page.locator('button:has-text("Add Test VIPs")');
await addVipsButton.scrollIntoViewIfNeeded();
await expect(addVipsButton).toBeVisible();
// Setup dialog handler for confirmation
page.on('dialog', async dialog => {
console.log(`📢 Confirmation: ${dialog.message()}`);
await dialog.accept();
});
await addVipsButton.click();
// Wait for success toast
await page.waitForSelector('text=/Added \\d+ test VIPs/', { timeout: 60000 });
console.log('✅ Test VIPs added successfully');
await page.waitForTimeout(1000);
await page.screenshot({ path: 'test-results/admin-02-vips-added.png', fullPage: true });
// Step 2: Add Test Drivers
console.log('\n🚗 Adding Test Drivers...');
const addDriversButton = page.locator('button:has-text("Add Test Drivers")');
await expect(addDriversButton).toBeVisible();
await addDriversButton.click();
await page.waitForSelector('text=/Added .* test [Dd]rivers?/', { timeout: 30000 });
console.log('✅ Test Drivers added successfully');
await page.waitForTimeout(1000);
await page.screenshot({ path: 'test-results/admin-03-drivers-added.png', fullPage: true });
// Step 3: Add Test Vehicles
console.log('\n🚐 Adding Test Vehicles...');
const addVehiclesButton = page.locator('button:has-text("Add Test Vehicles")');
await expect(addVehiclesButton).toBeVisible();
await addVehiclesButton.click();
await page.waitForSelector('text=/Added .* test [Vv]ehicles?/', { timeout: 30000 });
console.log('✅ Test Vehicles added successfully');
await page.waitForTimeout(1000);
await page.screenshot({ path: 'test-results/admin-04-vehicles-added.png', fullPage: true });
// Step 4: Add Test Schedule
console.log('\n📅 Adding Test Schedule Events...');
const addScheduleButton = page.locator('button:has-text("Add Test Schedule")');
await expect(addScheduleButton).toBeVisible();
await addScheduleButton.click();
// Wait for schedule creation to complete by polling the stats
// This can take a while as it creates many events
console.log('⏳ Waiting for schedule events to be created...');
let scheduleEventCount = 0;
for (let i = 0; i < 60; i++) { // Poll for up to 2 minutes
await page.waitForTimeout(2000);
// Click refresh button to update stats
const refreshButton = page.locator('button:has-text("Refresh")');
if (await refreshButton.isVisible({ timeout: 1000 })) {
await refreshButton.click();
await page.waitForTimeout(500);
}
// Check event count
const statsSection = page.locator('text=Database Statistics').locator('..');
const eventsStat = statsSection.locator('text=/Events/').locator('..');
const eventCountText = await eventsStat.locator('.text-3xl, .text-2xl, .text-xl').first().textContent();
scheduleEventCount = parseInt(eventCountText || '0');
if (scheduleEventCount > 0) {
console.log(`✅ Schedule created: ${scheduleEventCount} events found`);
break;
}
if (i % 5 === 0) {
console.log(`⏳ Still waiting... (${i * 2}s elapsed)`);
}
}
if (scheduleEventCount === 0) {
throw new Error('Schedule creation timed out - no events created after 2 minutes');
}
await page.waitForTimeout(1000);
await page.screenshot({ path: 'test-results/admin-05-schedule-added.png', fullPage: true });
// Step 5: Verify database statistics updated
console.log('\n📊 Verifying database statistics...');
// Refresh the page to get updated stats
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.screenshot({ path: 'test-results/admin-06-stats-updated.png', fullPage: true });
// Try to verify stats if visible
try {
const statsSection = page.locator('text=Database Statistics').locator('..');
if (await statsSection.isVisible({ timeout: 3000 })) {
console.log('✅ Database statistics section found');
// Look for any number indicators that show counts
const numbers = await statsSection.locator('.text-3xl, .text-2xl, .text-xl').allTextContents();
console.log(`📊 Stats visible: ${numbers.join(', ')}`);
} else {
console.log(' Stats section not found - will verify via Events page');
}
} catch (e) {
console.log(' Could not verify stats - will check Events page instead');
}
// Step 6: Navigate to Events view
console.log('\n📅 Navigating to Events view...');
await page.goto('http://localhost:5173/events');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.screenshot({ path: 'test-results/admin-07-events-page.png', fullPage: true });
// Verify Events page loaded
const eventsPageTitle = page.locator('h1, h2').filter({ hasText: /Events|Schedule/ });
await expect(eventsPageTitle).toBeVisible();
console.log('✅ Events page loaded');
// Step 7: Verify events are displayed
console.log('\n🔍 Verifying events are displayed...');
// Wait for table to load
await page.waitForSelector('table, .event-list, .event-card', { timeout: 10000 });
// Count event rows (could be in a table or as cards)
let eventCount = 0;
// Try to find table rows first
const tableRows = await page.locator('tbody tr').count();
if (tableRows > 0) {
eventCount = tableRows;
console.log(`📊 Found ${eventCount} events in table`);
} else {
// Try to find event cards
const eventCards = await page.locator('.event-card, [class*="event"]').count();
eventCount = eventCards;
console.log(`📊 Found ${eventCount} event cards`);
}
// Verify we have events
expect(eventCount).toBeGreaterThan(0);
console.log(`✅ Events displayed: ${eventCount} events found`);
// Step 8: Verify event details show VIPs, drivers, vehicles
console.log('\n🔍 Verifying event details...');
if (tableRows > 0) {
// Check first event row has data
const firstRow = page.locator('tbody tr').first();
await expect(firstRow).toBeVisible();
// Try to find VIP names
const vipCell = firstRow.locator('td').nth(1); // Usually second column
const vipCellText = await vipCell.textContent();
console.log(`📋 Sample VIP data: ${vipCellText?.substring(0, 50)}...`);
// Try to find vehicle info
const vehicleCell = firstRow.locator('td').nth(2); // Usually third column
const vehicleCellText = await vehicleCell.textContent();
console.log(`🚗 Sample Vehicle data: ${vehicleCellText?.substring(0, 50)}...`);
// Try to find driver info
const driverCell = firstRow.locator('td').nth(3); // Usually fourth column
const driverCellText = await driverCell.textContent();
console.log(`👤 Sample Driver data: ${driverCellText?.substring(0, 50)}...`);
// Verify cells have content (not empty)
expect(vipCellText).not.toBe('');
expect(vehicleCellText).not.toBe('');
expect(driverCellText).not.toBe('');
console.log('✅ Event details contain VIP, Vehicle, and Driver data');
}
await page.screenshot({ path: 'test-results/admin-08-events-verified.png', fullPage: true });
// Step 9: Verify multi-VIP events display correctly
console.log('\n👥 Checking for multi-VIP events...');
// Look for comma-separated VIP names (indicates multi-VIP event)
const multiVipRows = await page.locator('tbody tr').filter({ hasText: /,/ }).count();
console.log(`📊 Found ${multiVipRows} rows with comma-separated data (likely multi-VIP events)`);
if (multiVipRows > 0) {
const firstMultiVipRow = page.locator('tbody tr').filter({ hasText: /,/ }).first();
const vipCell = firstMultiVipRow.locator('td').nth(1);
const vipNames = await vipCell.textContent();
console.log(`👥 Sample multi-VIP event: ${vipNames?.substring(0, 100)}...`);
console.log('✅ Multi-VIP events displaying correctly');
} else {
console.log(' No obvious multi-VIP events detected (may be using badges/pills instead of commas)');
}
console.log('\n🎉 Test Data Load and Verification Complete!');
console.log(`${eventCount} events verified in Events view`);
});
test('should show error handling for database operations', async ({ page }) => {
console.log('\n⚠ Testing error handling (optional validation)');
await page.goto('http://localhost:5173/admin-tools');
await page.waitForLoadState('networkidle');
// Try clicking Clear All Data button if it exists
const clearButton = page.locator('button:has-text("Clear All Data"), button:has-text("Delete All")');
if (await clearButton.isVisible({ timeout: 2000 })) {
console.log('🗑️ Found Clear All Data button');
// Setup dialog handler for confirmation
page.on('dialog', async dialog => {
console.log(`📢 Confirmation dialog: ${dialog.message()}`);
await dialog.accept();
});
await page.screenshot({ path: 'test-results/admin-09-before-clear.png', fullPage: true });
await clearButton.click();
await page.waitForTimeout(3000);
await page.screenshot({ path: 'test-results/admin-10-after-clear.png', fullPage: true });
console.log('✅ Clear operation completed');
// Verify data is cleared by checking Events page
await page.goto('http://localhost:5173/events');
await page.waitForLoadState('networkidle');
const eventRows = await page.locator('tbody tr').count();
console.log(`📊 Event count after clear: ${eventRows}`);
if (eventRows === 0) {
console.log('✅ All events cleared successfully');
} else {
console.log(` ${eventRows} events still present (may be seed data)`);
}
} else {
console.log(' No Clear All Data button found - skipping clear test');
}
});
});