/** * Battery & Performance Tests - Extended Scanner Usage Testing * Tests 15-minute continuous scanning, thermal throttling simulation, * battery usage monitoring, and memory leak detection for extended gate operations */ /// import { test, expect } from '@playwright/test'; test.describe('Extended Continuous Scanning', () => { 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'); await page.goto(`/scan?eventId=${testEventId}`); await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); }); test('should handle 15-minute continuous scanning session', async ({ page }) => { test.setTimeout(60000); // Set up performance monitoring await page.addInitScript(() => { window.performanceMetrics = { startTime: performance.now(), memoryUsage: [], frameRates: [], scanCounts: 0, errors: [] }; // Monitor memory usage every 30 seconds setInterval(() => { if (performance.memory) { window.performanceMetrics!.memoryUsage.push({ used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize, limit: performance.memory.jsHeapSizeLimit, timestamp: performance.now() }); } }, 30000); // Monitor frame rate let frames = 0; let lastTime = performance.now(); function measureFPS() { frames++; const now = performance.now(); if (now - lastTime >= 1000) { const fps = Math.round((frames * 1000) / (now - lastTime)); window.performanceMetrics!.frameRates.push({ fps, timestamp: now }); frames = 0; lastTime = now; } requestAnimationFrame(measureFPS); } measureFPS(); }); // Configure scanner settings await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)'); await page.fill('input[placeholder*="Gate"]', 'Gate A - Endurance Test'); await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)'); // Simulate 15 minutes of continuous scanning (compressed to 30 seconds for testing) const testDurationMs = 30000; // 30 seconds for test, represents 15 minutes const scanInterval = 100; // Scan every 100ms const totalScans = Math.floor(testDurationMs / scanInterval); console.log(`Starting endurance test: ${totalScans} scans over ${testDurationMs}ms`); const startTime = Date.now(); let scanCount = 0; const scanTimer = setInterval(async () => { scanCount++; const qrCode = `ENDURANCE_TICKET_${scanCount.toString().padStart(4, '0')}`; await page.evaluate((data: { qr: string; scanCount: number }) => { window.performanceMetrics!.scanCounts++; window.dispatchEvent(new CustomEvent('mock-scan', { detail: { qr: data.qr, timestamp: Date.now(), scanNumber: data.scanCount } })); }, { qr: qrCode, scanCount }); // Stop after test duration if (Date.now() - startTime >= testDurationMs) { clearInterval(scanTimer); } }, scanInterval); // Wait for test completion await page.waitForTimeout(testDurationMs + 5000); // Collect performance metrics const metrics = await page.evaluate(() => window.performanceMetrics!); console.log(`Endurance test completed: ${metrics.scanCounts} scans processed`); // Verify scanner still responsive after endurance test await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); await expect(page.locator('video')).toBeVisible(); // Check memory usage didn't grow excessively if (metrics.memoryUsage.length > 1) { const initialMemory = metrics.memoryUsage[0]!.used; const finalMemory = metrics.memoryUsage[metrics.memoryUsage.length - 1]!.used; const memoryGrowth = finalMemory - initialMemory; const memoryGrowthMB = memoryGrowth / (1024 * 1024); console.log(`Memory growth: ${memoryGrowthMB.toFixed(2)} MB`); // Memory growth should be reasonable (less than 50MB for this test) expect(memoryGrowthMB).toBeLessThan(50); } // Check frame rates remained reasonable if (metrics.frameRates.length > 0) { const avgFPS = metrics.frameRates.reduce((sum: number, r: { fps: number }) => sum + r.fps, 0) / metrics.frameRates.length; console.log(`Average FPS: ${avgFPS.toFixed(1)}`); // Should maintain at least 15 FPS for usable performance expect(avgFPS).toBeGreaterThan(15); } // Verify scan counter accuracy expect(metrics.scanCounts).toBeGreaterThan(100); // Should have processed many scans }); test('should maintain performance under rapid scanning load', async ({ page }) => { // Test rapid scanning (simulating very busy gate) await page.addInitScript(() => { window.rapidScanMetrics = { processedScans: 0, droppedScans: 0, averageProcessingTime: [] }; }); // Simulate very rapid scanning - 10 scans per second for 10 seconds const rapidScanCount = 100; const scanDelay = 100; // 100ms = 10 scans per second for (let i = 0; i < rapidScanCount; i++) { // Monitor performance during rapid scanning await page.evaluate((data) => { const processingStart = performance.now(); window.dispatchEvent(new CustomEvent('mock-scan', { detail: { qr: data.qr, timestamp: Date.now() } })); // Simulate processing time measurement setTimeout(() => { const processingTime = performance.now() - processingStart; window.rapidScanMetrics!.averageProcessingTime.push(processingTime); window.rapidScanMetrics!.processedScans++; }, 10); }, { qr: `RAPID_SCAN_${i}` }); await page.waitForTimeout(scanDelay); } await page.waitForTimeout(2000); // Allow processing to complete // Check metrics const metrics = await page.evaluate(() => window.rapidScanMetrics!); // Should have processed most scans successfully expect(metrics.processedScans).toBeGreaterThan(rapidScanCount * 0.8); // 80% success rate // Average processing time should be reasonable if (metrics.averageProcessingTime.length > 0) { const avgTime = metrics.averageProcessingTime.reduce((a: number, b: number) => a + b) / metrics.averageProcessingTime.length; console.log(`Average scan processing time: ${avgTime.toFixed(2)}ms`); expect(avgTime).toBeLessThan(1000); // Should process in under 1 second } // Scanner should still be responsive await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); }); test('should handle rate limiting gracefully', async ({ page }) => { // Test the "slow down" message when scanning too rapidly await page.addInitScript(() => { window.rateLimitTesting = { warningsShown: 0, scansBlocked: 0 }; }); // Simulate scanning faster than 8 scans per second limit const rapidScans = 20; for (let i = 0; i < rapidScans; i++) { await page.evaluate((qr) => { window.dispatchEvent(new CustomEvent('mock-scan', { detail: { qr, timestamp: Date.now() } })); }, `RATE_LIMIT_TEST_${i}`); await page.waitForTimeout(50); // 50ms = 20 scans per second, well over limit } await page.waitForTimeout(1000); // Should show rate limiting feedback // (Implementation would show "Slow down" message or similar) await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); // Wait for rate limit to reset await page.waitForTimeout(2000); // 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); }); }); test.describe('Thermal and Resource Management', () => { 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'); await page.goto(`/scan?eventId=${testEventId}`); }); test('should adapt to simulated thermal throttling', async ({ page }) => { // Simulate device thermal state monitoring await page.addInitScript(() => { let thermalState = 'normal'; let frameReductionActive = false; window.thermalTesting = { thermalState, frameReductionActive, performanceAdaptations: [] }; // Simulate thermal state changes setTimeout(() => { thermalState = 'warm'; window.thermalTesting!.thermalState = thermalState; window.dispatchEvent(new CustomEvent('thermal-state-change', { detail: { state: thermalState } })); }, 2000); setTimeout(() => { thermalState = 'hot'; frameReductionActive = true; window.thermalTesting!.thermalState = thermalState; window.thermalTesting!.frameReductionActive = frameReductionActive; window.thermalTesting!.performanceAdaptations.push('reduced-fps'); window.dispatchEvent(new CustomEvent('thermal-state-change', { detail: { state: thermalState, adaptations: ['reduced-fps'] } })); }, 5000); }); await page.waitForTimeout(6000); // Check thermal adaptations were applied const thermalMetrics = await page.evaluate(() => window.thermalTesting!); expect(thermalMetrics?.thermalState).toBe('hot'); expect(thermalMetrics?.frameReductionActive).toBe(true); expect(thermalMetrics?.performanceAdaptations).toContain('reduced-fps'); // Scanner should still be functional despite thermal throttling await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); await expect(page.locator('video')).toBeVisible(); }); test('should monitor CPU and GPU usage patterns', async ({ page }) => { // Monitor performance metrics during scanning await page.addInitScript(() => { window.resourceMonitor = { cpuMetrics: [], renderMetrics: [], startTime: performance.now() }; // Monitor frame timing for GPU usage indication let lastFrameTime = performance.now(); function monitorFrames() { const now = performance.now(); const frameDelta = now - lastFrameTime; if (frameDelta > 0) { window.resourceMonitor!.renderMetrics.push({ frameDelta, timestamp: now }); } lastFrameTime = now; requestAnimationFrame(monitorFrames); } monitorFrames(); // Monitor performance observer if available if ('PerformanceObserver' in window) { try { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { window.resourceMonitor!.cpuMetrics.push({ name: entry.name, duration: entry.duration, timestamp: entry.startTime }); } }); observer.observe({ entryTypes: ['measure', 'navigation'] }); } catch (e) { console.log('Performance observer not fully supported'); } } }); // Run scanning simulation with resource monitoring for (let i = 0; i < 50; i++) { await page.evaluate((qr) => { window.dispatchEvent(new CustomEvent('mock-scan', { detail: { qr, timestamp: Date.now() } })); }, `RESOURCE_TEST_${i}`); await page.waitForTimeout(200); } // Collect resource usage data const resourceData = await page.evaluate(() => window.resourceMonitor!); // Analyze frame timing for performance issues if (resourceData?.renderMetrics.length > 0) { const avgFrameDelta = resourceData.renderMetrics.reduce((sum: number, m: { frameDelta: number }) => sum + m.frameDelta, 0) / resourceData.renderMetrics.length; const maxFrameDelta = Math.max(...resourceData.renderMetrics.map((m: { frameDelta: number }) => m.frameDelta)); console.log(`Average frame delta: ${avgFrameDelta.toFixed(2)}ms`); console.log(`Max frame delta: ${maxFrameDelta.toFixed(2)}ms`); // Frame times should generally be under 100ms for smooth operation expect(avgFrameDelta).toBeLessThan(100); expect(maxFrameDelta).toBeLessThan(500); // Allow some occasional spikes } // Scanner should remain responsive await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); }); test('should implement battery usage optimizations', async ({ page }) => { // Test battery API usage and power management const batteryInfo = await page.evaluate(async () => { if ('getBattery' in navigator) { try { const battery = await (navigator as any).getBattery(); return { level: battery.level, charging: battery.charging, chargingTime: battery.chargingTime, dischargingTime: battery.dischargingTime, supported: true }; } catch { return { supported: false }; } } return { supported: false }; }); if (batteryInfo.supported) { console.log(`Battery level: ${(batteryInfo.level * 100).toFixed(1)}%`); console.log(`Charging: ${batteryInfo.charging}`); // Test power-saving adaptations based on battery level if (batteryInfo.level < 0.2) { // Less than 20% battery await page.evaluate(() => { window.dispatchEvent(new CustomEvent('low-battery-detected', { detail: { level: 0.15, enablePowerSaving: true } })); }); await page.waitForTimeout(1000); // Should implement power-saving features // (Implementation would reduce frame rate, disable animations, etc.) } } // Test screen wake lock for preventing screen sleep during scanning const wakeLockSupported = await page.evaluate(async () => { if ('wakeLock' in navigator && navigator.wakeLock) { try { const wakeLock = await navigator.wakeLock.request('screen'); const isActive = !wakeLock.released; await wakeLock.release(); return { supported: true, worked: isActive }; } catch { return { supported: true, worked: false }; } } return { supported: false }; }); if (wakeLockSupported.supported) { expect(wakeLockSupported).toBeDefined(); } // Scanner should remain functional regardless of battery optimizations await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); }); }); test.describe('Memory Leak Detection', () => { 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'); await page.goto(`/scan?eventId=${testEventId}`); }); test('should not leak memory during extended scanning sessions', async ({ page }) => { test.setTimeout(90000); // Set up memory monitoring const initialMemory = await page.evaluate(() => { if (performance.memory) { return { used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize, timestamp: Date.now() }; } return null; }); if (!initialMemory) { test.skip(initialMemory === null, 'Memory monitoring not available in this browser'); return; } console.log(`Initial memory usage: ${(initialMemory.used / 1024 / 1024).toFixed(2)} MB`); // Perform intensive scanning operations const scanCycles = 20; const scansPerCycle = 25; for (let cycle = 0; cycle < scanCycles; cycle++) { // Rapid scanning cycle for (let scan = 0; scan < scansPerCycle; scan++) { await page.evaluate((data: { qr: string; cycle: number; scan: number }) => { // Create scan event with some data const scanData = { qr: data.qr, timestamp: Date.now(), deviceId: 'test-device', zone: 'Gate A', metadata: { cycle: data.cycle, scanInCycle: data.scan, randomData: Math.random().toString(36).substr(2, 10) } }; window.dispatchEvent(new CustomEvent('mock-scan', { detail: scanData })); }, { qr: `MEMORY_TEST_${cycle}_${scan}`, cycle, scan }); await page.waitForTimeout(50); } // Force garbage collection if possible await page.evaluate(() => { if (window.gc) { window.gc(); } }); // Check memory every 5 cycles if (cycle % 5 === 0) { const currentMemory = await page.evaluate(() => { if (performance.memory) { return { used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize, timestamp: Date.now() }; } return null; }); if (currentMemory) { const memoryIncreaseMB = (currentMemory.used - initialMemory.used) / 1024 / 1024; console.log(`Memory after cycle ${cycle}: ${(currentMemory.used / 1024 / 1024).toFixed(2)} MB (Δ ${memoryIncreaseMB.toFixed(2)} MB)`); } } await page.waitForTimeout(100); } // Final memory check await page.waitForTimeout(2000); // Allow cleanup const finalMemory = await page.evaluate(() => { if (performance.memory) { return { used: performance.memory.usedJSHeapSize, total: performance.memory.totalJSHeapSize, timestamp: Date.now() }; } return null; }); if (finalMemory) { const memoryIncreaseMB = (finalMemory.used - initialMemory.used) / 1024 / 1024; console.log(`Final memory usage: ${(finalMemory.used / 1024 / 1024).toFixed(2)} MB`); console.log(`Total memory increase: ${memoryIncreaseMB.toFixed(2)} MB`); // Memory increase should be reasonable (less than 20MB for this test) expect(memoryIncreaseMB).toBeLessThan(20); // Total memory usage should be reasonable (less than 100MB) expect(finalMemory.used / 1024 / 1024).toBeLessThan(100); } // Scanner should still be responsive await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); await expect(page.locator('video')).toBeVisible(); }); test('should clean up event listeners and timers', async ({ page }) => { // Track event listeners and intervals await page.addInitScript(() => { const originalAddEventListener = EventTarget.prototype.addEventListener; const originalRemoveEventListener = EventTarget.prototype.removeEventListener; const originalSetInterval = window.setInterval; const originalSetTimeout = window.setTimeout; const originalClearInterval = window.clearInterval; const originalClearTimeout = window.clearTimeout; window.leakDetector = { eventListeners: new Map(), intervals: new Set(), timeouts: new Set() }; EventTarget.prototype.addEventListener = function(type, listener, options) { const key = `${this.constructor.name}-${type}`; if (!window.leakDetector!.eventListeners.has(key)) { window.leakDetector!.eventListeners.set(key, 0); } const currentCount = window.leakDetector!.eventListeners.get(key) || 0; window.leakDetector!.eventListeners.set(key, currentCount + 1); return originalAddEventListener.call(this, type, listener, options); }; EventTarget.prototype.removeEventListener = function(type, listener, options) { const key = `${this.constructor.name}-${type}`; if (window.leakDetector!.eventListeners.has(key)) { const currentCount = window.leakDetector!.eventListeners.get(key) || 0; window.leakDetector!.eventListeners.set(key, currentCount - 1); } return originalRemoveEventListener.call(this, type, listener, options); }; // Override timer functions with proper type handling (window as any).setInterval = function(callback: TimerHandler, delay?: number) { const id = originalSetInterval(callback, delay); window.leakDetector!.intervals.add(id as number); return id; }; (window as any).clearInterval = function(id: number) { window.leakDetector!.intervals.delete(id); return originalClearInterval(id); }; (window as any).setTimeout = function(callback: TimerHandler, delay?: number) { const id = originalSetTimeout(callback, delay); window.leakDetector!.timeouts.add(id as number); return id; }; (window as any).clearTimeout = function(id: number) { window.leakDetector!.timeouts.delete(id); return originalClearTimeout(id); }; }); // Use scanner features that create event listeners await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)'); await page.fill('input[placeholder*="Gate"]', 'Memory Test Gate'); await page.click('button:has([data-testid="settings-icon"]), button:has(.lucide-settings)'); // Simulate multiple scans for (let i = 0; i < 10; i++) { await page.evaluate((qr) => { window.dispatchEvent(new CustomEvent('mock-scan', { detail: { qr, timestamp: Date.now() } })); }, `CLEANUP_TEST_${i}`); await page.waitForTimeout(100); } // Navigate away to trigger cleanup await page.goto('/dashboard'); await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible(); // Navigate back await page.goto(`/scan?eventId=${testEventId}`); await expect(page.locator('h1:has-text("Ticket Scanner")')).toBeVisible(); // Check for resource leaks const leakReport = await page.evaluate(() => window.leakDetector!); console.log('Event listeners:', Object.fromEntries(leakReport?.eventListeners || new Map())); console.log('Active intervals:', leakReport?.intervals.size || 0); console.log('Active timeouts:', leakReport?.timeouts.size || 0); // Should not have excessive active timers expect(leakReport?.intervals.size || 0).toBeLessThan(10); expect(leakReport?.timeouts.size || 0).toBeLessThan(20); }); test('should handle camera stream cleanup properly', async ({ page }) => { // Monitor video element and media streams await page.addInitScript(() => { // Track stream creation and cleanup const originalGetUserMedia = navigator.mediaDevices.getUserMedia; window.mediaStreamTracker = { createdStreams: 0, activeStreams: 0, cleanedUpStreams: 0 }; navigator.mediaDevices.getUserMedia = function(constraints) { window.mediaStreamTracker!.createdStreams++; window.mediaStreamTracker!.activeStreams++; return originalGetUserMedia.call(this, constraints).then(stream => { // Track when streams are stopped const tracks = stream.getTracks(); if (tracks.length === 0) return stream; tracks.forEach(track => { const originalStop = track.stop.bind(track); track.stop = function() { window.mediaStreamTracker!.activeStreams--; window.mediaStreamTracker!.cleanedUpStreams++; return originalStop(); }; }); return stream; }); }; }); // Initial camera access await expect(page.locator('video')).toBeVisible({ timeout: 10000 }); // Navigate away (should cleanup camera) await page.goto('/dashboard'); await page.waitForTimeout(2000); // Navigate back (should create new camera stream) await page.goto(`/scan?eventId=${testEventId}`); await expect(page.locator('video')).toBeVisible({ timeout: 10000 }); await page.waitForTimeout(2000); // Check stream management const streamReport = await page.evaluate(() => window.mediaStreamTracker!); console.log('Stream report:', streamReport); // Should have created streams expect(streamReport?.createdStreams || 0).toBeGreaterThan(0); // Should not have excessive active streams (leaking) expect(streamReport?.activeStreams || 0).toBeLessThanOrEqual(1); }); });