- Fix billing components ConnectError type compatibility with exactOptionalPropertyTypes - Update Select component usage to match proper API (options vs children) - Remove unused imports and fix optional property assignments in system components - Resolve duplicate Order/Ticket type definitions and add null safety checks - Handle optional branding properties correctly in organization features - Add window property type declarations for test environment - Fix Playwright API usage (page.setOffline → page.context().setOffline) - Clean up unused imports, variables, and parameters across codebase - Add comprehensive global type declarations for test window extensions Resolves major TypeScript compilation issues and improves type safety throughout the application. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
933 lines
31 KiB
TypeScript
933 lines
31 KiB
TypeScript
/**
|
|
* Real-World Gate Scenarios - Advanced Field Testing
|
|
* Tests network handoff, background/foreground transitions, multi-device racing,
|
|
* rapid scanning rate limits, and QR code quality scenarios for actual gate operations
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('Network Handoff Scenarios', () => {
|
|
const testEventId = 'evt-001';
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
await page.setViewportSize({ width: 375, height: 667 }); // Mobile viewport
|
|
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');
|
|
await page.goto(`/scan?eventId=${testEventId}`);
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
|
|
test('should handle WiFi to cellular network transitions', async ({ page }) => {
|
|
// Simulate starting on WiFi
|
|
await expect(page.locator('text=Online')).toBeVisible();
|
|
|
|
// Set up network monitoring
|
|
await page.addInitScript(() => {
|
|
window.networkTransitionTest = {
|
|
connectionChanges: 0,
|
|
networkTypes: [],
|
|
lastSyncTime: null
|
|
};
|
|
|
|
// Monitor connection type changes
|
|
if ('connection' in navigator) {
|
|
const {connection} = navigator;
|
|
window.networkTransitionTest.networkTypes.push(connection.effectiveType);
|
|
|
|
connection.addEventListener('change', () => {
|
|
window.networkTransitionTest.connectionChanges++;
|
|
window.networkTransitionTest.networkTypes.push(connection.effectiveType);
|
|
});
|
|
}
|
|
|
|
// Monitor online/offline events
|
|
window.addEventListener('online', () => {
|
|
window.networkTransitionTest.lastSyncTime = Date.now();
|
|
});
|
|
});
|
|
|
|
// Simulate network quality changes (WiFi -> cellular)
|
|
await page.route('**/*', (route) => {
|
|
// Simulate slower cellular connection
|
|
setTimeout(() => route.continue(), Math.random() * 500 + 100);
|
|
});
|
|
|
|
// Perform scanning during network transition
|
|
for (let i = 0; i < 5; i++) {
|
|
await page.evaluate((qr) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now() }
|
|
}));
|
|
}, `NETWORK_TRANSITION_${i}`);
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
// Remove network delay
|
|
await page.unroute('**/*');
|
|
|
|
// Should remain online and functional
|
|
await expect(page.locator('text=Online')).toBeVisible();
|
|
|
|
const networkData = await page.evaluate(() => window.networkTransitionTest);
|
|
console.log('Network transition data:', networkData);
|
|
|
|
// Should handle network changes gracefully
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
|
|
test('should maintain sync during poor cellular conditions', async ({ page }) => {
|
|
// Simulate poor cellular network conditions
|
|
let requestCount = 0;
|
|
await page.route('**/api/**', (route) => {
|
|
requestCount++;
|
|
|
|
// Simulate cellular network issues
|
|
if (Math.random() < 0.3) { // 30% failure rate
|
|
route.abort();
|
|
} else {
|
|
// Slow but successful requests
|
|
setTimeout(() => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
valid: true,
|
|
message: 'Entry allowed',
|
|
latencyMs: 2500 + Math.random() * 1500
|
|
})
|
|
});
|
|
}, 1000 + Math.random() * 2000);
|
|
}
|
|
});
|
|
|
|
// Enable optimistic scanning for poor network
|
|
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
|
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
|
|
const isEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
|
|
if (!isEnabled) {await toggle.click();}
|
|
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
|
|
|
// Perform scanning under poor conditions
|
|
for (let i = 0; i < 10; i++) {
|
|
await page.evaluate((qr) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now() }
|
|
}));
|
|
}, `POOR_CELLULAR_${i}`);
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
await page.waitForTimeout(5000); // Allow retries to complete
|
|
|
|
console.log(`Total API requests made: ${requestCount}`);
|
|
|
|
// Should handle poor network gracefully
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
expect(requestCount).toBeGreaterThan(5); // Should have made retry attempts
|
|
});
|
|
|
|
test('should adapt to network quality changes', async ({ page }) => {
|
|
// Monitor network adaptation behavior
|
|
await page.addInitScript(() => {
|
|
window.networkAdaptation = {
|
|
qualityChanges: [],
|
|
adaptationMade: false
|
|
};
|
|
|
|
// Simulate network quality detection
|
|
function detectNetworkQuality() {
|
|
const startTime = performance.now();
|
|
|
|
fetch('/api/ping')
|
|
.then(() => {
|
|
const latency = performance.now() - startTime;
|
|
window.networkAdaptation.qualityChanges.push({
|
|
latency,
|
|
timestamp: Date.now(),
|
|
quality: latency < 100 ? 'fast' : latency < 500 ? 'medium' : 'slow'
|
|
});
|
|
|
|
if (latency > 1000) {
|
|
window.networkAdaptation.adaptationMade = true;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
window.networkAdaptation.qualityChanges.push({
|
|
latency: 999999,
|
|
timestamp: Date.now(),
|
|
quality: 'offline'
|
|
});
|
|
});
|
|
}
|
|
|
|
// Check network quality periodically
|
|
setInterval(detectNetworkQuality, 2000);
|
|
});
|
|
|
|
// Simulate varying network conditions
|
|
let responseDelay = 100;
|
|
await page.route('**/api/ping', (route) => {
|
|
setTimeout(() => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ pong: Date.now() })
|
|
});
|
|
}, responseDelay);
|
|
|
|
// Increase delay to simulate degrading network
|
|
responseDelay += 200;
|
|
});
|
|
|
|
await page.waitForTimeout(8000); // Let quality checks run
|
|
|
|
const adaptationData = await page.evaluate(() => window.networkAdaptation);
|
|
console.log('Network adaptation data:', adaptationData);
|
|
|
|
// Should detect quality changes
|
|
expect(adaptationData.qualityChanges.length).toBeGreaterThan(2);
|
|
|
|
// Scanner should remain functional
|
|
await expect(page.locator('text=Online')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Background/Foreground Transitions', () => {
|
|
const testEventId = 'evt-001';
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
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');
|
|
await page.goto(`/scan?eventId=${testEventId}`);
|
|
});
|
|
|
|
test('should handle app going to background during scanning', async ({ page }) => {
|
|
await expect(page.locator('video')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Set up app lifecycle monitoring
|
|
await page.addInitScript(() => {
|
|
window.appLifecycleTest = {
|
|
visibilityChanges: 0,
|
|
backgroundTime: 0,
|
|
cameraRestored: false,
|
|
scanningResumed: false
|
|
};
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
window.appLifecycleTest.visibilityChanges++;
|
|
|
|
if (document.visibilityState === 'hidden') {
|
|
window.appLifecycleTest.backgroundTime = Date.now();
|
|
} else if (document.visibilityState === 'visible') {
|
|
if (window.appLifecycleTest.backgroundTime > 0) {
|
|
const backgroundDuration = Date.now() - window.appLifecycleTest.backgroundTime;
|
|
window.appLifecycleTest.backgroundTime = backgroundDuration;
|
|
window.appLifecycleTest.cameraRestored = true;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Simulate scanning activity
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr: 'BACKGROUND_TEST_001', timestamp: Date.now() }
|
|
}));
|
|
});
|
|
|
|
// Simulate app going to background
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(document, 'visibilityState', {
|
|
writable: true,
|
|
value: 'hidden'
|
|
});
|
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
});
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Simulate returning to foreground
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(document, 'visibilityState', {
|
|
writable: true,
|
|
value: 'visible'
|
|
});
|
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
});
|
|
|
|
await page.waitForTimeout(3000); // Allow camera to reinitialize
|
|
|
|
// Camera should be restored
|
|
await expect(page.locator('video')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Should be able to scan again
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr: 'BACKGROUND_TEST_002', timestamp: Date.now() }
|
|
}));
|
|
});
|
|
|
|
const lifecycleData = await page.evaluate(() => window.appLifecycleTest);
|
|
console.log('App lifecycle data:', lifecycleData);
|
|
|
|
expect(lifecycleData.visibilityChanges).toBeGreaterThanOrEqual(2);
|
|
expect(lifecycleData.cameraRestored).toBe(true);
|
|
});
|
|
|
|
test('should preserve scan queue during background transitions', async ({ page }) => {
|
|
// Enable offline scanning
|
|
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
|
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
|
|
const isEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
|
|
if (!isEnabled) {await toggle.click();}
|
|
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
|
|
|
// Go offline and scan items
|
|
await page.context().setOffline(true);
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
|
|
window.dispatchEvent(new Event('offline'));
|
|
});
|
|
|
|
// Scan while offline
|
|
for (let i = 0; i < 3; i++) {
|
|
await page.evaluate((qr) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now() }
|
|
}));
|
|
}, `QUEUE_PRESERVE_${i}`);
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
// Go to background
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(document, 'visibilityState', {
|
|
writable: true,
|
|
value: 'hidden'
|
|
});
|
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
});
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Return to foreground
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(document, 'visibilityState', {
|
|
writable: true,
|
|
value: 'visible'
|
|
});
|
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
});
|
|
|
|
// Come back online
|
|
await page.context().setOffline(false);
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(navigator, 'onLine', { writable: true, value: true });
|
|
window.dispatchEvent(new Event('online'));
|
|
});
|
|
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Queue should be preserved and sync
|
|
const pendingElement = page.locator('text=Pending:').locator('..');
|
|
await expect(pendingElement).toBeVisible();
|
|
|
|
// Should show online status
|
|
await expect(page.locator('text=Online')).toBeVisible();
|
|
});
|
|
|
|
test('should handle wake lock across background transitions', async ({ page }) => {
|
|
const wakeLockTest = await page.evaluate(async () => {
|
|
if ('wakeLock' in navigator) {
|
|
try {
|
|
// Request wake lock
|
|
const wakeLock = await navigator.wakeLock.request('screen');
|
|
|
|
const initialState = !wakeLock.released;
|
|
|
|
// Simulate going to background
|
|
Object.defineProperty(document, 'visibilityState', {
|
|
writable: true,
|
|
value: 'hidden'
|
|
});
|
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
const backgroundState = !wakeLock.released;
|
|
|
|
// Return to foreground
|
|
Object.defineProperty(document, 'visibilityState', {
|
|
writable: true,
|
|
value: 'visible'
|
|
});
|
|
document.dispatchEvent(new Event('visibilitychange'));
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Try to re-request wake lock
|
|
let newWakeLock = null;
|
|
try {
|
|
newWakeLock = await navigator.wakeLock.request('screen');
|
|
} catch (e) {
|
|
console.log('Wake lock re-request failed:', e.message);
|
|
}
|
|
|
|
// Cleanup
|
|
if (!wakeLock.released) {wakeLock.release();}
|
|
if (newWakeLock && !newWakeLock.released) {newWakeLock.release();}
|
|
|
|
return {
|
|
supported: true,
|
|
initialState,
|
|
backgroundState,
|
|
reRequestSuccessful: !!newWakeLock
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
supported: true,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
return { supported: false };
|
|
});
|
|
|
|
console.log('Wake lock test:', wakeLockTest);
|
|
|
|
// Scanner should remain functional regardless
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Multi-Device Race Conditions', () => {
|
|
const testEventId = 'evt-001';
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
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');
|
|
await page.goto(`/scan?eventId=${testEventId}`);
|
|
});
|
|
|
|
test('should handle simultaneous scanning of same ticket', async ({ page }) => {
|
|
const duplicateTicket = 'RACE_CONDITION_TICKET_001';
|
|
|
|
// Mock race condition scenario
|
|
let scanAttempts = 0;
|
|
await page.route('**/api/scan/**', (route) => {
|
|
scanAttempts++;
|
|
|
|
if (scanAttempts === 1) {
|
|
// First scan succeeds
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
valid: true,
|
|
message: 'Entry allowed',
|
|
scannedAt: new Date().toISOString(),
|
|
device: 'device-1'
|
|
})
|
|
});
|
|
} else {
|
|
// Second scan shows already scanned
|
|
route.fulfill({
|
|
status: 409,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
valid: false,
|
|
reason: 'already_scanned',
|
|
message: 'Ticket already scanned by another device',
|
|
originalScanTime: new Date(Date.now() - 1000).toISOString(),
|
|
originalDevice: 'device-1'
|
|
})
|
|
});
|
|
}
|
|
});
|
|
|
|
// First scan attempt
|
|
await page.evaluate((qr) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now(), deviceId: 'scanner-device-2' }
|
|
}));
|
|
}, duplicateTicket);
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Second scan attempt (simulating another device scanned it first)
|
|
await page.evaluate((qr) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now(), deviceId: 'scanner-device-2' }
|
|
}));
|
|
}, duplicateTicket);
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
expect(scanAttempts).toBe(2);
|
|
|
|
// Scanner should handle race condition gracefully
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
|
|
test('should prevent double-scanning within rate limit window', async ({ page }) => {
|
|
const testTicket = 'DOUBLE_SCAN_PREVENT_001';
|
|
|
|
// Monitor scan attempts
|
|
await page.addInitScript(() => {
|
|
window.scanPrevention = {
|
|
scanAttempts: 0,
|
|
preventedScans: 0,
|
|
lastScanTime: 0
|
|
};
|
|
});
|
|
|
|
// First scan
|
|
await page.evaluate((data) => {
|
|
const now = Date.now();
|
|
window.scanPrevention.scanAttempts++;
|
|
window.scanPrevention.lastScanTime = now;
|
|
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr: data.qr, timestamp: now }
|
|
}));
|
|
}, { qr: testTicket });
|
|
|
|
await page.waitForTimeout(100);
|
|
|
|
// Rapid second scan (should be prevented)
|
|
await page.evaluate((data) => {
|
|
const now = Date.now();
|
|
const timeSinceLastScan = now - window.scanPrevention.lastScanTime;
|
|
|
|
if (timeSinceLastScan < 2000) { // Less than 2 seconds
|
|
window.scanPrevention.preventedScans++;
|
|
console.log('Preventing duplicate scan within rate limit');
|
|
return;
|
|
}
|
|
|
|
window.scanPrevention.scanAttempts++;
|
|
window.scanPrevention.lastScanTime = now;
|
|
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr: data.qr, timestamp: now }
|
|
}));
|
|
}, { qr: testTicket });
|
|
|
|
await page.waitForTimeout(100);
|
|
|
|
// Check prevention worked
|
|
const preventionData = await page.evaluate(() => window.scanPrevention);
|
|
console.log('Scan prevention data:', preventionData);
|
|
|
|
expect(preventionData.preventedScans).toBeGreaterThan(0);
|
|
expect(preventionData.scanAttempts).toBe(1); // Only one actual scan
|
|
});
|
|
|
|
test('should handle concurrent offline queue sync', async ({ page }) => {
|
|
// Create offline queue
|
|
await page.context().setOffline(true);
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(navigator, 'onLine', { writable: true, value: false });
|
|
window.dispatchEvent(new Event('offline'));
|
|
});
|
|
|
|
// Enable optimistic scanning
|
|
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
|
const toggle = page.locator('button.inline-flex').filter({ hasText: '' });
|
|
const isEnabled = await toggle.evaluate(el => el.classList.contains('bg-primary-500'));
|
|
if (!isEnabled) {await toggle.click();}
|
|
await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)');
|
|
|
|
// Scan multiple tickets offline
|
|
for (let i = 0; i < 5; i++) {
|
|
await page.evaluate((qr) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now() }
|
|
}));
|
|
}, `CONCURRENT_SYNC_${i}`);
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
// Mock concurrent sync handling
|
|
let syncRequestCount = 0;
|
|
await page.route('**/api/scan/batch', (route) => {
|
|
syncRequestCount++;
|
|
|
|
// Simulate processing delay
|
|
setTimeout(() => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
processed: 5,
|
|
successful: 5,
|
|
failed: 0,
|
|
conflicts: []
|
|
})
|
|
});
|
|
}, 1000);
|
|
});
|
|
|
|
// Come back online (trigger sync)
|
|
await page.context().setOffline(false);
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(navigator, 'onLine', { writable: true, value: true });
|
|
window.dispatchEvent(new Event('online'));
|
|
});
|
|
|
|
await page.waitForTimeout(5000);
|
|
|
|
// Should not make multiple concurrent sync requests
|
|
expect(syncRequestCount).toBeLessThanOrEqual(2);
|
|
|
|
await expect(page.locator('text=Online')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Rapid Scanning Rate Limits', () => {
|
|
const testEventId = 'evt-001';
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
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');
|
|
await page.goto(`/scan?eventId=${testEventId}`);
|
|
});
|
|
|
|
test('should enforce 8 scans per second rate limit', async ({ page }) => {
|
|
// Set up rate limit monitoring
|
|
await page.addInitScript(() => {
|
|
window.rateLimitMonitor = {
|
|
scansAttempted: 0,
|
|
scansProcessed: 0,
|
|
scansBlocked: 0,
|
|
rateLimitWarnings: 0,
|
|
scanTimes: []
|
|
};
|
|
});
|
|
|
|
// Attempt rapid scanning (faster than 8/second = 125ms interval)
|
|
const rapidScanCount = 20;
|
|
const scanInterval = 50; // 50ms = 20 scans/second, well over limit
|
|
|
|
for (let i = 0; i < rapidScanCount; i++) {
|
|
await page.evaluate((data) => {
|
|
const now = Date.now();
|
|
window.rateLimitMonitor.scansAttempted++;
|
|
window.rateLimitMonitor.scanTimes.push(now);
|
|
|
|
// Simulate rate limiting logic
|
|
const recentScans = window.rateLimitMonitor.scanTimes.filter(
|
|
time => now - time < 1000 // Last 1 second
|
|
);
|
|
|
|
if (recentScans.length > 8) {
|
|
window.rateLimitMonitor.scansBlocked++;
|
|
window.rateLimitMonitor.rateLimitWarnings++;
|
|
console.log('Rate limit exceeded - scan blocked');
|
|
return;
|
|
}
|
|
|
|
window.rateLimitMonitor.scansProcessed++;
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr: data.qr, timestamp: now }
|
|
}));
|
|
}, { qr: `RATE_LIMIT_${i}` });
|
|
|
|
await page.waitForTimeout(scanInterval);
|
|
}
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
const rateLimitData = await page.evaluate(() => window.rateLimitMonitor);
|
|
console.log('Rate limit data:', rateLimitData);
|
|
|
|
// Should have blocked excessive scans
|
|
expect(rateLimitData.scansBlocked).toBeGreaterThan(0);
|
|
expect(rateLimitData.scansProcessed).toBeLessThan(rateLimitData.scansAttempted);
|
|
expect(rateLimitData.rateLimitWarnings).toBeGreaterThan(0);
|
|
|
|
// Should not process more than ~8 scans per second
|
|
expect(rateLimitData.scansProcessed).toBeLessThan(15);
|
|
});
|
|
|
|
test('should show "slow down" message for excessive scanning', async ({ page }) => {
|
|
// Monitor UI feedback for rate limiting
|
|
await page.addInitScript(() => {
|
|
window.rateLimitUI = {
|
|
warningsShown: 0,
|
|
lastWarningTime: 0
|
|
};
|
|
|
|
// Listen for rate limit events
|
|
window.addEventListener('rate-limit-warning', () => {
|
|
window.rateLimitUI.warningsShown++;
|
|
window.rateLimitUI.lastWarningTime = Date.now();
|
|
});
|
|
});
|
|
|
|
// Rapidly attempt scans
|
|
for (let i = 0; i < 15; i++) {
|
|
await page.evaluate((qr) => {
|
|
// Trigger rate limit warning
|
|
if (Math.random() < 0.3) { // 30% chance of warning
|
|
window.dispatchEvent(new Event('rate-limit-warning'));
|
|
}
|
|
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now() }
|
|
}));
|
|
}, `RAPID_SCAN_${i}`);
|
|
|
|
await page.waitForTimeout(30); // Very rapid
|
|
}
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
const uiData = await page.evaluate(() => window.rateLimitUI);
|
|
console.log('Rate limit UI data:', uiData);
|
|
|
|
// Should show warnings for excessive scanning
|
|
if (uiData.warningsShown > 0) {
|
|
expect(uiData.warningsShown).toBeGreaterThan(0);
|
|
}
|
|
|
|
// Scanner should remain functional
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
|
|
test('should recover from rate limiting', async ({ page }) => {
|
|
// Trigger rate limiting
|
|
for (let i = 0; i < 12; i++) {
|
|
await page.evaluate((qr) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now() }
|
|
}));
|
|
}, `RATE_LIMIT_TRIGGER_${i}`);
|
|
await page.waitForTimeout(50);
|
|
}
|
|
|
|
// Wait for rate limit to reset
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Should accept scans normally again
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr: 'RATE_LIMIT_RECOVERY_TEST', timestamp: Date.now() }
|
|
}));
|
|
});
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Scanner should be fully functional
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
await expect(page.locator('video')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('QR Code Quality and Edge Cases', () => {
|
|
const testEventId = 'evt-001';
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
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');
|
|
await page.goto(`/scan?eventId=${testEventId}`);
|
|
});
|
|
|
|
test('should handle various QR code formats and sizes', async ({ page }) => {
|
|
const qrCodeVariants = [
|
|
'STANDARD_TICKET_12345678', // Standard format
|
|
'BCT-EVT001-TKT-987654321', // BCT format with dashes
|
|
'{"ticket":"123","event":"evt-001"}', // JSON format
|
|
'https://bct.com/verify/abc123', // URL format
|
|
'VERY_LONG_TICKET_CODE_WITH_MANY_CHARACTERS_1234567890', // Long format
|
|
'123', // Very short
|
|
'TICKET_WITH_SPECIAL_CHARS_@#$%', // Special characters
|
|
'ticket_lowercase_123', // Lowercase
|
|
'TICKET_WITH_UNICODE_ñáéíóú_123' // Unicode characters
|
|
];
|
|
|
|
// Test each QR code variant
|
|
for (const qrCode of qrCodeVariants) {
|
|
await page.evaluate((qr) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: {
|
|
qr,
|
|
timestamp: Date.now(),
|
|
format: qr.length > 30 ? 'long' : qr.startsWith('{') ? 'json' : 'standard'
|
|
}
|
|
}));
|
|
}, qrCode);
|
|
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
// Scanner should handle all formats gracefully
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
|
|
test('should handle damaged or partial QR code reads', async ({ page }) => {
|
|
const corruptedQRCodes = [
|
|
'PARTIAL_SCAN_', // Incomplete scan
|
|
'CORRUPT_QR_?@#$INVALID', // Contains invalid characters
|
|
'', // Empty scan
|
|
' ', // Whitespace only
|
|
'TICKET_WITH\nNEWLINE', // Contains newline
|
|
'TICKET_WITH\tTAB', // Contains tab
|
|
'TICKET_WITH\0NULL', // Contains null character
|
|
];
|
|
|
|
// Set up error handling monitoring
|
|
await page.addInitScript(() => {
|
|
window.qrErrorHandling = {
|
|
invalidScans: 0,
|
|
emptyScans: 0,
|
|
handledGracefully: 0
|
|
};
|
|
});
|
|
|
|
for (const qrCode of corruptedQRCodes) {
|
|
await page.evaluate((qr) => {
|
|
// Simulate QR validation logic
|
|
if (!qr || qr.trim().length === 0) {
|
|
window.qrErrorHandling.emptyScans++;
|
|
console.log('Empty QR code detected');
|
|
return;
|
|
}
|
|
|
|
if (qr.includes('\n') || qr.includes('\t') || qr.includes('\0')) {
|
|
window.qrErrorHandling.invalidScans++;
|
|
console.log('Invalid QR code format detected');
|
|
return;
|
|
}
|
|
|
|
window.qrErrorHandling.handledGracefully++;
|
|
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: { qr, timestamp: Date.now(), quality: 'poor' }
|
|
}));
|
|
}, qrCode);
|
|
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
const errorData = await page.evaluate(() => window.qrErrorHandling);
|
|
console.log('QR error handling data:', errorData);
|
|
|
|
// Should detect and handle invalid QR codes
|
|
expect(errorData.emptyScans + errorData.invalidScans).toBeGreaterThan(0);
|
|
|
|
// Scanner should remain stable despite invalid inputs
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
|
|
test('should adapt to different lighting conditions', async ({ page }) => {
|
|
// Test torch functionality for low light
|
|
const torchButton = page.locator('button:has([data-testid="flashlight-icon"]), button:has(.lucide-flashlight)');
|
|
|
|
if (await torchButton.isVisible()) {
|
|
// Simulate low light scanning
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new CustomEvent('lighting-change', {
|
|
detail: { condition: 'low-light', brightness: 0.2 }
|
|
}));
|
|
});
|
|
|
|
// Enable torch for better scanning
|
|
await torchButton.tap();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Test scanning in low light with torch
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: {
|
|
qr: 'LOW_LIGHT_SCAN_123',
|
|
timestamp: Date.now(),
|
|
conditions: { lighting: 'low', torch: true }
|
|
}
|
|
}));
|
|
});
|
|
|
|
await page.waitForTimeout(300);
|
|
|
|
// Simulate bright light
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new CustomEvent('lighting-change', {
|
|
detail: { condition: 'bright-light', brightness: 0.9 }
|
|
}));
|
|
});
|
|
|
|
// Test scanning in bright light
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: {
|
|
qr: 'BRIGHT_LIGHT_SCAN_123',
|
|
timestamp: Date.now(),
|
|
conditions: { lighting: 'bright', torch: false }
|
|
}
|
|
}));
|
|
});
|
|
|
|
// Disable torch in bright conditions
|
|
await torchButton.tap();
|
|
}
|
|
|
|
// Scanner should adapt to lighting changes
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
|
|
test('should handle different QR code angles and distances', async ({ page }) => {
|
|
const scanAngles = [
|
|
{ angle: 0, distance: 'optimal', quality: 'high' },
|
|
{ angle: 15, distance: 'close', quality: 'medium' },
|
|
{ angle: 30, distance: 'far', quality: 'low' },
|
|
{ angle: 45, distance: 'very-close', quality: 'poor' },
|
|
{ angle: -15, distance: 'medium', quality: 'medium' }
|
|
];
|
|
|
|
for (const scan of scanAngles) {
|
|
await page.evaluate((scanData) => {
|
|
window.dispatchEvent(new CustomEvent('mock-scan', {
|
|
detail: {
|
|
qr: `ANGLE_TEST_${scanData.angle}_${scanData.distance}`,
|
|
timestamp: Date.now(),
|
|
scanConditions: scanData
|
|
}
|
|
}));
|
|
}, scan);
|
|
|
|
await page.waitForTimeout(400);
|
|
}
|
|
|
|
// Should handle various scan conditions
|
|
await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible();
|
|
});
|
|
}); |