/** * 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(); } }); });