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:
2025-08-26 09:25:10 -06:00
parent d5c3953888
commit aa81eb5adb
438 changed files with 90509 additions and 2787 deletions

View 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);
});
});