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:
445
reactrebuild0825/tests/pwa-field-test.spec.ts
Normal file
445
reactrebuild0825/tests/pwa-field-test.spec.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user