- 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>
445 lines
15 KiB
TypeScript
445 lines
15 KiB
TypeScript
/**
|
|
* 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();
|
|
}
|
|
});
|
|
}); |