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:
703
reactrebuild0825/tests/battery-performance.spec.ts
Normal file
703
reactrebuild0825/tests/battery-performance.spec.ts
Normal file
@@ -0,0 +1,703 @@
|
||||
/**
|
||||
* 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 }) => {
|
||||
// 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) => {
|
||||
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, r) => 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
|
||||
}, 60000); // 60 second timeout for this test
|
||||
|
||||
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++) {
|
||||
const startTime = performance.now();
|
||||
|
||||
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, b) => 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, m) => sum + m.frameDelta, 0) / resourceData.renderMetrics.length;
|
||||
const maxFrameDelta = Math.max(...resourceData.renderMetrics.map(m => 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.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) {
|
||||
try {
|
||||
const wakeLock = await navigator.wakeLock.request('screen');
|
||||
const isActive = !wakeLock.released;
|
||||
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 }) => {
|
||||
// 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('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) => {
|
||||
// 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();
|
||||
}, 90000); // 90 second timeout for memory test
|
||||
|
||||
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);
|
||||
}
|
||||
window.leakDetector.eventListeners.set(key, window.leakDetector.eventListeners.get(key) + 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)) {
|
||||
window.leakDetector.eventListeners.set(key, window.leakDetector.eventListeners.get(key) - 1);
|
||||
}
|
||||
return originalRemoveEventListener.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
window.setInterval = function(callback, delay) {
|
||||
const id = originalSetInterval(callback, delay);
|
||||
window.leakDetector.intervals.add(id);
|
||||
return id;
|
||||
};
|
||||
|
||||
window.clearInterval = function(id) {
|
||||
window.leakDetector.intervals.delete(id);
|
||||
return originalClearInterval(id);
|
||||
};
|
||||
|
||||
window.setTimeout = function(callback, delay) {
|
||||
const id = originalSetTimeout(callback, delay);
|
||||
window.leakDetector.timeouts.add(id);
|
||||
return id;
|
||||
};
|
||||
|
||||
window.clearTimeout = function(id) {
|
||||
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));
|
||||
console.log('Active intervals:', leakReport.intervals.size);
|
||||
console.log('Active timeouts:', leakReport.timeouts.size);
|
||||
|
||||
// Should not have excessive active timers
|
||||
expect(leakReport.intervals.size).toBeLessThan(10);
|
||||
expect(leakReport.timeouts.size).toBeLessThan(20);
|
||||
});
|
||||
|
||||
test('should handle camera stream cleanup properly', async ({ page }) => {
|
||||
// Monitor video element and media streams
|
||||
await page.addInitScript(() => {
|
||||
const streamCount = 0;
|
||||
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 originalStop = stream.getTracks()[0].stop;
|
||||
stream.getTracks().forEach(track => {
|
||||
const trackStop = track.stop;
|
||||
track.stop = function() {
|
||||
window.mediaStreamTracker.activeStreams--;
|
||||
window.mediaStreamTracker.cleanedUpStreams++;
|
||||
return trackStop.call(this);
|
||||
};
|
||||
});
|
||||
|
||||
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).toBeGreaterThan(0);
|
||||
|
||||
// Should not have excessive active streams (leaking)
|
||||
expect(streamReport.activeStreams).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user