feat: add advanced analytics and territory management system

- Add comprehensive analytics components with export functionality
- Implement territory management with manager performance tracking
- Add seatmap components for venue layout management
- Create customer management features with modal interface
- Add advanced hooks for dashboard flags and territory data
- Implement seat selection and venue management utilities
- Add type definitions for ticketing and seatmap systems

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-26 09:25:10 -06:00
parent d5c3953888
commit aa81eb5adb
438 changed files with 90509 additions and 2787 deletions

View File

@@ -0,0 +1,445 @@
/**
* PWA Field Tests - Scanner PWA Installation and Core Functionality
* Tests PWA installation, manifest loading, service worker registration, and core PWA features
*/
import { test, expect } from '@playwright/test';
test.describe('PWA Installation Tests', () => {
const testEventId = 'evt-001';
test.beforeEach(async ({ page, context }) => {
// Grant all necessary permissions for PWA testing
await context.grantPermissions(['camera', 'microphone', 'notifications']);
// Login as staff user who has scan permissions
await page.goto('/login');
await page.fill('[name="email"]', 'staff@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
});
test('should load PWA manifest correctly', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Check manifest is linked in the head
const manifestLink = await page.locator('link[rel="manifest"]').getAttribute('href');
expect(manifestLink).toBe('/manifest.json');
// Fetch and validate manifest content
const manifestResponse = await page.request.get('/manifest.json');
expect(manifestResponse.status()).toBe(200);
const manifest = await manifestResponse.json();
expect(manifest.name).toBe('BCT Scanner - Black Canyon Tickets');
expect(manifest.short_name).toBe('BCT Scanner');
expect(manifest.start_url).toBe('/scan');
expect(manifest.display).toBe('standalone');
expect(manifest.background_color).toBe('#0f0f23');
expect(manifest.theme_color).toBe('#6366f1');
expect(manifest.orientation).toBe('portrait');
// Verify PWA features
expect(manifest.features).toContain('camera');
expect(manifest.features).toContain('offline');
expect(manifest.features).toContain('background-sync');
expect(manifest.features).toContain('vibration');
// Verify icons are properly configured
expect(manifest.icons).toHaveLength(8);
expect(manifest.icons.some(icon => icon.purpose === 'maskable')).toBe(true);
// Verify shortcuts
expect(manifest.shortcuts).toHaveLength(1);
expect(manifest.shortcuts[0].url).toBe('/scan');
});
test('should register service worker', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Wait for service worker registration
await page.waitForFunction(() => 'serviceWorker' in navigator);
const swRegistration = await page.evaluate(async () => {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.getRegistration();
return {
exists: !!registration,
scope: registration?.scope,
state: registration?.active?.state
};
}
return { exists: false };
});
expect(swRegistration.exists).toBe(true);
expect(swRegistration.scope).toContain(page.url().split('/').slice(0, 3).join('/'));
});
test('should detect offline capability', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Check for offline indicators in the UI
await expect(page.locator('text=Online')).toBeVisible();
// Test offline detection API
const offlineCapable = await page.evaluate(() => 'onLine' in navigator && 'serviceWorker' in navigator);
expect(offlineCapable).toBe(true);
});
test('should handle camera permissions in PWA context', async ({ page, context }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Camera should be accessible
await expect(page.locator('video')).toBeVisible({ timeout: 10000 });
// Check camera permission status
const cameraPermission = await page.evaluate(async () => {
try {
const permission = await navigator.permissions.query({ name: 'camera' as any });
return permission.state;
} catch {
return 'unknown';
}
});
expect(['granted', 'prompt']).toContain(cameraPermission);
});
test('should support Add to Home Screen on mobile viewports', async ({ page, browserName }) => {
// Test on mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto(`/scan?eventId=${testEventId}`);
// Check for PWA install prompt capability
const installable = await page.evaluate(() =>
// Check if beforeinstallprompt event can be triggered
'onbeforeinstallprompt' in window
);
// Note: Actual install prompt testing is limited in automated tests
// but we can verify the PWA infrastructure is in place
const manifestPresent = await page.locator('link[rel="manifest"]').count();
expect(manifestPresent).toBeGreaterThan(0);
// Verify viewport meta tag for mobile
const viewportMeta = await page.locator('meta[name="viewport"]').getAttribute('content');
expect(viewportMeta).toContain('width=device-width');
});
test('should load correctly when launched as PWA', async ({ page }) => {
// Simulate PWA launch by setting display-mode
await page.addInitScript(() => {
// Mock PWA display mode
Object.defineProperty(window, 'matchMedia', {
value: (query: string) => ({
matches: query.includes('display-mode: standalone'),
addEventListener: () => {},
removeEventListener: () => {}
})
});
});
await page.goto(`/scan?eventId=${testEventId}`);
// PWA should launch directly to scanner
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
// Should not show browser UI elements in standalone mode
// This is more of a visual check that would be done manually
});
});
test.describe('PWA Storage and Caching', () => {
const testEventId = 'evt-001';
test.beforeEach(async ({ page, context }) => {
await context.grantPermissions(['camera']);
await page.goto('/login');
await page.fill('[name="email"]', 'staff@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
});
test('should cache critical resources for offline use', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Let page load completely
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
await page.waitForTimeout(2000);
// Check cache storage exists
const cacheExists = await page.evaluate(async () => {
if ('caches' in window) {
const cacheNames = await caches.keys();
return cacheNames.length > 0;
}
return false;
});
// Service worker should have cached resources
expect(cacheExists).toBe(true);
});
test('should use IndexedDB for scan queue storage', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Check IndexedDB availability
const indexedDBSupported = await page.evaluate(() => 'indexedDB' in window);
expect(indexedDBSupported).toBe(true);
// Verify scan queue database can be created
const dbCreated = await page.evaluate(async () => new Promise((resolve) => {
const request = indexedDB.open('bct-scanner-queue', 1);
request.onsuccess = () => resolve(true);
request.onerror = () => resolve(false);
setTimeout(() => resolve(false), 3000);
}));
expect(dbCreated).toBe(true);
});
test('should persist scanner settings across sessions', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Open settings and configure
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
await page.fill('input[placeholder*="Gate"]', 'Gate A - Field Test');
// Toggle optimistic accept
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
await toggle.click();
// Reload page
await page.reload();
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
// Check settings persisted
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
await expect(page.locator('input[placeholder*="Gate"]')).toHaveValue('Gate A - Field Test');
});
});
test.describe('PWA Network Awareness', () => {
const testEventId = 'evt-001';
test.beforeEach(async ({ page, context }) => {
await context.grantPermissions(['camera']);
await page.goto('/login');
await page.fill('[name="email"]', 'staff@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
});
test('should detect online/offline status changes', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Should start online
await expect(page.locator('text=Online')).toBeVisible();
// Simulate going offline
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: false
});
window.dispatchEvent(new Event('offline'));
});
// Should show offline status
await expect(page.locator('text=Offline')).toBeVisible({ timeout: 5000 });
// Simulate coming back online
await page.evaluate(() => {
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: true
});
window.dispatchEvent(new Event('online'));
});
// Should show online status again
await expect(page.locator('text=Online')).toBeVisible({ timeout: 5000 });
});
test('should handle background sync registration', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Check if background sync is supported
const backgroundSyncSupported = await page.evaluate(async () => {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.ready;
return 'sync' in registration;
}
return false;
});
// Background sync support varies by browser
// Chrome supports it, Safari/Firefox may not
if (backgroundSyncSupported) {
expect(backgroundSyncSupported).toBe(true);
}
});
test('should show network quality indicators', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Look for connection quality indicators
// This would typically show latency or connection strength
await expect(page.locator('text=Online')).toBeVisible();
// Test slow network simulation
await page.route('**/*', (route) => {
// Simulate slow network by delaying responses
setTimeout(() => route.continue(), 1000);
});
// Reload to trigger slow network
await page.reload();
// Should still load but may show slower response times
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible({ timeout: 15000 });
});
});
test.describe('PWA Platform Integration', () => {
const testEventId = 'evt-001';
test.beforeEach(async ({ page, context }) => {
await context.grantPermissions(['camera', 'notifications']);
await page.goto('/login');
await page.fill('[name="email"]', 'staff@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
});
test('should support vibration API for scan feedback', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Check vibration API support
const vibrationSupported = await page.evaluate(() => 'vibrate' in navigator);
if (vibrationSupported) {
// Test vibration pattern
const vibrationWorked = await page.evaluate(() => {
try {
navigator.vibrate([100, 50, 100]);
return true;
} catch {
return false;
}
});
expect(vibrationWorked).toBe(true);
}
});
test('should support notification API for alerts', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Check notification permission
const notificationPermission = await page.evaluate(async () => {
if ('Notification' in window) {
return Notification.permission;
}
return 'unsupported';
});
expect(['granted', 'denied', 'default']).toContain(notificationPermission);
if (notificationPermission === 'granted') {
// Test notification creation
const notificationCreated = await page.evaluate(() => {
try {
new Notification('Test Scanner Notification', {
body: 'Field test notification',
icon: '/icon-96x96.png',
tag: 'field-test'
});
return true;
} catch {
return false;
}
});
expect(notificationCreated).toBe(true);
}
});
test('should handle device orientation changes', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Test portrait mode (default)
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
// Simulate orientation change to landscape
await page.setViewportSize({ width: 667, height: 375 });
// Should still be usable in landscape
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
await expect(page.locator('video')).toBeVisible();
// Settings should still be accessible
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
await expect(page.locator('text=Gate/Zone')).toBeVisible();
});
test('should support wake lock API to prevent screen sleep', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Check wake lock API support
const wakeLockSupported = await page.evaluate(() => 'wakeLock' in navigator);
if (wakeLockSupported) {
// Test wake lock request
const wakeLockRequested = await page.evaluate(async () => {
try {
const wakeLock = await navigator.wakeLock.request('screen');
wakeLock.release();
return true;
} catch {
return false;
}
});
expect(wakeLockRequested).toBe(true);
}
});
test('should handle visibility API for background/foreground transitions', async ({ page }) => {
await page.goto(`/scan?eventId=${testEventId}`);
// Check page visibility API support
const visibilitySupported = await page.evaluate(() => typeof document.visibilityState !== 'undefined');
expect(visibilitySupported).toBe(true);
if (visibilitySupported) {
// Simulate page going to background
await page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', {
writable: true,
value: 'hidden'
});
document.dispatchEvent(new Event('visibilitychange'));
});
await page.waitForTimeout(500);
// Simulate page coming back to foreground
await page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', {
writable: true,
value: 'visible'
});
document.dispatchEvent(new Event('visibilitychange'));
});
// Scanner should still be functional
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
}
});
});